Améliorez les performances INSERT par seconde de SQLite


2976

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_v2pour 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_bindingset 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 whileboucle. 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 SELECTperformances, 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.


8
Bon point! Dans notre cas, nous avons affaire à environ 1,5 million de paires clé / valeur lues à partir de fichiers texte XML et CSV dans 200k enregistrements. Petit par rapport aux bases de données qui exécutent des sites comme SO - mais suffisamment grand pour que le réglage des performances SQLite devienne important.
Mike Willekes

51
"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." pourquoi ne gardez-vous pas tout dans la base de données sqlite en premier lieu, au lieu de le stocker en XML, puis de tout charger au moment de l'initialisation?
CAFxX

14
Avez-vous essayé de ne pas appeler 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.
ahcox

21
Avez-vous fait des mesures répétées? Le 4s "gagner" pour éviter 7 pointeurs locaux est étrange, même en supposant un optimiseur confus.
peterchen

5
Ne pas utiliser feof()pour contrôler la terminaison de votre boucle d'entrée. Utilisez le résultat renvoyé par fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson

Réponses:


785

Quelques conseils:

  1. Mettez des insertions / mises à jour dans une transaction.
  2. Pour les anciennes versions de SQLite - Envisagez un mode de journal moins paranoïaque ( pragma journal_mode). Il y a NORMAL, et puis il y a OFF, ce qui peut augmenter considérablement la vitesse d'insertion si vous n'êtes pas trop inquiet de la possibilité que la base de données soit corrompue si le système d'exploitation se bloque. Si votre application plante, les données devraient être correctes. Notez que dans les versions plus récentes, les OFF/MEMORYparamètres ne sont pas sûrs pour les plantages au niveau de l'application.
  3. Jouer avec les tailles de page fait également la différence ( PRAGMA page_size). Avoir des tailles de page plus grandes peut accélérer la lecture et l'écriture car les pages plus grandes sont conservées en mémoire. Notez que davantage de mémoire sera utilisée pour votre base de données.
  4. Si vous avez des indices, pensez à appeler CREATE INDEXaprès avoir fait toutes vos insertions. C'est beaucoup plus rapide que de créer l'index puis de faire vos insertions.
  5. Vous devez être très prudent si vous avez un accès simultané à SQLite, car toute la base de données est verrouillée lorsque les écritures sont terminées, et bien que plusieurs lecteurs soient possibles, les écritures seront verrouillées. Cela a été quelque peu amélioré avec l'ajout d'un WAL dans les nouvelles versions de SQLite.
  6. Tirez parti de l'économie d'espace ... les petites bases de données vont plus vite. Par exemple, si vous avez des paires clé-valeur, essayez de rendre la clé INTEGER PRIMARY KEYsi possible, ce qui remplacera la colonne de numéro de ligne unique implicite dans le tableau.
  7. Si vous utilisez plusieurs threads, vous pouvez essayer d'utiliser le cache de page partagé , qui permettra aux pages chargées d'être partagées entre les threads, ce qui peut éviter des appels d'E / S coûteux.
  8. N'utilisez pas !feof(file)!

J'ai également posé des questions similaires ici et ici .


9
Les documents ne connaissent pas de PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld

4
Cela fait un moment, mes suggestions s'appliquaient aux anciennes versions avant l'introduction d'un WAL. Il semble que DELETE soit le nouveau paramètre normal, et maintenant il y a aussi les paramètres OFF et MEMORY. Je suppose que OFF / MEMORY améliorera les performances d'écriture au détriment de l'intégrité de la base de données, et OFF désactive complètement les restaurations.
Snazzer

4
pour # 7, avez-vous un exemple sur la façon d'activer le cache de page partagé à l'aide du wrapper c # system.data.sqlite?
Aaron Hudon

4
# 4 a ramené des souvenirs anciens - Il y avait au moins un cas dans les temps précédents où la suppression d'un index avant un groupe d'ajouts et sa recréation par la suite accélérait considérablement les insertions. Peut encore fonctionner plus rapidement sur les systèmes modernes pour certains ajouts où vous savez que vous avez le seul accès à la table pour la période.
Bill K

Bravo pour le n ° 1: j'ai moi-même eu beaucoup de chance avec les transactions.
Enno

