Verbessern Sie die INSERT-pro-Sekunde-Leistung von SQLite

2975

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_v2unsere 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_bindingsund 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 = OFFweisen 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 whileSchleife 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 SELECTLeistungsmessung 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.

Mike Willekes
quelle
8
Guter Punkt! In unserem Fall handelt es sich um ungefähr 1,5 Millionen Schlüssel / Wert-Paare, die aus XML- und CSV-Textdateien in 200.000 Datensätze gelesen werden. Klein im Vergleich zu Datenbanken, auf denen Websites wie SO ausgeführt werden - aber groß genug, um die SQLite-Leistung zu optimieren.
Mike Willekes
51
"Wir haben große Mengen an Konfigurationsdaten in XML-Dateien gespeichert, die analysiert und zur weiteren Verarbeitung bei der Initialisierung der Anwendung in eine SQLite-Datenbank geladen werden." Warum behalten Sie nicht alles in der SQLite-Datenbank, anstatt es in XML zu speichern und dann alles zur Initialisierungszeit zu laden?
CAFxX
14
Haben Sie versucht, nicht anzurufen 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.
Ahcox
21
Haben Sie wiederholte Messungen durchgeführt? Der 4s "Gewinn" für das Vermeiden von 7 lokalen Zeigern ist seltsam, selbst wenn ein verwirrter Optimierer vorausgesetzt wird.
Peterchen
5
Verwenden Sie diese Option nicht feof(), um die Beendigung Ihrer Eingangsschleife zu steuern. Verwenden Sie das von zurückgegebene Ergebnis fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson

Antworten:

785

Mehrere Tipps:

  1. Fügen Sie Einfügungen / Aktualisierungen in eine Transaktion ein.
  2. Für ältere Versionen von SQLite - Betrachten Sie einen weniger paranoiden Journalmodus ( pragma journal_mode). Es gibt NORMALund dann gibt es OFF, die die Einfügegeschwindigkeit erheblich erhöhen können, wenn Sie nicht zu besorgt sind, dass die Datenbank möglicherweise beschädigt wird, wenn das Betriebssystem abstürzt. Wenn Ihre Anwendung abstürzt, sollten die Daten in Ordnung sein. Beachten Sie, dass in neueren Versionen die OFF/MEMORYEinstellungen für Abstürze auf Anwendungsebene nicht sicher sind.
  3. Das Spielen mit Seitengrößen macht ebenfalls einen Unterschied ( PRAGMA page_size). Durch größere Seiten können Lese- und Schreibvorgänge etwas schneller ausgeführt werden, da größere Seiten im Speicher gespeichert werden. Beachten Sie, dass mehr Speicher für Ihre Datenbank verwendet wird.
  4. Wenn Sie Indizes haben, sollten Sie CREATE INDEXnach allen Einfügungen aufrufen . Dies ist erheblich schneller als das Erstellen des Index und das anschließende Einfügen.
  5. Sie müssen sehr vorsichtig sein, wenn Sie gleichzeitig auf SQLite zugreifen können, da die gesamte Datenbank beim Schreiben gesperrt ist und obwohl mehrere Leser möglich sind, werden Schreibvorgänge gesperrt. Dies wurde durch das Hinzufügen einer WAL in neueren SQLite-Versionen etwas verbessert.
  6. Nutzen Sie die Platzersparnis ... kleinere Datenbanken gehen schneller. Wenn Sie beispielsweise Schlüsselwertpaare haben, versuchen Sie, den Schlüssel INTEGER PRIMARY KEYnach Möglichkeit zu einem Schlüssel zu machen, der die implizite eindeutige Zeilennummernspalte in der Tabelle ersetzt.
  7. Wenn Sie mehrere Threads verwenden, können Sie versuchen, den Cache für gemeinsam genutzte Seiten zu verwenden , damit geladene Seiten von Threads gemeinsam genutzt werden können, wodurch teure E / A-Aufrufe vermieden werden.
  8. Nicht benutzen !feof(file)!

Ich habe auch hier und hier ähnliche Fragen gestellt .

