8

Delphi XE8 と FireDAC を使用して、大規模な SQLite データベースをロードしています。そのために、配列 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) をすべて 1 つとしてロードします。または、自分の TCollection をできる限り効率的にロードし、TCollection の Assign メソッドを使用して、自分の TCollection を FireDAC の TCollection にコピーすることもできます。

私の質問は、FireDAC が必要とするパラメータのこの TCollection をロードする最速の方法は何でしょうか?


更新: Arnaud のタイミングをいくつか含めています。

Using SQLite with FireDAC で説明されているように(配列 DML セクションを参照):

v 3.7.11 以降、SQLite は複数の値を持つ INSERT コマンドをサポートしています。Params.BindMode = pbByNumber の場合、FireDAC はこの機能を使用して配列 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)

これらのタイミングの興味深い点は、これらの 33,790 レコードを TCollection にロードするのに、1 回のテスト実行ごとに 93 ミリ秒かかっていることです。一度に 1 つずつ追加されるか、一度に 10000 ずつ追加されるかは関係ありません。Param の 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)

このテストのすべてのケースで、Params の TCollection をロードするオーバーヘッドは約 503 ミリ秒かかります。

したがって、TCollection の読み込みは 1 秒あたり約 400,000 レコードのようです。これは挿入時間のかなりの部分であり、何百万もの大規模なデータベースで作業を開始すると、この追加時間はプログラムのユーザーにとって非常に顕著になります。

これを改善したいのですが、Params の読み込みを高速化する方法をまだ見つけていません。


更新 2: すべてのコードを StartTransaction と Commit の間に配置して、すべてのブロックが一度に処理されるようにすることで、約 10% の時間を短縮できました。

しかし、Params の TCollection をより高速にロードする方法をまだ探しています。


もう1つのアイデア:

うまく機能し、可能であれば最大 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]);

ただし、ParamValues は、Param の最初のセット、つまり NumIndiParms = 0 にのみ割り当てられます。

ループ内の各インデックス、つまり NumIndiParms のすべてのインスタンスに対してこれを行う方法はありますか?


Bounty: Params の読み込みを高速化したいです。現在、FireDAC に実装されている Params 配列 TCollection の読み込みを高速化する方法を見つけてくれる人に報奨金を提供しています。

4

2 に答える 2

7

私には時期尚早の最適化のように聞こえます。私見プロファイラーは、ループが呼び出し自体repeat .... until doneよりもはるかに短い時間を要することを示します。Executeの割り当ては、参照によってテキストをコピーするDelphi型のCopyOnWriteパラダイムのおかげで、 のinteger割り当てと同じようにほとんど瞬時に行われます。stringstring

実際には、 SQLite3には配列 DML 機能がないことに注意してください。FireDacは、複数の挿入を作成することで配列 DML をエミュレートします。

insert into indi values (?,?,?,....),(?,?,?,....),(?,?,?,....),....,(?,?,?,....);

私の知る限り、これはSQLite3を使用してデータを挿入する最速の方法です。少なくとも、今後の OTA 機能が利用可能になるまで。

また、複数のトランザクション内で挿入をネストし、一度に設定するパラメーターの数が多すぎないようにしてください。私のテストから、挿入する行が多い場合は、いくつかのトランザクションも作成する必要があります。単一のトランザクションを維持すると、プロセスが遅くなります。実験から、トランザクションあたり 10000 行は適切な数です。

ところで、私たちの ORM は、それが実行されるバックエンド エンジンに応じて、このすべての低レベルの配管を独自に行うことができます。

更新: FireDac パラメーターが実際のボトルネックである可能性がある場合のように聞こえます。TCollectionしたがって、FireDAC をバイパスし、コンテンツをSQlite3エンジンに直接バインドする必要があります。SynSQLite3.pas unitなどを試してみてください。複数の挿入 ( ) を使用して INSERT ステートメントを準備し(?,?,?,....),(?,?,?,....),....、値を直接バインドすることを忘れないでください。ところで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);

完全なソース コードは、paste.ee でオンラインで入手できます

1,000,000 レコードのタイミングは次のとおりです。

Prepared 1000000 rows in 874.54ms
Inserted 1000000 rows in 5.79s

よく計算すると1秒あたり17万行以上の挿入です。ここで、ORM はオーバーヘッドではなく、利点です。すべてのマルチ INSERT 作業、トランザクション (10000 行ごと)、マーシャリングはフレームワークによって行われます。すべてのTSQLRestBatchコンテンツを JSON としてメモリに格納し、一度に SQL を計算します。ダイレクト FireDAC がどのように機能するかを比較してみたいと思います。また、必要に応じて、別のデータベース (別の RDBMS (MySQL、Oracle、MSSQL、FireBird) または MongoDB) に切り替えることもできます。新しい行を追加するだけです。

それが役に立てば幸い!

于 2015-07-20T18:08:03.643 に答える