TClientDataSet在处理字符串字段时会占用过多内存。

7
当我尝试使用MCVE支持this question时,我被触发提出这个问题。我最近开始注意到TClientDataSet很快就会用完内存。在生产中,我遇到了一个问题,它无法加载大约60,000的数据集,这对我来说似乎非常低。客户端数据集通过连接到一个带有ADODataSet的提供程序进行连接,这个连接是正常的。我单独运行该查询并将结果输出到CSV,得到的文件大小小于30MB。所以我做了一个小测试,在客户端数据集中可以加载多达165K条记录,其中包含一个大小为4000的字符串字段。该字段的实际值只有3个字符,但对结果似乎没有影响。看起来每条记录至少占用那些4000个字符。4000 x 2字节x 165K记录= 1.3GB,因此它开始接近32位内存限制。如果我将其转换为备忘录字段,则可以轻松添加500万行。
program ClientDataSetTest;
{$APPTYPE CONSOLE}
uses SysUtils, DB, DBClient;

var
  c: TClientDataSet;
  i: Integer;
begin
  c := TClientDataSet.Create(nil);
  c.FieldDefs.Add('Id', ftInteger);
  c.FieldDefs.Add('Test', ftString, 4000); // Actually claims this much space...
  //c.FieldDefs.Add('Test', ftMemo); // Way more space efficient (and not notably slower)
  //c.FieldDefs.Add('Test', ftMemo, 1); // But specifying size doesn't have any effect.
  c.CreateDataSet;

  try
    i := 0;
    while i < 5000000 do
    begin
      c.Append;
      c['Id'] := i;
      c['Test'] := 'xyz';
      c.Post;

      if (i mod 1000) = 0 then
        WriteLn(i, c['Test']);

      Inc(i);
    end;

  except
    on e: Exception do
    begin
      c.Cancel;
      WriteLn('Error adding row', i);
      Writeln(e.ClassName, ': ', e.Message);
    end;
  end;

  c.SaveToFile('c:\temp\output.xml', dfXML);
  Writeln('Press ''any'' key');
  ReadLn;
end.

所以问题本身有点宽泛,但我想要一个解决方案,并能够通过更有效地使用字符串空间来加载更大的数据集。字段很大的原因是它们可以包含注释。对于大多数记录,这些注释将为空或很短,因此这是一种极大的浪费空间。
  • TClientDataSet是否可以配置为以不同的方式处理此类情况?我浏览了其属性,但找不到任何相关的内容。
  • 可以通过使用不同的字段类型来解决吗?我想到了ftMemo,但它还有一些其他缺点,比如大小不能用于截断,显示问题,如TDBGrid将其显示为(MEMO),而不是实际值。
  • 是否有TClientDataSet的替代品可以解决这个问题?这不仅涉及内存部分,还涉及与通过TProvider在此项目中主要使用的ADO组件的通信,因此并非所有内存数据集都能起作用。
对于最后一点,我碰巧找到了this question,在评论中提到了vgLib,但我发现所有关于它的信息都是错误的链接,而且我甚至不知道它是否能解决这个问题。显然,MidasLib的C++代码现在可用,但由于它是1.5MB的晦涩代码,所以在我深入研究之前,我想在这里询问一下。;)

1
值得一提的是,网格中显示的(MEMO)可以通过使用字段的OnGetText事件轻松修复。我经常使用它来在网格中显示前几个字符,并在双击行以显示完整内容时打开一个带有备忘录控件的表单。截断可以在OnBeforePost中处理。 - Ken White
@KenWhite 感谢您的建议!如果没有更通用的解决方案,那肯定是特定情况下的一个选项。这意味着 ADO 数据集中的字段类型也必须是备忘录,否则会产生错误。这意味着我必须将它作为 clob 从我的查询返回,否则会出现 ADO 数据集的错误。我需要弄清楚这样做的影响,但这不是一个微不足道的变化。 - GolezTrol
1
我想我应该说“解决”而不是“容易修复”。 :-) - Ken White
哎呀,只是在查询中添加 TO_CLOB 以适应备忘录字段,就使得(Oracle 11g)查询几乎慢了10倍,需要花费数分钟才能打开!也许不是死路,但至少是受到致命伤害了。:p - GolezTrol
1
是的,时间可能会成为一个问题。我没有意识到涉及到Oracle。只是出于好奇,Direct Oracle Access(DOA)组件还存在吗?当使用Delphi和Oracle时,它们比ADO或Delphi自己的驱动程序要好得多。我刚刚检查了一下 - 它们在AllRoundAutomations上仍然存在。 - Ken White
1
它们确实存在,我已经在特定情况下使用过它们。但是我的项目有100万行代码和数千个ADO组件,所以要替换它们并不容易。有些还很棘手,因为这样我就有了两个连接,必须小心不要在一个事务中混合它们。但是我也在PLSQL Developer中运行了查询(也可能使用了AllroundAutomations的DAC),但是在那里查询速度也慢得多,尽管似乎比在Delphi中快...抱歉我省略了所有上下文,但在原始问题中感觉没有必要提及。 - GolezTrol
2个回答

3
blob字段(大文本)和普通字段存储和检索数据的方式有所不同。blob字段不会将数据存储在记录缓冲区中(请参见TblobField.GetDataSize),它们在存储或检索数据时使用不同的方法。
每个记录的大小由调用TField.GetDataSize返回。对于TStringField,这是所需字符串大小+1。
TCustomClientDataSet.InitBufferPointers在计算FRecBufSize的值时使用此值,该值用作TCustomClientDataSet.AllocRecordBuffer中为每个记录分配的内存大小。
因此,回答您的问题:
- TClientDataSet无法配置为以任何不同的方式执行此操作。 - 可以通过其他字段类型解决,但它们必须从TBlobField继承。缓冲区大小预先分配,因此常规字段不能包含根据其内容而异的不同大小。 - 我不确定是否有现成的替代方案。Dev Express拥有一个dxMemData,但我不知道它是否遇到了相同的问题,也不知道它是否是可替换方案。

清楚了,感谢您的见解。确实是分配的方式。我有点希望能够影响它,但您已经明确表示客户数据集不支持这样的选项。 - GolezTrol

3

每当我需要一个相当长的"CDS"字符串字段时,我倾向于创建一个"memo"字段。除了上述的显示问题(这可以比较容易地解决),还有一些其他的限制,所以我有一个自定义的CDS后代。Hyperbase(而不是vglib)内部字符串格式相同,因此在这方面不会改变任何内容。顺便说一句,还有一些DACS(例如FireDAC),允许自定义和选择目标字段类型映射。不确定ADO组件是否可以被修补/增强以实现类似的功能。此外,据我所知,FireDAC数据集有控制内部字符串字段布局的选项("inline"或指向动态分配的指针),但它并不是CDS的1:1替代品。


有趣的是,特别是控制内部字符串字段布局的选项。我希望在TClientDataSet中也可以实现这一点,但似乎不行。但是,如果我能找到一种好的方法将char或varchar查询结果映射到memo字段,我也可以将其作为客户端数据集中的memo字段。我认为这可能是最好的前进方式。 - GolezTrol
如果不能用不同的数据访问组件替换ADO,可以检查其内部是否可能实现字段类型映射功能。 - Vladimir Ulchenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接