Snazzer
quelle
9
Docs kennen keinen PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld
4
Es ist eine Weile her, dass meine Vorschläge für ältere Versionen gelten, bevor eine WAL eingeführt wurde. Es sieht so aus, als ob DELETE die neue normale Einstellung ist, und jetzt gibt es auch die Einstellungen OFF und MEMORY. Ich nehme an, OFF / MEMORY verbessert die Schreibleistung auf Kosten der Datenbankintegrität und OFF deaktiviert Rollbacks vollständig.
Snazzer
4
Haben Sie für # 7 ein Beispiel zum Aktivieren des Caches für gemeinsam genutzte Seiten mit dem Wrapper c # system.data.sqlite?
Aaron Hudon
4
# 4 brachte uralte Erinnerungen zurück - Es gab mindestens einen Fall in der Vergangenheit, in dem ein Index vor einer Gruppe von Adds gelöscht und anschließend neu erstellt wurde, was Einfügungen erheblich beschleunigte. Möglicherweise funktionieren einige Systeme auf modernen Systemen noch schneller, wenn Sie wissen, dass Sie für diesen Zeitraum alleinigen Zugriff auf die Tabelle haben.
Bill K
Daumen hoch für # 1: Ich hatte selbst sehr viel Glück mit Transaktionen.
Enno
146

Versuchen Sie es SQLITE_STATICanstelle von SQLITE_TRANSIENTfür diese Einsätze.

SQLITE_TRANSIENT bewirkt, dass SQLite die Zeichenfolgendaten kopiert, bevor es zurückkehrt.

SQLITE_STATICteilt mit, dass die von Ihnen angegebene Speicheradresse gültig ist, bis die Abfrage ausgeführt wurde (was in dieser Schleife immer der Fall ist). Auf diese Weise sparen Sie mehrere Zuweisungs-, Kopier- und Freigabevorgänge pro Schleife. Möglicherweise eine große Verbesserung.

Alexander Farber
quelle
109

Vermeiden Sie sqlite3_clear_bindings(stmt).

Der Code im Test legt die Bindungen jedes Mal fest, durch die ausreichend sein sollte.

Das C-API-Intro aus den SQLite-Dokumenten lautet:

Vor dem ersten Aufruf von sqlite3_step () oder unmittelbar nach sqlite3_reset () kann die Anwendung die Schnittstellen sqlite3_bind () aufrufen , um Werte an die Parameter anzuhängen. Jeder Aufruf von sqlite3_bind () überschreibt vorherige Bindungen für denselben Parameter

In den Dokumenten gibt es nichts zu sqlite3_clear_bindingssagen, dass Sie es zusätzlich zum einfachen Festlegen der Bindungen aufrufen müssen.

Weitere Details: Avoid_sqlite3_clear_bindings ()

ahcox
quelle
5
Wunderbar richtig: "Im Gegensatz zur Intuition vieler setzt sqlite3_reset () die Bindungen für eine vorbereitete Anweisung nicht zurück. Verwenden Sie diese Routine, um alle Hostparameter auf NULL zurückzusetzen." - sqlite.org/c3ref/clear_bindings.html
Francis Straccia
63

Auf Masseneinsätzen

Inspiriert von diesem Beitrag und der Frage zum Stapelüberlauf, die mich hierher geführt hat: Ist es möglich, mehrere Zeilen gleichzeitig in eine SQLite-Datenbank einzufügen? - Ich habe mein erstes Git- Repository gepostet :

https://github.com/rdpoor/CreateOrUpdate

Diese Masse lädt ein Array von ActiveRecords in MySQL- , SQLite- oder PostgreSQL- Datenbanken. Es enthält die Option, vorhandene Datensätze zu ignorieren, zu überschreiben oder einen Fehler auszulösen. Meine rudimentären Benchmarks zeigen eine 10-fache Geschwindigkeitsverbesserung im Vergleich zu sequentiellen Schreibvorgängen - YMMV.

Ich verwende es in Produktionscode, wo ich häufig große Datenmengen importieren muss, und bin ziemlich zufrieden damit.