146

Essayez d'utiliser SQLITE_STATICau lieu de SQLITE_TRANSIENTpour ces insertions.

SQLITE_TRANSIENT entraînera SQLite pour copier les données de chaîne avant de retourner.

SQLITE_STATIClui indique que l'adresse mémoire que vous lui avez donnée sera valide jusqu'à ce que la requête soit effectuée (ce qui est toujours le cas dans cette boucle). Cela vous évitera plusieurs opérations d'allocation, de copie et de désallocation par boucle. Peut-être une grande amélioration.


109

A éviter sqlite3_clear_bindings(stmt).

Le code du test définit à chaque fois les liaisons qui devraient suffire.

L' intro de l' API C des documents SQLite dit:

Avant d'appeler sqlite3_step () pour la première fois ou immédiatement après sqlite3_reset () , l'application peut appeler les 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

Il n'y a rien dans les documents pour sqlite3_clear_bindingsdire que vous devez l'appeler en plus de simplement définir les liaisons.

Plus de détails: Avoid_sqlite3_clear_bindings ()


5
Merveilleusement à droite: "Contrairement à l'intuition de beaucoup, sqlite3_reset () ne réinitialise pas les liaisons sur une instruction préparée. Utilisez cette routine pour réinitialiser tous les paramètres de l'hôte à NULL." - sqlite.org/c3ref/clear_bindings.html
Francis Straccia

63

Sur inserts en vrac

Inspiré par ce post et par la question de débordement de pile qui m'a conduit ici - Est-il possible d'insérer plusieurs lignes à la fois dans une base de données SQLite? - J'ai posté mon premier dépôt Git :

https://github.com/rdpoor/CreateOrUpdate

qui charge en masse un tableau d'ActiveRecords dans les bases de données MySQL , SQLite ou PostgreSQL . Il comprend une option pour ignorer les enregistrements existants, les écraser ou déclencher une erreur. Mes repères rudimentaires montrent une amélioration de la vitesse 10x par rapport aux écritures séquentielles - YMMV.

Je l'utilise dans le code de production où j'ai souvent besoin d'importer de grands ensembles de données, et j'en suis assez content.


4
@Jess: Si vous suivez le lien, vous verrez qu'il s'agissait de la syntaxe d'insertion par lots.
Alix Axel

48

Les importations en masse semblent fonctionner mieux si vous pouvez fragmenter vos instructions INSERT / UPDATE . Une valeur de 10 000 environ a bien fonctionné pour moi sur une table avec seulement quelques lignes, YMMV ...


22
Vous souhaitez régler x = 10 000 afin que x = cache [= taille_cache * taille_page] / taille moyenne de votre insert.
Alix Axel

43

Si vous ne vous souciez que de la lecture, la version un peu plus rapide (mais peut lire les données périmées) consiste à lire à partir de plusieurs connexions à partir de plusieurs threads (connexion par thread).

Trouvez d'abord les articles, dans le tableau:

SELECT COUNT(*) FROM table

puis lisez en pages (LIMIT / OFFSET):

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

où et sont calculés par thread, comme ceci:

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

pour chaque fil:

int offset = thread_index * limit

Pour notre petite base de données (200 Mo), cela a accéléré de 50 à 75% (3.8.0.2 64 bits sur Windows 7). Nos tableaux sont fortement non normalisés (1 000 à 1 500 colonnes, environ 100 000 lignes ou plus).

Trop ou trop peu de fils ne le feront pas, vous devez vous évaluer et vous profiler.

Aussi pour nous, SHAREDCACHE a ralenti les performances, j'ai donc mis manuellement PRIVATECACHE (car il a été activé globalement pour nous)


29

Je ne pourrais obtenir aucun gain des transactions jusqu'à ce que j'augmente cache_size à une valeur plus élevée, c.-à-d. PRAGMA cache_size=10000;


Notez que l'utilisation d'une valeur positive pour cache_sizedéfinit le nombre de pages à mettre en cache et non la taille totale de la RAM. Avec la taille de page par défaut de 4 Ko, ce paramètre contiendra jusqu'à 40 Mo de données par fichier ouvert (ou par processus, s'il s'exécute avec un cache partagé ).
Groo

21

