在Delphi FireDAC中加载数组DML的最快方法

lke*_*ler 8 delphi performance dml tcollection firedac

我使用Delphi XE8和FireDAC来加载一个大的SQLite数据库.为此,我使用Array DML执行技术一次有效地插入大量记录,如下所示:

FDQueryAddINDI.SQL.Text := 'insert into indi values ('
  + ':indikey, :hasdata, :gedcomnames, :sex, :birthdate, :died, '
  + ':deathdate, :changed, :eventlinesneedprocessing, :eventlines, '
  + ':famc, :fams, :linkinfo, :todo, :nextreportindi, :firstancestralloop'
  + ')';
FDQueryAddINDI.Params.Bindmode := pbByNumber; {more efficient than by name }
FDQueryAddINDI.Params.ArraySize := MaxParams; { large enough to load all of them } 

NumParams := 0;
repeat
  { the code to determin IndiKey,... is not shown, but goes here }

  FDQueryAddINDI.Params[0].AsStrings[NumParams] := IndiKey;   
  FDQueryAddINDI.Params[1].AsIntegers[NumParams] := HasData;
  FDQueryAddINDI.Params[2].AsStrings[NumParams] := GedcomNames;
  FDQueryAddINDI.Params[3].AsStrings[NumParams] := Sex;
  FDQueryAddINDI.Params[4].AsStrings[NumParams] := Birthdate;
  FDQueryAddINDI.Params[5].AsIntegers[NumParams] := Died;
  FDQueryAddINDI.Params[6].AsStrings[NumParams] := Deathdate;
  FDQueryAddINDI.Params[7].AsStrings[NumParams] := Changed;
  FDQueryAddINDI.Params[8].AsIntegers[NumParams] := EventLinesNeedProcessing;
  FDQueryAddINDI.Params[9].AsStrings[NumParams] := EventLines;
  FDQueryAddINDI.Params[10].AsIntegers[NumParams] := FamC;
  FDQueryAddINDI.Params[11].AsIntegers[NumParams] := FamS;
  FDQueryAddINDI.Params[12].AsIntegers[NumParams] := Linkinfo;
  FDQueryAddINDI.Params[13].AsIntegers[NumParams] := ToDo;
  FDQueryAddINDI.Params[14].AsIntegers[NumParams] := NextReportIndi;
  FDQueryAddINDI.Params[15].AsIntegers[NumParams] := FirstAncestralLoop;
  inc(NumParams);
until done;
FDQueryAddINDI.Params.ArraySize := NumParams;  { Reset to actual number }

FDQueryAddINDI.Execute(LogoAppForm.FDQueryAddINDI.Params.ArraySize);
Run Code Online (Sandbox Code Playgroud)

将数据实际加载到SQLite数据库中的速度非常快,我对速度没有任何问题.

让我失望的是在重复循环中将所有值分配给参数所花费的时间.

Params内置于FireDAC中,是一个TCollection.我无法访问源代码,因此我无法看到AsStrings和AsIntegers方法实际上在做什么.

为每个插入分配每个参数的每个值在我看来并不是加载此TCollection的非常有效的方法.加载这个有更快的方法吗?我想也许可以一次加载一整套参数,例如(IndiKey,HasData,... FirstAncestralLoop).或者也许尽可能高效地加载我自己的TCollection,然后使用TCollection的Assign方法将我的TCollection复制到FireDAC的TCollection中.

所以我的问题是加载FireDAC所需的TCollection参数的最快方法是什么?


更新:我为Arnaud提供了一些时间安排.

使用SQLite和FireDAC中所述(请参阅其Array DML部分):

从v 3.7.11开始,SQLite支持具有多个VALUES的INSERT命令.当Params.BindMode = pbByNumber时,FireDAC使用此功能来实现Array DML.否则,FireDAC会模拟阵列DML.

我已经测试了插入33,790条记录来更改数组大小(每次执行加载的记录数),并使用pbByName(用于仿真)和pbByNumber(使用多个值插入)计时加载时间.

这是时间:

Arraysize: 1, Executes: 33,790, Timing: 1530 ms (pbByName), 1449 ms (pbByNumber)
Arraysize: 10, Executes: 3,379, Timing: 1034 ms (pbByName), 782 ms (pbByNumber)
Arraysize: 100, Executes: 338, Timing:  946 ms (pbByName), 499 ms (pbByNumber)
Arraysize: 1000, Executes: 34, Timing: 890 ms (pbByName), 259 ms (pbByNumber)
Arraysize: 10000, Executes: 4, Timing: 849 ms (pbByName), 227 ms (pbByNumber)
Arraysize: 20000, Executes: 2, Timing: 594 ms (pbByName), 172 ms (pbByNumber)
Arraysize: 50000, Executes: 1, Timing: 94 ms (pbByName), 94 ms (pbByNumber)
Run Code Online (Sandbox Code Playgroud)

现在关于这些时间的有趣之处在于,将这些33,790条记录加载到TCollection中,每次单次测试运行需要93毫秒.无论是一次添加1还是一次添加10000都无关紧要,填充参数的TCollection的这一开销始终存在.

为了比较,我做了一个更大的测试,只有pbByNumber的198,522个插入:

Arraysize: 100, Executes: 1986, Timing: 2774 ms (pbByNumber)
Arraysize: 1000, Executes: 199, Timing: 1371 ms (pbByNumber)
Arraysize: 10000, Executes: 20, Timing: 1292 ms (pbByNumber)
Arraysize: 100000, Executes: 2, Timing: 894 ms (pbByNumber)
Arraysize: 1000000, Executes: 1, Timing: 506 ms (pbByNumber)
Run Code Online (Sandbox Code Playgroud)

对于此测试的所有情况,加载参数的TCollection的开销大约需要503 ms.

因此,TCollection的加载似乎是每秒约400,000条记录.这是插入时间的重要部分,一旦我开始使用数百万的大型数据库,这个增加的时间将对我的程序用户非常明显.

我想改进这一点,但我还没有找到一种方法来加速Params的加载.


更新2:通过将我的所有代码放在StartTransaction和Commit之间,我能够大约节省10%的时间,以便立即处理所有块.

但是我仍然在寻找一些方法来更快地加载Params的TCollection.


另一个想法:

什么可能效果很好,如果可能的话,可能会快16倍,就像ParamValues方法一样.这样可以同时分配多个参数,并且具有直接提供变量数组的附加优势,并且无需投射值.

它会像这样工作:

    FDQueryAddINDI.Params.ParamValues['indikey;hasdata;gedcomnames;sex;birthdate;died;deathdate;changed;eventlinesneedprocessing;eventlines;famc;fams;linkinfo;todo;nextreportindi;firstancestralloop']
       := VarArrayOf([Indikey, 0, ' ', ' ', ' ', 0, ' ', ' ', 1, ' ', -1, -1, -1, -1, -1, -1]);
Run Code Online (Sandbox Code Playgroud)

但是,ParamValues只会分配给第一组Params,即NumIndiParms = 0.

有没有办法为循环中的每个索引执行此操作,即NumIndiParms的每个实例?


赏金:我真的想加快Params的加载速度.我现在正在为某人提供奖励,以帮助我找到一种方法来加速FireDAC中实现的Params数组TCollection的加载.

Arn*_*hez 5

对我来说听起来有点过早的优化。恕我直言,分析器将显示该repeat .... until done循环比Execute调用本身花费的时间少得多。分配一个integer几乎是瞬间,就像分配string,由于写入时复制德尔福的模式string类型,复制文本的参考

请注意,实际上,SQLite3中没有数组DML功能。FireDac 通过创建多个插入(即执行)来模拟数组DML

insert into indi values (?,?,?,....),(?,?,?,....),(?,?,?,....),....,(?,?,?,....);
Run Code Online (Sandbox Code Playgroud)

AFAIK这是使用SQLite3插入数据的最快方法。至少要等到即将推出的OTA功能可用为止。

还要确保将插入内容嵌套在多个事务中,并且一次设置的参数数量不要太高。根据我的测试,如果要插入很多行,则还应该创建几个事务。维护单个事务会减慢该过程。根据实验,每笔交易10000行是一个好数字。

顺便说一句,我们的ORM可以自行运行所有这些低级管道,具体取决于其运行的后端引擎。

更新:听起来像FireDac参数可能是您真正的瓶颈。因此,您应该绕过FireDAC,并直接将TCollection内容与SQlite3引擎绑定。尝试例如我们的SynSQLite3.pas单元。请记住,使用多个插入((?,?,?,....),(?,?,?,....),....)准备INSERT语句,然后直接绑定您的值。BTW DB.pas可能是一个真正的瓶颈,这就是为什么我们整个ORM绕过此层的原因(但如果需要可以使用它)。

Update2:自从您提出要求以来,这就是使用mORMot的版本。

首先,定义记录:

type
  TSQLIndy = class(TSQLRecord)
...
  published
    property indikey: string read findikey write findikey;
    property hasdata: boolean read fhasdata write fhasdata;
    property gedcomnames: string read fgedcomnames write fgedcomnames;
    property sex: string read fsex write fsex;
    property birthdate: string read fbirthdate write fbirthdate;
    property died: boolean read fdied write fdied;
...
  end;
Run Code Online (Sandbox Code Playgroud)

然后,您通过ORM运行插入:

db := TSQLRestServerDB.CreateWithOwnModel([TSQLIndy],'test.db3');
db.CreateMissingTables; // will CREATE TABLE if not existing
batch := TSQLRestBatch.Create(db,TSQLIndy,10000);
try
  indy := TSQLIndy.Create;
  try
    for i := 1 to COUNT do begin
      indy.indikey := IntToString(i);
      indy.hasdata := i and 1=0;
      ...
      batch.Add(indy,true);
    end;
  finally
    indy.Free;
  end;
  db.BatchSend(batch);
Run Code Online (Sandbox Code Playgroud)

完整的源代码可在paste.ee上在线获得

以下是1,000,000条记录的时间安排:

Prepared 1000000 rows in 874.54ms
Inserted 1000000 rows in 5.79s
Run Code Online (Sandbox Code Playgroud)

如果计算正确,则每秒插入的行数超过170,000。在这里,ORM不是开销,而是优势。所有的多INSERT工作,事务(每10000行),封送处理均由框架完成。在TSQLRestBatch将存储的所有内容作为JSON在内存中,然后计算该SQL一次。我很好奇FireDAC在比较中的表现如何。如果需要,您将能够切换到其他数据库-另一个RDBMS(MySQL,Oracle,MSSQL,FireBird)甚至MongoDB。通过添加新行。

希望能帮助到你!