furchtloser Narr
quelle
4
@Jess: Wenn Sie dem Link folgen, werden Sie sehen, dass er die Batch-Insert-Syntax meinte.
Alix Axel
48

Massenimporte scheinen am besten zu funktionieren, wenn Sie Ihre INSERT / UPDATE- Anweisungen aufteilen können . Ein Wert von ungefähr 10.000 hat für mich bei einer Tabelle mit nur wenigen Zeilen gut funktioniert, YMMV ...

Leon
quelle
22
Sie möchten x = 10.000 so einstellen, dass x = cache [= cache_size * page_size] / durchschnittliche Größe Ihrer Einfügung.
Alix Axel
43

Wenn Sie sich nur für das Lesen interessieren, ist es etwas schneller (aber möglicherweise veraltete Daten zu lesen), von mehreren Verbindungen aus mehreren Threads zu lesen (Verbindung pro Thread).

Finden Sie zuerst die Elemente in der Tabelle:

SELECT COUNT(*) FROM table

dann Seiten einlesen (LIMIT / OFFSET):

SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>

wo und werden pro Thread wie folgt berechnet:

int limit = (count + n_threads - 1)/n_threads;

für jeden Thread:

int offset = thread_index * limit

Für unsere kleine (200 MB) Datenbank führte dies zu einer Beschleunigung von 50-75% (3.8.0.2 64-Bit unter Windows 7). Unsere Tabellen sind stark nicht normalisiert (1000-1500 Spalten, ungefähr 100.000 oder mehr Zeilen).

Zu viele oder zu kleine Threads reichen nicht aus. Sie müssen sich selbst bewerten und profilieren.

Auch für uns hat SHAREDCACHE die Leistung verlangsamt, daher habe ich PRIVATECACHE manuell eingefügt (weil es für uns global aktiviert wurde).

Malkia
quelle
29

Ich kann keinen Gewinn aus Transaktionen erzielen, bis ich cache_size auf einen höheren Wert erhöht habe, d. H. PRAGMA cache_size=10000;

anefeletos
quelle
Beachten Sie, dass bei Verwendung eines positiven Werts für cache_sizedie Anzahl der zu zwischenspeichernden Seiten und nicht die gesamte RAM-Größe festgelegt wird. Bei einer Standardseitengröße von 4 KB enthält diese Einstellung bis zu 40 MB Daten pro geöffneter Datei (oder pro Prozess, wenn sie mit gemeinsam genutztem Cache ausgeführt wird ).
Groo
21

Nachdem ich dieses Tutorial gelesen hatte, versuchte ich es in mein Programm zu implementieren.

Ich habe 4-5 Dateien, die Adressen enthalten. Jede Datei enthält ca. 30 Millionen Datensätze. Ich verwende dieselbe Konfiguration, die Sie vorschlagen, aber meine Anzahl von INSERTs pro Sekunde ist sehr niedrig (~ 10.000 Datensätze pro Sekunde).

Hier schlägt Ihr Vorschlag fehl. Sie verwenden eine einzelne Transaktion für alle Datensätze und eine einzelne Einfügung ohne Fehler / Fehler. Angenommen, Sie teilen jeden Datensatz in mehrere Einfügungen in verschiedenen Tabellen auf. Was passiert, wenn der Rekord gebrochen ist?

Der Befehl ON CONFLICT wird nicht angewendet. Wenn Sie 10 Elemente in einem Datensatz haben und jedes Element in eine andere Tabelle eingefügt werden muss und Element 5 einen CONSTRAINT-Fehler erhält, müssen auch alle vorherigen 4 Einfügungen ausgeführt werden.

Hier kommt also der Rollback. Das einzige Problem beim Rollback ist, dass Sie alle Ihre Einsätze verlieren und von oben beginnen. Wie können Sie das lösen?

Meine Lösung bestand darin, mehrere Transaktionen zu verwenden. Ich beginne und beende alle 10.000 Datensätze eine Transaktion (Fragen Sie nicht, warum diese Nummer die schnellste war, die ich getestet habe). Ich habe ein Array mit einer Größe von 10.000 erstellt und dort die erfolgreichen Datensätze eingefügt. Wenn der Fehler auftritt, führe ich einen Rollback durch, beginne eine Transaktion, füge die Datensätze aus meinem Array ein, schreibe fest und beginne nach dem fehlerhaften Datensatz eine neue Transaktion.