Après avoir lu ce tutoriel, j'ai essayé de l'implémenter dans mon programme.

J'ai 4-5 fichiers qui contiennent des adresses. Chaque fichier contient environ 30 millions d'enregistrements. J'utilise la même configuration que celle que vous proposez, mais mon nombre d'insertions par seconde est très faible (~ 10 000 enregistrements par seconde).

Voici où votre suggestion échoue. Vous utilisez une seule transaction pour tous les enregistrements et une seule insertion sans erreur / échec. Disons que vous divisez chaque enregistrement en plusieurs insertions sur différentes tables. Que se passe-t-il si le record est battu?

La commande ON CONFLICT ne s'applique pas, car si vous avez 10 éléments dans un enregistrement et que vous avez besoin que chaque élément soit inséré dans une table différente, si l'élément 5 obtient une erreur CONSTRAINT, les 4 insertions précédentes doivent également y aller.

Voici donc où vient le retour en arrière. Le seul problème avec la restauration est que vous perdez toutes vos insertions et commencez par le haut. Comment pouvez-vous résoudre ce problème?

Ma solution était d'utiliser plusieurs transactions. Je commence et termine une transaction tous les 10 000 enregistrements (ne demandez pas pourquoi ce nombre, c'était le plus rapide que j'ai testé). J'ai créé un tableau de 10 000 et j'y ai inséré les enregistrements réussis. Lorsque l'erreur se produit, je fais une restauration, commence une transaction, insère les enregistrements de mon tableau, valide et puis commence une nouvelle transaction après l'enregistrement cassé.

Cette solution m'a aidé à contourner les problèmes que j'ai lorsque je traite des fichiers contenant des enregistrements incorrects / en double (j'avais près de 4% d'enregistrements incorrects).

L'algorithme que j'ai créé m'a aidé à réduire mon processus de 2 heures. Processus de chargement final du fichier 1h30 qui est encore lent mais pas comparé aux 4h qu'il a fallu initialement. J'ai réussi à accélérer les inserts de 10.000 / s à ~ 14.000 / s

Si quelqu'un a d'autres idées sur la façon de l'accélérer, je suis ouvert aux suggestions.

MISE À JOUR :

En plus de ma réponse ci-dessus, vous devez garder à l'esprit que les insertions par seconde en fonction du disque dur que vous utilisez également. Je l'ai testé sur 3 PC différents avec différents disques durs et j'ai eu d'énormes différences dans le temps. PC1 (1 h 30), PC2 (6 h) PC3 (14 h), alors j'ai commencé à me demander pourquoi.

Après deux semaines de recherche et de vérification de plusieurs ressources: Disque dur, RAM, Cache, j'ai découvert que certains paramètres de votre disque dur peuvent affecter le taux d'E / S. En cliquant sur les propriétés de votre lecteur de sortie souhaité, vous pouvez voir deux options dans l'onglet général. Opt1: compresser ce lecteur, Opt2: autoriser l'indexation du contenu des fichiers de ce lecteur.

En désactivant ces deux options, les 3 PC prennent maintenant environ le même temps pour terminer (1 heure et 20 à 40 minutes). Si vous rencontrez des insertions lentes, vérifiez si votre disque dur est configuré avec ces options. Cela vous fera gagner beaucoup de temps et vous évitera des maux de tête en essayant de trouver la solution


Je proposerai ce qui suit. * Utilisez SQLITE_STATIC vs SQLITE_TRANSIENT pour éviter une copie de chaîne, vous devez vous assurer que la chaîne ne sera pas modifiée avant l'exécution de la transaction * Utilisez l'insertion en bloc INSERT INTO stop_times VALUES (NULL,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?) * Mmap le fichier pour réduire le nombre de syscalls.
rouzier

Ce faisant, je peux importer 5 582 642 enregistrements en 11,51 secondes
rouzier


-1

Utilisez ContentProvider pour insérer les données en masse dans db. La méthode ci-dessous utilisée pour insérer des données en masse dans la base de données. Cela devrait améliorer les performances INSERT par seconde de SQLite.

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

}

Appelez la méthode bulkInsert:

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

Lien: https://www.vogella.com/tutorials/AndroidSQLite/article.html consultez Utilisation de la section ContentProvider pour plus de détails

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.