2015-07-20 6 views
7

Ich verwende Delphi XE8 mit FireDAC, um eine große SQLite-Datenbank zu laden. Dazu verwende ich die Array DML Ausführungstechnik, um effizient eine große Anzahl von Datensätzen auf einmal einsetzen, wie folgt aus:Der schnellste Weg zum Laden einer Array-DML in Delphi FireDAC

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); 

Die eigentliche Laden der Daten in die SQLite-Datenbank ist sehr schnell, und ich habe kein Problem mit der Geschwindigkeit davon.

Was mich verlangsamt, ist die Zeit, die in der Wiederholungsschleife benötigt wird, um alle Werte den Parametern zuzuweisen.

Die Parameter sind in FireDAC integriert und sind eine TCollection. Ich habe keinen Zugriff auf den Quellcode, daher kann ich nicht sehen, was die AsStrings- und AsIntegers-Methoden tatsächlich tun.

Das Zuweisen jedes Werts zu jedem Parameter für jede Einfügung scheint mir keine sehr effiziente Möglichkeit zu sein, diese TCollection zu laden. Gibt es eine schnellere Möglichkeit, dies zu laden? Ich denke, vielleicht eine Möglichkeit, einen ganzen Satz von Parametern gleichzeitig zu laden, z.B. (IndiKey, HasData, ... FirstAncestralLoop) alle als eins. Oder vielleicht, um meine eigene TCollection so effizient wie möglich zu laden, und dann die Assign-Methode der TCollection zu verwenden, um meine TCollection in die TCollection des FireDAC zu kopieren.

Also meine Frage ist, was wäre der schnellste Weg zum Laden dieser TCollection von Parametern, die FireDAC benötigt?


Update: Ich bin ein paar Timings für Arnaud enthalten.

Wie in Using SQLite with FireDAC angegeben (seine Array DML Abschnitt):

mit v Starten 3.7.11 unterstützt SQLite den INSERT-Befehl mit mehr Werten. FireDAC verwendet diese Funktion zum Implementieren von Array DML, , wenn Params.BindMode = pbByNumber. Andernfalls emuliert FireDAC Array DML.

Ich habe 33.790 Datensätze getestet Einsetzen des Arraysize Ändern (Anzahl der Datensätze pro ausführen zu laden) und zeitlich gesteuert, die Ladezeit mit beiden pbByName (für die Emulation) und pbByNumber (mehrere Werte einfügen verwenden).

Dies war das Timing:

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) 

Nun das Interessante an diesen Zeitpunkt ist, dass das Laden dieser 33.790 Datensätze in die TCollection ist ein vollen 93 ms jeden einzelnen Testlauf nehmen. Es spielt keine Rolle, ob sie 1 gleichzeitig oder 10000 gleichzeitig hinzugefügt werden, dieser Overhead zum Füllen der TCollection of Params ist immer da.

Zum Vergleich habe ich einen größeren Test mit 198.522 Einsätzen nur für pbByNumber:

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) 

Für alle Fälle dieses Tests, der Aufwand für die TCollection von Param Laden dauert etwa 503 ms.

So scheint das Laden der TCollection bei etwa 400.000 Datensätze pro Sekunde zu sein. Dies ist ein bedeutender Teil der Einfügezeit, und sobald ich anfange, mit großen Datenbanken in Millionenhöhe zu arbeiten, wird diese zusätzliche Zeit für den Benutzer meines Programms ziemlich auffällig sein.

Ich möchte dies verbessern, aber ich habe noch keinen Weg gefunden, um das Laden der Params zu beschleunigen.


Update 2: Ich konnte über eine 10% ige Zeitverbesserung erhalten, indem all meine Codes zwischen einer Starttransaction setzen und einem Commit, so dass alle Blöcke auf einmal verarbeitet werden.

Aber ich bin immer noch auf der Suche nach einer Möglichkeit, die TCollection of Params viel schneller zu laden.


Eine andere Idee:

Was könnte gut funktionieren und schneller bis zu 16-mal sein könnte, wenn es möglich wäre, so etwas wie the ParamValues method wäre. Dies weist mehrere Parameter gleichzeitig zu und hat den zusätzlichen Vorteil, dass direkt ein variantes Array bereitgestellt wird und die Notwendigkeit, Werte zu konvertieren, vermieden wird.

Es würde so funktionieren:

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]); 

jedoch ParamValues ​​nur auf den ersten Satz von Param vergeben wird, dh wo NumIndiParms = 0.

Gibt es eine Möglichkeit, dies für jeden Index zu tun in der Schleife, dh jede Instanz von NumIndiParms?


Bounty: Ich möchte wirklich das Laden der Params beschleunigen. Ich biete jetzt ein Kopfgeld für jemanden an, der mir hilft, das Laden des Params-Arrays TCollection, wie es in FireDAC implementiert ist, zu beschleunigen.

Antwort

3