Diese Lösung hat mir geholfen, die Probleme zu umgehen, die ich beim Umgang mit Dateien habe, die fehlerhafte / doppelte Datensätze enthalten (ich hatte fast 4% fehlerhafte Datensätze).

Der von mir erstellte Algorithmus hat mir geholfen, meinen Prozess um 2 Stunden zu reduzieren. Letzter Ladevorgang der Datei 1 Stunde 30 Minuten, der immer noch langsam ist, aber nicht mit den 4 Stunden verglichen wird, die ursprünglich benötigt wurden. Ich habe es geschafft, die Einsätze von 10.000 / s auf ~ 14.000 / s zu beschleunigen

Wenn jemand andere Ideen hat, wie man es beschleunigen kann, bin ich offen für Vorschläge.

UPDATE :

Zusätzlich zu meiner obigen Antwort sollten Sie berücksichtigen, dass Einfügungen pro Sekunde abhängig von der Festplatte sind, die Sie ebenfalls verwenden. Ich habe es auf 3 verschiedenen PCs mit verschiedenen Festplatten getestet und dabei massive Zeitunterschiede festgestellt. PC1 (1 Std. 30 m), PC2 (6 Std.) PC3 (14 Std.), Also begann ich mich zu fragen, warum das so sein sollte.

Nach zweiwöchiger Recherche und Überprüfung mehrerer Ressourcen: Festplatte, RAM, Cache stellte ich fest, dass einige Einstellungen auf Ihrer Festplatte die E / A-Rate beeinflussen können. Durch Klicken auf Eigenschaften auf dem gewünschten Ausgabelaufwerk werden auf der Registerkarte Allgemein zwei Optionen angezeigt. Opt1: Komprimieren Sie dieses Laufwerk. Opt2: Ermöglichen Sie, dass Dateien dieses Laufwerks indiziert werden.

Durch Deaktivieren dieser beiden Optionen dauert es ungefähr 3 Stunden, bis alle 3 PCs fertig sind (1 Stunde und 20 bis 40 Minuten). Wenn Sie auf langsame Einfügungen stoßen, prüfen Sie, ob Ihre Festplatte mit diesen Optionen konfiguriert ist. Sie sparen viel Zeit und Kopfschmerzen beim Versuch, die Lösung zu finden

Jimmy_A
quelle
Ich werde folgendes vorschlagen. * Verwenden Sie SQLITE_STATIC vs SQLITE_TRANSIENT, um eine Zeichenfolgenkopie zu vermeiden. Sie müssen sicherstellen, dass die Zeichenfolge nicht geändert wird, bevor die Transaktion ausgeführt wird. * Verwenden Sie die Masseneinfügung INSERT INTO stop_times VALUES (NULL,?,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?) * Mmap die Datei, um die Anzahl der zu reduzieren Systemaufrufe.
Rouzier
Auf diese Weise kann ich 5.582.642 Datensätze in 11,51 Sekunden
importieren
-1

Verwenden Sie ContentProvider zum Einfügen der Massendaten in db. Die folgende Methode wird zum Einfügen von Massendaten in die Datenbank verwendet. Dies sollte die INSERT-pro-Sekunde-Leistung von SQLite verbessern.

private SQLiteDatabase database;
database = dbHelper.getWritableDatabase();

public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {

database.beginTransaction();

for (ContentValues value : values)
 db.insert("TABLE_NAME", null, value);

database.setTransactionSuccessful();
database.endTransaction();

}

Rufen Sie die BulkInsert-Methode auf:

App.getAppContext().getContentResolver().bulkInsert(contentUriTable,
            contentValuesArray);

Link: https://www.vogella.com/tutorials/AndroidSQLite/article.html Weitere Informationen finden Sie im Abschnitt Verwenden des ContentProvider-Abschnitts

vishnuc156
quelle