L'ottimizzazione di SQLite è complicata. Le prestazioni degli inserti in blocco di un'applicazione C possono variare da 85 inserti al secondo a oltre 96.000 inserti al secondo!
Sfondo: stiamo usando SQLite come parte di un'applicazione desktop. Disponiamo di grandi quantità di dati di configurazione archiviati in file XML che vengono analizzati e caricati in un database SQLite per un'ulteriore elaborazione quando l'applicazione viene inizializzata. SQLite è ideale per questa situazione perché è veloce, non richiede alcuna configurazione specializzata e il database è archiviato su disco come un singolo file.
Motivazione: Inizialmente sono rimasto deluso dall'esibizione che stavo vedendo. Si scopre che le prestazioni di SQLite possono variare in modo significativo (sia per inserimenti in blocco che per selezioni) in base alla configurazione del database e al modo in cui si utilizza l'API. Non è stata una questione da poco capire quali fossero tutte le opzioni e le tecniche, quindi ho pensato che fosse prudente creare questa voce wiki della community per condividere i risultati con i lettori Stack Overflow al fine di salvare agli altri i problemi delle stesse indagini.
L'esperimento: piuttosto che parlare semplicemente di suggerimenti prestazionali in senso generale (cioè "Usa una transazione!" ), Ho pensato che fosse meglio scrivere un codice C e misurare l'impatto di varie opzioni. Inizieremo con alcuni semplici dati:
- Un file di testo delimitato da TAB da 28 MB (circa 865.000 record) del programma di transito completo per la città di Toronto
- La mia macchina di prova è una P4 da 3,60 GHz con Windows XP.
- Il codice viene compilato con Visual C ++ 2005 come "Release" con "Full Optimization" (/ Ox) e Favor Fast Code (/ Ot).
- Sto usando SQLite "Amalgamation", compilato direttamente nella mia applicazione di prova. La versione di SQLite che mi capita di avere è un po 'più vecchia (3.6.7), ma sospetto che questi risultati saranno paragonabili all'ultima versione (si prega di lasciare un commento se si pensa diversamente).
Scriviamo un po 'di codice!
Il codice: un semplice programma C che legge il file di testo riga per riga, suddivide la stringa in valori e quindi inserisce i dati in un database SQLite. In questa versione "di base" del codice, viene creato il database, ma in realtà non inseriremo dati:
/*************************************************************
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;
}
Il controllo"
L'esecuzione del codice così com'è in realtà non esegue alcuna operazione sul database, ma ci darà un'idea di quanto siano veloci le operazioni di I / O del file C grezzo e le operazioni di elaborazione delle stringhe.
864913 record importati in 0,94 secondi
Grande! Siamo in grado di eseguire 920.000 inserti al secondo, purché in realtà non eseguiamo alcun inserto :-)
Lo "scenario peggiore"
Genereremo la stringa SQL usando i valori letti dal file e invocheremo tale operazione SQL usando sqlite3_exec:
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);
Questo sarà lento perché l'SQL verrà compilato nel codice VDBE per ogni inserimento e ogni inserimento avverrà nella propria transazione. Quanto è lento?
864913 record importati in 9933,61 secondi
Yikes! 2 ore e 45 minuti! Sono solo 85 inserti al secondo.
Utilizzando una transazione
Per impostazione predefinita, SQLite valuterà ogni istruzione INSERT / UPDATE all'interno di una transazione univoca. Se si esegue un numero elevato di inserti, è consigliabile completare l'operazione in una transazione:
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 record importati in 38,03 secondi
Così va meglio. Il semplice avvolgimento di tutti i nostri inserti in un'unica transazione ha migliorato le nostre prestazioni a 23.000 inserti al secondo.
Utilizzando una dichiarazione preparata
L'uso di una transazione è stato un enorme miglioramento, ma la ricompilazione dell'istruzione SQL per ogni inserimento non ha senso se si utilizza lo stesso SQL ripetutamente. Usiamo sqlite3_prepare_v2
per compilare una volta la nostra istruzione SQL e quindi associare i nostri parametri a tale istruzione usando 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 record importati in 16,27 secondi
Bello! C'è un po 'più di codice (non dimenticare di chiamare sqlite3_clear_bindings
e sqlite3_reset
), ma abbiamo più che raddoppiato le nostre prestazioni a 53.000 inserimenti al secondo.
PRAGMA sincrono = OFF
Per impostazione predefinita, SQLite verrà messo in pausa dopo aver emesso un comando di scrittura a livello di sistema operativo. Ciò garantisce che i dati vengano scritti sul disco. Impostando synchronous = OFF
, stiamo istruendo SQLite a semplicemente consegnare i dati al sistema operativo per la scrittura e quindi continuare. È possibile che il file del database venga danneggiato se il computer subisce un arresto anomalo (o interruzione di corrente) prima che i dati vengano scritti sul piatto:
/* 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 record importati in 12,41 secondi
I miglioramenti sono ora più piccoli, ma siamo fino a 69.600 inserti al secondo.
PRAGMA journal_mode = MEMORY
Valutare la possibilità di archiviare il giornale di rollback in memoria PRAGMA journal_mode = MEMORY
. La tua transazione sarà più veloce, ma se perdi energia o il programma si arresta in modo anomalo durante una transazione, il database potrebbe rimanere in uno stato corrotto con una transazione parzialmente completata:
/* 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 record importati in 13,50 secondi
Un po 'più lento della precedente ottimizzazione con 64.000 inserti al secondo.
PRAGMA sincrono = OFF e PRAGMA journal_mode = MEMORY
Uniamo le due precedenti ottimizzazioni. È un po 'più rischioso (in caso di crash), ma stiamo solo importando dati (non eseguendo una banca):
/* 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 record importati in 12,00 secondi
Fantastico! Siamo in grado di eseguire 72.000 inserti al secondo.
Utilizzo di un database in memoria
Solo per i calci, costruiamo su tutte le precedenti ottimizzazioni e ridefiniamo il nome del file del database in modo da lavorare completamente nella RAM:
#define DATABASE ":memory:"
864913 record importati in 10,94 secondi
Non è super pratico archiviare il nostro database nella RAM, ma è impressionante poter eseguire 79.000 inserimenti al secondo.
Refactoring Codice C.
Sebbene non sia specificamente un miglioramento di SQLite, non mi piacciono le char*
operazioni di assegnazione extra nel while
ciclo. Rifattorizziamo rapidamente quel codice per passare strtok()
direttamente l'output di sqlite3_bind_text()
e lasciamo che il compilatore provi ad accelerare le cose per noi:
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);
Nota: torniamo a utilizzare un file di database reale. I database in memoria sono veloci, ma non necessariamente pratici
864913 record importati in 8,94 secondi
Un leggero refactoring al codice di elaborazione delle stringhe utilizzato nella nostra associazione dei parametri ci ha permesso di eseguire 96.700 inserimenti al secondo. Penso che sia sicuro dire che questo è molto veloce . Quando inizieremo a modificare altre variabili (ad es. Dimensioni della pagina, creazione dell'indice, ecc.) Questo sarà il nostro punto di riferimento.
Riepilogo (finora)
Spero che tu sia ancora con me! Il motivo per cui abbiamo iniziato su questa strada è che le prestazioni di inserimento in blocco variano in modo così selvaggio con SQLite, e non è sempre ovvio quali modifiche debbano essere apportate per accelerare le nostre operazioni. Usando lo stesso compilatore (e opzioni del compilatore), la stessa versione di SQLite e gli stessi dati abbiamo ottimizzato il nostro codice e il nostro utilizzo di SQLite per passare da uno scenario peggiore di 85 inserti al secondo a oltre 96.000 inserimenti al secondo!
CREA INDICE quindi INSERISCI vs. INSERISCI quindi CREA INDICE
Prima di iniziare a misurare le SELECT
prestazioni, sappiamo che creeremo indici. È stato suggerito in una delle risposte di seguito che quando si eseguono inserimenti di massa, è più veloce creare l'indice dopo che i dati sono stati inseriti (invece di creare prima l'indice e poi l'inserimento dei dati). Proviamo:
Crea indice quindi Inserisci dati
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
864913 record importati in 18,13 secondi
Inserisci dati quindi Crea indice
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
864913 record importati in 13,66 secondi
Come previsto, gli inserimenti di massa sono più lenti se una colonna è indicizzata, ma fa la differenza se l'indice viene creato dopo l'inserimento dei dati. La nostra baseline senza indice è di 96.000 inserti al secondo. La creazione dell'indice prima quindi l'inserimento dei dati ci dà 47.700 inserti al secondo, mentre l'inserimento dei dati prima quindi la creazione dell'indice ci dà 63.300 inserti al secondo.
Sarei lieto di ricevere suggerimenti per altri scenari da provare ... E presto compilerò dati simili per le query SELECT.
sqlite3_clear_bindings(stmt);
? Puoi impostare i binding ogni volta che dovrebbero essere sufficienti: prima di chiamare sqlite3_step () per la prima volta o immediatamente dopo sqlite3_reset (), l'applicazione può invocare una delle interfacce sqlite3_bind () per allegare i valori ai parametri. Ogni chiamata a sqlite3_bind () sovrascrive i bind precedenti sullo stesso parametro (consultare: sqlite.org/cintro.html ). Non c'è nulla nei documenti per quella funzione che dice che devi chiamarlo.
feof()
per controllare la chiusura del ciclo di input. Usa il risultato restituito da fgets()
. stackoverflow.com/a/15485689/827263