Klingt ein bisschen wie vorzeitige Optimierung für mich. IMHO würde ein Profiler zeigen, dass die repeat .... until done Schleife viel weniger Zeit als der Execute Anruf selbst nimmt. Zuweisen eines integer ist fast sofort, genau wie ein string zuweisen, dank der CopyOnWrite Paradigma von Delphi string Typ, der den Text durch Referenz kopiert.

Beachten Sie, dass es in der Praxis keine Array-DML-Funktion in SQLite3 gibt. FireDac emuliert Array DML durch mehrere Insertion Erstellen, d.h.

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

Ausführung AFAIK Dies ist der schnellste Weg, um Daten des Einsetzens SQLite3 verwenden. Zumindest bis die upcoming OTA feature verfügbar ist.

Vergewissern Sie sich auch, dass Sie Ihre Einfügung innerhalb mehrere Transaktionen verschachteln, und dass die Anzahl der Parameter auf einmal nicht zu hoch ist. Aus meinen Tests sollten Sie auch mehrere Transaktionen erstellen, wenn Sie viele einzufügende Zeilen haben. Das Warten einer einzelnen Transaktion verlangsamt den Prozess. 10000 Zeilen pro Transaktion sind eine gute Zahl aus dem Experiment.

BTW, unser ORM ist in der Lage, alle this low-level plumbing eigenständig zu machen, abhängig von der Backend-Engine, auf der es läuft.

Update: Klingt, als ob die FireDac-Parameter in Ihrem Fall ein echter Flaschenhals sein könnten.Sie sollten daher FireDAC umgehen und Ihren TCollection Inhalt direkt mit dem SQlite3 Motor verbinden. Versuchen Sie z.B. our SynSQLite3.pas unit. Denken Sie daran, Ihre INSERT-Anweisung mit einer Mehrfacheinfügung vorzubereiten ((?,?,?,....),(?,?,?,....),....) und binden Sie dann direkt Ihre Werte. BTW DB.pas kann ein echter Flaschenhals sein, deshalb umgeht unser gesamtes ORM diese Schicht (kann sie aber bei Bedarf verwenden).

Update2: Da Sie danach gefragt, hier ist eine Version mit mORMot.

Zuerst definieren Sie Ihren Eintrag:

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; 

Dann führen Sie das Einfügen über die 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); 

Der vollständige Quellcode ist available online on paste.ee.

Hier sind der Zeitpunkt für 1.000.000 Datensätze:

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

Wenn ich auch berechnen, ist es mehr als 170.000 Zeilen pro Sekunde für die Insertion ist. Hier ist das ORM kein Overhead, es ist ein Vorteil. Alle multi INSERT Arbeit, Transaktionen (alle 10000 Zeilen), Marshalling würde durch das Framework erfolgen. Die TSQLRestBatch würde den gesamten Inhalt als JSON im Speicher speichern und dann die SQL sofort berechnen. Ich bin gespannt, wie direkt FireDAC im Vergleich funktioniert. Und Sie könnten bei Bedarf zu einer anderen Datenbank wechseln - einem anderen RDBMS (MySQL, Oracle, MSSQL, FireBird) oder sogar MongoDB. Indem Sie einfach eine neue Zeile hinzufügen.

Hoffe es hilft!

+0

Danke, Arnaud, für Ihre Ideen. Dies ist keine vorzeitige Optimierung. Ich mache jetzt gerade die Optimierung. :-) Ich hatte vorher verschiedene Arraygrößen pro Transaktion getestet und habe nun einige davon in meinem Update zu meiner Frage hinzugefügt. Ich stimme Ihnen zu, dass eine Integer- oder String-Zuweisung sofort erfolgen sollte, daher ist offensichtlich, dass beim Hinzufügen zu einer TCollection viel mehr passiert als nur die Zuweisungen. Hoffentlich kann mir jemand einen Einblick geben, wie ich diesen Teil schneller machen kann. – lkessler

+0

@lkessler Verwenden Sie also nicht die FireDAC-Abstraktion, sondern direkt die SQLite3-Ebene. Siehe mein Update. –

+0

@lkessler Ich habe gerade die Methode 'TSQLRequest.BindS' optimiert, um jegliche Speicherzuordnung während der' string' Parameterbindung zu vermeiden. Es kann helfen, die Zeichenfolgenwerte direkt von Ihrer 'TCollection' zu binden. Siehe [dieses Commit] (http://synopse.info/fossil/info/88ef687fa2). –

1

Die beste Verbesserung, die ich finden kann, besteht darin, die AsString- und AsInteger-Aufrufe durch Werteaufrufe zu ersetzen. Dies verhindert die Zuordnung des Datentyps (String oder Integer) zu jedem Element und spart ca. 10% des Overheads.

Also 93 ms im kleinen Test ist bis 83 ms. Und 503 ms im großen Test ist auf 456 ms herunter.

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; 

Der Typ kann optional initialisiert werden, wenn die Datei geöffnet wird. Maximale Stringlängen können ebenfalls eingestellt werden. Dies hat keinen Einfluss auf die Zeit, und das Einstellen der Längen reduziert nicht den verwendeten Speicher. Die Typen und Längen werden auf diese Weise eingestellt:

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; 
Verwandte Themen