我正在使用带有FireDAC的Delphi XE8来加载大型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);
将数据实际加载到SQLite数据库中的速度非常快,而且速度没问题。
让我失望的是在重复循环中将所有值分配给参数所花费的时间。
Params内置于FireDAC中,是一个TCollection。我无法访问源代码,因此我无法看到AsStrings和AsIntegers方法实际上在做什么。
为每个插入分配每个参数的每个值在我看来并不是加载此TCollection的非常有效的方法。加载这个有更快的方法吗?我想也许可以一次加载一整套参数,例如(IndiKey,HasData,...... FirstAncestralLoop)全部为一体。或者也许尽可能高效地加载我自己的TCollection,然后使用TCollection的Assign方法将我的TCollection复制到FireDAC的TCollection中。
所以我的问题是加载FireDAC所需的TCollection参数的最快方法是什么?
更新:我为Arnaud提供了一些时间安排。
如Using SQLite with FireDAC中所述(参见其数组DML部分):
从v 3.7.11开始,SQLite支持INSERT命令 多重价值。 FireDAC使用此功能来实现Array DML, 当Params.BindMode = pbByNumber时。否则,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)
现在关于这些时间的有趣之处在于,将这些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)
对于此测试的所有情况,加载参数的TCollection的开销大约需要503 ms。
因此,TCollection的加载似乎是每秒约400,000条记录。这是插入时间的重要部分,一旦我开始使用数百万的大型数据库,这个增加的时间将对我的程序用户非常明显。
我想改进这一点,但我还没有找到一种方法来加速Params的加载。
更新2:通过将我的所有代码放在StartTransaction和Commit之间,我可以节省大约10%的时间,这样就可以立即处理所有块。
但是我仍然在寻找一些方法来加快速度的TCollection。
另一个想法:
什么可能效果很好,如果可能的话可能会快16倍,就像the ParamValues method。这样可以同时分配多个参数,并且具有直接提供变量数组的附加优势,并且无需投射值。
它会像这样工作:
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]);
但是,ParamValues只会分配给第一组参数,即NumIndiParms = 0。
有没有办法为循环中的每个索引执行此操作,即NumIndiParms的每个实例?
Bounty:我真的想加快Params的加载速度。我现在正在为某人提供奖励,以帮助我找到一种方法来加速FireDAC中实现的Params数组TCollection的加载。
答案 0 :(得分:5)
听起来有点像我过早的优化。恕我直言,分析器将显示repeat .... until done
循环所需的时间比Execute
调用本身少得多。分配integer
几乎是即时的,就像分配string
一样,这要归功于Delphi string
类型的CopyOnWrite范例,它通过引用复制文本
请注意,实际上, SQLite3 中没有数组DML功能。 FireDac 通过创建多个插入来模拟数组DML,即执行
insert into indi values (?,?,?,....),(?,?,?,....),(?,?,?,....),....,(?,?,?,....);
AFAIK这是使用 SQLite3 插入数据的最快方法。至少在upcoming OTA feature可用之前。
还要确保将插入嵌套在多个事务中,并确保一次设置的参数数量不会太高。从我的测试中,如果要插入大量行,还应创建多个事务。维护单个事务会减慢进程。每次交易10000行是一个很好的数字,来自实验。
顺便说一句,我们的ORM可以自己完成所有this low-level plumbing,具体取决于它运行的后端引擎。 更新:听起来好像FireDac参数可能是您真正的瓶颈。因此,您应该绕过FireDAC,并直接将您的TCollection
内容与 SQlite3 引擎绑定。尝试例如our SynSQLite3.pas unit。请记住使用多次插入((?,?,?,....),(?,?,?,....),....
)准备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;
然后通过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);
完整的源代码是available online on paste.ee。
以下是1,000,000条记录的时间:
Prepared 1000000 rows in 874.54ms
Inserted 1000000 rows in 5.79s
如果计算得好,插入时每秒超过170,000行。在这里,ORM不是开销,它是一个优势。所有多INSERT工作,事务(每10000行),编组都将由框架完成。 TSQLRestBatch
会将所有内容作为JSON存储在内存中,然后立即计算SQL。我很好奇FireDAC的直接表现。如果需要,您可以切换到其他数据库 - 另一个RDBMS(MySQL,Oracle,MSSQL,FireBird)甚至MongoDB。只需添加一个新行。
希望它有所帮助!
答案 1 :(得分:1)
我能找到的最好的改进是用Values调用替换AsString和AsInteger调用。这可以防止为每个项目分配数据类型(字符串或整数),并节省大约10%的开销。
因此,小测试中的93 ms降至83 ms。 大测试中的503毫秒降至456毫秒。
FDQueryAddINDI.Params[0].Values[NumParams] := IndiKey;
FDQueryAddINDI.Params[1].Values[NumParams] := HasData;
FDQueryAddINDI.Params[2].Values[NumParams] := GedcomNames;
FDQueryAddINDI.Params[3].Values[NumParams] := Sex;
FDQueryAddINDI.Params[4].Values[NumParams] := Birthdate;
FDQueryAddINDI.Params[5].Values[NumParams] := Died;
FDQueryAddINDI.Params[6].Values[NumParams] := Deathdate;
FDQueryAddINDI.Params[7].Values[NumParams] := Changed;
FDQueryAddINDI.Params[8].Values[NumParams] := EventLinesNeedProcessing;
FDQueryAddINDI.Params[9].Values[NumParams] := EventLines;
FDQueryAddINDI.Params[10].Values[NumParams] := FamC;
FDQueryAddINDI.Params[11].Values[NumParams] := FamS;
FDQueryAddINDI.Params[12].Values[NumParams] := Linkinfo;
FDQueryAddINDI.Params[13].Values[NumParams] := ToDo;
FDQueryAddINDI.Params[14].Values[NumParams] := NextReportIndi;
FDQueryAddINDI.Params[15].Values[NumParams] := FirstAncestralLoop;
可以选择在打开文件时初始设置类型。也可以设置最大字符串长度。这对时间没有任何影响,设置长度不会减少使用的内存。类型和长度以这种方式设置:
FDQueryAddINDI.Params[0].DataType := ftString;
FDQueryAddINDI.Params[1].DataType := ftInteger;
FDQueryAddINDI.Params[2].DataType := ftString;
FDQueryAddINDI.Params[3].DataType := ftString;
FDQueryAddINDI.Params[4].DataType := ftString;
FDQueryAddINDI.Params[5].DataType := ftInteger;
FDQueryAddINDI.Params[6].DataType := ftString;
FDQueryAddINDI.Params[7].DataType := ftString;
FDQueryAddINDI.Params[8].DataType := ftInteger;
FDQueryAddINDI.Params[9].DataType := ftString;
FDQueryAddINDI.Params[10].DataType := ftInteger;
FDQueryAddINDI.Params[11].DataType := ftInteger;
FDQueryAddINDI.Params[12].DataType := ftInteger;
FDQueryAddINDI.Params[13].DataType := ftInteger;
FDQueryAddINDI.Params[14].DataType := ftInteger;
FDQueryAddINDI.Params[15].DataType := ftInteger;
FDQueryAddINDI.Params[0].Size := 20;
FDQueryAddINDI.Params[2].Size := 1;
FDQueryAddINDI.Params[3].Size := 1;
FDQueryAddINDI.Params[4].Size := 1;
FDQueryAddINDI.Params[6].Size := 1;
FDQueryAddINDI.Params[7].Size := 1;
FDQueryAddINDI.Params[9].Size := 1;