Die Optimierung von SQLite ist schwierig. Die Bulk-Insert-Leistung einer C-Anwendung kann von 85 Inserts pro Sekunde bis zu über 96.000 Inserts pro Sekunde variieren!
Hintergrund: Wir verwenden SQLite als Teil einer Desktop-Anwendung. In XML-Dateien sind große Mengen an Konfigurationsdaten gespeichert, die analysiert und zur weiteren Verarbeitung bei der Initialisierung der Anwendung in eine SQLite-Datenbank geladen werden. SQLite ist ideal für diese Situation, da es schnell ist, keine spezielle Konfiguration erfordert und die Datenbank als einzelne Datei auf der Festplatte gespeichert ist.
Begründung: Anfangs war ich von der Leistung, die ich sah, enttäuscht. Es stellt sich heraus, dass die Leistung von SQLite erheblich variieren kann (sowohl für Masseneinfügungen als auch für Auswahlen), je nachdem, wie die Datenbank konfiguriert ist und wie Sie die API verwenden. Es war keine triviale Angelegenheit, herauszufinden, welche Optionen und Techniken es gab. Daher hielt ich es für ratsam, diesen Community-Wiki-Eintrag zu erstellen, um die Ergebnisse mit Stack Overflow-Lesern zu teilen und anderen die Mühe derselben Untersuchungen zu ersparen.
Das Experiment: Anstatt nur über Leistungstipps im allgemeinen Sinne zu sprechen (dh "Verwenden Sie eine Transaktion!" ), Hielt ich es für das Beste, C-Code zu schreiben und die Auswirkungen verschiedener Optionen tatsächlich zu messen . Wir beginnen mit einigen einfachen Daten:
- Eine 28 MB TAB-getrennte Textdatei (ca. 865.000 Datensätze) des vollständigen Transitplans für die Stadt Toronto
- Mein Testcomputer ist ein 3,60 GHz P4 unter Windows XP.
- Der Code wird mit Visual C ++ 2005 als "Release" mit "Full Optimization" (/ Ox) und Favor Fast Code (/ Ot) kompiliert .
- Ich verwende die SQLite "Amalgamation", die direkt in meine Testanwendung kompiliert wurde. Die SQLite-Version, die ich zufällig habe, ist etwas älter (3.6.7), aber ich vermute, dass diese Ergebnisse mit der neuesten Version vergleichbar sind (bitte hinterlassen Sie einen Kommentar, wenn Sie etwas anderes denken).
Schreiben wir einen Code!
Der Code: Ein einfaches C-Programm, das die Textdatei zeilenweise liest, die Zeichenfolge in Werte aufteilt und die Daten dann in eine SQLite-Datenbank einfügt. In dieser "Basis" -Version des Codes wird die Datenbank erstellt, aber wir werden keine Daten einfügen:
/*************************************************************
Baseline code to experiment with SQLite performance.
Input data is a 28 MB TAB-delimited text file of the
complete Toronto Transit System schedule/route info
from http://www.toronto.ca/open/datasets/ttc-routes/
**************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "sqlite3.h"
#define INPUTDATA "C:\\TTC_schedule_scheduleitem_10-27-2009.txt"
#define DATABASE "c:\\TTC_schedule_scheduleitem_10-27-2009.sqlite"
#define TABLE "CREATE TABLE IF NOT EXISTS TTC (id INTEGER PRIMARY KEY, Route_ID TEXT, Branch_Code TEXT, Version INTEGER, Stop INTEGER, Vehicle_Index INTEGER, Day Integer, Time TEXT)"
#define BUFFER_SIZE 256
int main(int argc, char **argv) {
sqlite3 * db;
sqlite3_stmt * stmt;
char * sErrMsg = 0;
char * tail = 0;
int nRetCode;
int n = 0;
clock_t cStartClock;
FILE * pFile;
char sInputBuf [BUFFER_SIZE] = "\0";
char * sRT = 0; /* Route */
char * sBR = 0; /* Branch */
char * sVR = 0; /* Version */
char * sST = 0; /* Stop Number */
char * sVI = 0; /* Vehicle */
char * sDT = 0; /* Date */
char * sTM = 0; /* Time */
char sSQL [BUFFER_SIZE] = "\0";
/*********************************************/
/* Open the Database and create the Schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
/*********************************************/
/* Open input file and import into Database*/
cStartClock = clock();
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
/* ACTUAL INSERT WILL GO HERE */
n++;
}
fclose (pFile);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_close(db);
return 0;
}
Die Kontrolle"
Wenn Sie den Code so ausführen, wie er ist, werden keine Datenbankoperationen ausgeführt, aber wir erhalten eine Vorstellung davon, wie schnell die E / A- und Zeichenfolgenverarbeitungsoperationen der C-Rohdatei sind.
864913 Datensätze in 0,94 Sekunden importiert
Großartig! Wir können 920.000 Einfügungen pro Sekunde machen, vorausgesetzt, wir machen eigentlich keine Einfügungen :-)
Das "Worst-Case-Szenario"
Wir werden die SQL-Zeichenfolge mit den aus der Datei gelesenen Werten generieren und diese SQL-Operation mit sqlite3_exec aufrufen:
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", sRT, sBR, sVR, sST, sVI, sDT, sTM);
sqlite3_exec(db, sSQL, NULL, NULL, &sErrMsg);
Dies wird langsam sein, da SQL für jede Einfügung in VDBE-Code kompiliert wird und jede Einfügung in einer eigenen Transaktion erfolgt. Wie langsam?
864913 Datensätze in 9933,61 Sekunden importiert
Huch! 2 Stunden und 45 Minuten! Das sind nur 85 Einsätze pro Sekunde.
Verwenden einer Transaktion
Standardmäßig wertet SQLite jede INSERT / UPDATE-Anweisung innerhalb einer eindeutigen Transaktion aus. Wenn Sie eine große Anzahl von Einfügungen ausführen, ist es ratsam, Ihren Vorgang in eine Transaktion einzuschließen:
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
...
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
864913 Datensätze in 38,03 Sekunden importiert
Das ist besser. Durch einfaches Verpacken aller unserer Beilagen in einer einzigen Transaktion wurde unsere Leistung auf 23.000 Beilagen pro Sekunde verbessert .
Verwenden einer vorbereiteten Anweisung
Die Verwendung einer Transaktion war eine enorme Verbesserung, aber das Neukompilieren der SQL-Anweisung für jede Einfügung ist nicht sinnvoll, wenn wir immer wieder dasselbe SQL verwenden. Lassen Sie uns sqlite3_prepare_v2
unsere SQL-Anweisung einmal kompilieren und dann unsere Parameter mit folgender Anweisung an diese Anweisung binden sqlite3_bind_text
:
/* Open input file and import into the database */
cStartClock = clock();
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, @RT, @BR, @VR, @ST, @VI, @DT, @TM)");
sqlite3_prepare_v2(db, sSQL, BUFFER_SIZE, &stmt, &tail);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
sqlite3_bind_text(stmt, 1, sRT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, sBR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, sVR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, sST, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 5, sVI, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, sDT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 7, sTM, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
n++;
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
864913 Datensätze in 16,27 Sekunden importiert
Nett! Es ist ein bisschen mehr Code (vergessen Sie nicht zu nennen sqlite3_clear_bindings
und sqlite3_reset
), aber wir haben mehr als unsere Leistung verdoppelt 53.000 Einsätze pro Sekunde.
PRAGMA synchron = AUS
Standardmäßig wird SQLite angehalten, nachdem ein Schreibbefehl auf Betriebssystemebene ausgegeben wurde. Dies garantiert, dass die Daten auf die Festplatte geschrieben werden. Durch die Einstellung synchronous = OFF
weisen wir SQLite an, die Daten einfach zum Schreiben an das Betriebssystem zu übergeben und dann fortzufahren. Es besteht die Möglichkeit, dass die Datenbankdatei beschädigt wird, wenn der Computer einen katastrophalen Absturz (oder Stromausfall) erleidet, bevor die Daten auf den Plattenteller geschrieben werden:
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
864913 Datensätze in 12,41 Sekunden importiert
Die Verbesserungen sind jetzt kleiner, aber wir haben bis zu 69.600 Einfügungen pro Sekunde.
PRAGMA journal_mode = MEMORY
Erwägen Sie, das Rollback-Journal durch Auswerten im Speicher zu speichern PRAGMA journal_mode = MEMORY
. Ihre Transaktion wird schneller sein, aber wenn Sie die Stromversorgung verlieren oder Ihr Programm während einer Transaktion abstürzt, kann Ihre Datenbank mit einer teilweise abgeschlossenen Transaktion in einem beschädigten Zustand belassen werden:
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
864913 Datensätze in 13,50 Sekunden importiert
Etwas langsamer als die vorherige Optimierung mit 64.000 Einfügungen pro Sekunde.
PRAGMA synchron = OFF und PRAGMA journal_mode = MEMORY
Kombinieren wir die beiden vorherigen Optimierungen. Es ist etwas riskanter (im Falle eines Absturzes), aber wir importieren nur Daten (keine Bank):
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
864913 Datensätze in 12.00 Sekunden importiert
Fantastisch! Wir können 72.000 Einfügungen pro Sekunde ausführen.
Verwenden einer In-Memory-Datenbank
Lassen Sie uns nur zum Spaß auf allen vorherigen Optimierungen aufbauen und den Dateinamen der Datenbank neu definieren, damit wir vollständig im RAM arbeiten:
#define DATABASE ":memory:"
864913 Datensätze in 10,94 Sekunden importiert
Es ist nicht besonders praktisch, unsere Datenbank im RAM zu speichern, aber es ist beeindruckend, dass wir 79.000 Einfügungen pro Sekunde ausführen können .
Refactoring von C-Code
Obwohl dies keine spezielle SQLite-Verbesserung ist, gefallen mir die zusätzlichen char*
Zuweisungsoperationen in der while
Schleife nicht. Lassen Sie uns diesen Code schnell umgestalten, um die Ausgabe strtok()
direkt an zu übergeben sqlite3_bind_text()
, und den Compiler versuchen lassen, die Dinge für uns zu beschleunigen:
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sqlite3_bind_text(stmt, 1, strtok (sInputBuf, "\t"), -1, SQLITE_TRANSIENT); /* Get Route */
sqlite3_bind_text(stmt, 2, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Branch */
sqlite3_bind_text(stmt, 3, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Version */
sqlite3_bind_text(stmt, 4, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Stop Number */
sqlite3_bind_text(stmt, 5, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Vehicle */
sqlite3_bind_text(stmt, 6, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Date */
sqlite3_bind_text(stmt, 7, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Time */
sqlite3_step(stmt); /* Execute the SQL Statement */
sqlite3_clear_bindings(stmt); /* Clear bindings */
sqlite3_reset(stmt); /* Reset VDBE */
n++;
}
fclose (pFile);
Hinweis: Wir verwenden wieder eine echte Datenbankdatei. In-Memory-Datenbanken sind schnell, aber nicht unbedingt praktisch
864913 Datensätze in 8,94 Sekunden importiert
Durch eine geringfügige Überarbeitung des in unserer Parameterbindung verwendeten Zeichenfolgenverarbeitungscodes konnten 96.700 Einfügungen pro Sekunde ausgeführt werden. Ich denke, man kann mit Sicherheit sagen, dass dies schnell genug ist . Wenn wir anfangen, andere Variablen (z. B. Seitengröße, Indexerstellung usw.) zu optimieren, wird dies unser Maßstab sein.
Zusammenfassung (bisher)
Ich hoffe du bist noch bei mir! Der Grund, warum wir diesen Weg eingeschlagen haben, ist, dass die Leistung von Masseneinfügungen mit SQLite so stark variiert und es nicht immer offensichtlich ist, welche Änderungen vorgenommen werden müssen, um unseren Betrieb zu beschleunigen. Mit demselben Compiler (und denselben Compileroptionen), derselben SQLite-Version und denselben Daten haben wir unseren Code und unsere Verwendung von SQLite optimiert, um von einem Worst-Case-Szenario mit 85 Einfügungen pro Sekunde auf über 96.000 Einfügungen pro Sekunde zu gelangen!
CREATE INDEX dann INSERT vs. INSERT dann CREATE INDEX
Bevor wir mit der SELECT
Leistungsmessung beginnen, wissen wir, dass wir Indizes erstellen werden. In einer der folgenden Antworten wurde vorgeschlagen, dass es beim Masseneinfügen schneller ist, den Index nach dem Einfügen der Daten zu erstellen (im Gegensatz zum erstmaligen Erstellen des Index und dann zum Einfügen der Daten). Lass es uns versuchen:
Index erstellen und dann Daten einfügen
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
864913 Datensätze in 18,13 Sekunden importiert
Daten einfügen, dann Index erstellen
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
864913 Datensätze in 13,66 Sekunden importiert
Wie erwartet sind Bulk-Einfügungen langsamer, wenn eine Spalte indiziert ist. Es macht jedoch einen Unterschied, ob der Index nach dem Einfügen der Daten erstellt wird. Unsere Basislinie ohne Index beträgt 96.000 Einfügungen pro Sekunde. Wenn Sie zuerst den Index erstellen und dann Daten einfügen, erhalten Sie 47.700 Einfügungen pro Sekunde. Wenn Sie zuerst die Daten einfügen und dann den Index erstellen, erhalten Sie 63.300 Einfügungen pro Sekunde.
Ich würde gerne Vorschläge für andere Szenarien machen, um es zu versuchen ... Und werde bald ähnliche Daten für SELECT-Abfragen zusammenstellen.
sqlite3_clear_bindings(stmt);
? Sie legen die Bindungen jedes Mal fest, wenn dies ausreichen sollte: Vor dem ersten Aufruf von sqlite3_step () oder unmittelbar nach sqlite3_reset () kann die Anwendung eine der Schnittstellen sqlite3_bind () aufrufen, um Werte an die Parameter anzuhängen. Jeder Aufruf von sqlite3_bind () überschreibt vorherige Bindungen für denselben Parameter (siehe: sqlite.org/cintro.html ). In den Dokumenten ist nichts für diese Funktion enthalten, was besagt , dass Sie sie aufrufen müssen.
feof()
, um die Beendigung Ihrer Eingangsschleife zu steuern. Verwenden Sie das von zurückgegebene Ergebnis fgets()
. stackoverflow.com/a/15485689/827263