L'optimisation de SQLite est délicate. Les performances d'insertion en vrac d'une application C peuvent varier de 85 inserts par seconde à plus de 96 000 inserts par seconde!
Contexte: Nous utilisons SQLite dans le cadre d'une application de bureau. Nous avons de grandes quantités de données de configuration stockées dans des fichiers XML qui sont analysées et chargées dans une base de données SQLite pour un traitement ultérieur lorsque l'application est initialisée. SQLite est idéal pour cette situation car il est rapide, il ne nécessite aucune configuration spécialisée et la base de données est stockée sur disque en tant que fichier unique.
Justification: Au début, j'étais déçu de la performance que je voyais. Il s'avère que les performances de SQLite peuvent varier considérablement (à la fois pour les insertions en masse et les sélections) en fonction de la configuration de la base de données et de la façon dont vous utilisez l'API. Ce n'était pas une question triviale de comprendre quelles étaient toutes les options et techniques, donc j'ai pensé qu'il était prudent de créer cette entrée wiki communautaire pour partager les résultats avec les lecteurs Stack Overflow afin de sauver les autres des ennuis des mêmes investigations.
L'expérience: Plutôt que de simplement parler de conseils de performance au sens général (c'est-à-dire "Utiliser une transaction!" ), J'ai pensé qu'il était préférable d'écrire du code C et de mesurer réellement l'impact de diverses options. Nous allons commencer avec quelques données simples:
- Un fichier texte délimité par une tabulation de 28 Mo (environ 865 000 enregistrements) de l' horaire de transport en commun complet de la ville de Toronto
- Ma machine de test est un P4 3,60 GHz fonctionnant sous Windows XP.
- Le code est compilé avec Visual C ++ 2005 comme "Release" avec "Full Optimization" (/ Ox) et Favoriser Fast Code (/ Ot).
- J'utilise SQLite "Amalgamation", compilé directement dans mon application de test. La version de SQLite dont je dispose est un peu plus ancienne (3.6.7), mais je pense que ces résultats seront comparables à la dernière version (veuillez laisser un commentaire si vous pensez le contraire).
Écrivons du code!
Le code: un programme C simple qui lit le fichier texte ligne par ligne, divise la chaîne en valeurs, puis insère les données dans une base de données SQLite. Dans cette version "baseline" du code, la base de données est créée, mais nous n'insérerons pas réellement de données:
/*************************************************************
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;
}
Le contrôle"
L'exécution du code en l'état n'effectue en fait aucune opération de base de données, mais il nous donnera une idée de la vitesse des opérations d'E / S du fichier C brut et du traitement des chaînes.
Importé 864913 enregistrements en 0,94 secondes
Génial! Nous pouvons faire 920 000 insertions par seconde, à condition que nous n'effectuions aucune insertion :-)
Le "pire scénario"
Nous allons générer la chaîne SQL en utilisant les valeurs lues dans le fichier et invoquer cette opération SQL en utilisant 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);
Cela va être lent car le SQL sera compilé en code VDBE pour chaque insertion et chaque insertion se produira dans sa propre transaction. C'est lent?
Importé 864913 enregistrements en 9933,61 secondes
Oui! 2 heures et 45 minutes! Cela ne représente que 85 insertions par seconde.
Utilisation d'une transaction
Par défaut, SQLite évaluera chaque instruction INSERT / UPDATE dans une transaction unique. Si vous effectuez un grand nombre d'insertions, il est conseillé d'envelopper votre opération dans une transaction:
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);
Importé 864913 enregistrements en 38,03 secondes
C'est mieux. Le simple emballage de tous nos inserts en une seule transaction a amélioré nos performances à 23 000 inserts par seconde.
Utilisation d'une instruction préparée
L'utilisation d'une transaction a été une énorme amélioration, mais recompiler l'instruction SQL pour chaque insert n'a pas de sens si nous utilisons le même SQL à plusieurs reprises. Utilisons sqlite3_prepare_v2
pour compiler notre instruction SQL une fois, puis lions nos paramètres à cette instruction en utilisant 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;
Importé 864913 enregistrements en 16,27 secondes
Agréable! Il y a un peu plus de code (n'oubliez pas d'appeler sqlite3_clear_bindings
et sqlite3_reset
), mais nous avons plus que doublé nos performances à 53 000 insertions par seconde.
PRAGMA synchrone = OFF
Par défaut, SQLite se met en pause après l'émission d'une commande d'écriture au niveau du système d'exploitation. Cela garantit que les données sont écrites sur le disque. En définissant synchronous = OFF
, nous demandons à SQLite de simplement transmettre les données au système d'exploitation pour l'écriture, puis de continuer. Il est possible que le fichier de base de données soit corrompu si l'ordinateur subit un crash catastrophique (ou une panne de courant) avant que les données ne soient écrites sur le plateau:
/* 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);
Importé 864913 enregistrements en 12,41 secondes
Les améliorations sont maintenant plus petites, mais nous atteignons 69 600 inserts par seconde.
PRAGMA journal_mode = MEMOIRE
Envisagez de stocker le journal de restauration en mémoire en procédant à une évaluation PRAGMA journal_mode = MEMORY
. Votre transaction sera plus rapide, mais si vous perdez de l'énergie ou que votre programme tombe en panne pendant une transaction, votre base de données peut être laissée dans un état corrompu avec une transaction partiellement terminée:
/* 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);
Importé 864913 enregistrements en 13,50 secondes
Un peu plus lent que l'optimisation précédente à 64 000 insertions par seconde.
PRAGMA synchrone = OFF et PRAGMA journal_mode = MEMORY
Combinons les deux optimisations précédentes. C'est un peu plus risqué (en cas de plantage), mais nous importons simplement des données (pas une banque):
/* 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);
Importé 864913 enregistrements en 12,00 secondes
Fantastique! Nous sommes capables de faire 72 000 insertions par seconde.
Utilisation d'une base de données en mémoire
Juste pour les coups de pied, construisons sur toutes les optimisations précédentes et redéfinissons le nom de fichier de la base de données afin que nous travaillions entièrement en RAM:
#define DATABASE ":memory:"
Importé 864913 enregistrements en 10,94 secondes
Ce n'est pas super pratique de stocker notre base de données dans la RAM, mais il est impressionnant de pouvoir effectuer 79 000 insertions par seconde.
Refactorisation du code C
Bien qu'il ne s'agisse pas spécifiquement d'une amélioration SQLite, je n'aime pas les char*
opérations d'affectation supplémentaires dans la while
boucle. Refactorisons rapidement ce code pour passer strtok()
directement la sortie de sqlite3_bind_text()
, et laissons le compilateur essayer d'accélérer les choses pour nous:
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);
Remarque: Nous revenons à l'utilisation d'un vrai fichier de base de données. Les bases de données en mémoire sont rapides, mais pas nécessairement pratiques
Importé 864913 enregistrements en 8,94 secondes
Une légère refactorisation du code de traitement de chaîne utilisé dans notre liaison de paramètres nous a permis d'effectuer 96 700 insertions par seconde. Je pense qu'il est prudent de dire que c'est très rapide . Alors que nous commençons à modifier d'autres variables (par exemple, la taille de la page, la création d'index, etc.), ce sera notre référence.
Résumé (jusqu'à présent)
J'espère que tu es toujours avec moi! La raison pour laquelle nous avons commencé dans cette voie est que les performances de l'insertion en vrac varient énormément avec SQLite, et il n'est pas toujours évident de savoir quelles modifications doivent être apportées pour accélérer nos opérations. En utilisant le même compilateur (et les options du compilateur), la même version de SQLite et les mêmes données, nous avons optimisé notre code et notre utilisation de SQLite pour passer du pire des cas de 85 insertions par seconde à plus de 96 000 insertions par seconde!
CRÉER INDEX puis INSÉRER vs INSÉRER puis CRÉER INDEX
Avant de commencer à mesurer les SELECT
performances, nous savons que nous allons créer des indices. Il a été suggéré dans l'une des réponses ci-dessous que lors de l'insertion en bloc, il est plus rapide de créer l'index après l'insertion des données (par opposition à la création de l'index en premier, puis à l'insertion des données). Essayons:
Créer un index puis insérer des données
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
864913 enregistrements importés en 18.13 secondes
Insérer des données puis créer un index
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
Importé 864913 enregistrements en 13,66 secondes
Comme prévu, les insertions groupées sont plus lentes si une colonne est indexée, mais cela fait une différence si l'index est créé après l'insertion des données. Notre base de référence sans indice est de 96 000 insertions par seconde. La création de l'index en premier, puis l'insertion des données nous donne 47 700 insertions par seconde, tandis que l'insertion des données en premier puis la création de l'index nous donne 63 300 insertions par seconde.
Je serais ravi de prendre des suggestions pour d'autres scénarios à essayer ... Et je vais bientôt compiler des données similaires pour les requêtes SELECT.
sqlite3_clear_bindings(stmt);
? Vous définissez les liaisons à chaque fois, ce qui devrait être suffisant: Avant d'appeler sqlite3_step () pour la première fois ou immédiatement après sqlite3_reset (), l'application peut appeler l'une des interfaces sqlite3_bind () pour attacher des valeurs aux paramètres. Chaque appel à sqlite3_bind () remplace les liaisons précédentes sur le même paramètre (voir: sqlite.org/cintro.html ). Il n'y a rien dans les documents pour cette fonction qui dit que vous devez l'appeler.
feof()
pour contrôler la terminaison de votre boucle d'entrée. Utilisez le résultat renvoyé par fgets()
. stackoverflow.com/a/15485689/827263