Migliora le prestazioni INSERT al secondo di SQLite


2975

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_v2per 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_bindingse 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 whileciclo. 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 SELECTprestazioni, 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.


8
Buon punto! Nel nostro caso abbiamo a che fare con circa 1,5 milioni di coppie chiave / valore lette da file di testo XML e CSV in 200.000 record. Piccolo rispetto ai database che eseguono siti come SO - ma abbastanza grande da rendere importante l'ottimizzazione delle prestazioni di SQLite.
Mike Willekes,

51
"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." perché non tenere tutto nel database sqlite in primo luogo, invece di archiviare in XML e quindi caricare tutto al momento dell'inizializzazione?
CAFxX,

14
Hai provato a non chiamare 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.
ahcox,

21
Hai fatto misurazioni ripetute? La "vittoria" 4s per evitare 7 puntatori locali è strana, anche assumendo un ottimizzatore confuso.
Peter

5
Non utilizzare feof()per controllare la chiusura del ciclo di input. Usa il risultato restituito da fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson,

Risposte:


785

Diversi consigli:

  1. Inserisci inserti / aggiornamenti in una transazione.
  2. Per le versioni precedenti di SQLite: considera una modalità journal meno paranoica ( pragma journal_mode). C'è NORMAL, e poi c'è OFF, che può aumentare significativamente la velocità di inserimento se non sei troppo preoccupato che il database possa corrompersi in caso di crash del sistema operativo. Se l'applicazione si arresta in modo anomalo, i dati dovrebbero andare bene. Si noti che nelle versioni più recenti, le OFF/MEMORYimpostazioni non sono sicure per gli arresti anomali a livello di applicazione.
  3. Anche giocare con le dimensioni della pagina fa la differenza ( PRAGMA page_size). Avere dimensioni di pagina più grandi può rendere le letture e le scritture un po 'più veloci poiché le pagine più grandi vengono conservate in memoria. Si noti che verrà utilizzata più memoria per il database.
  4. Se si dispone di indici, prendere in considerazione la chiamata CREATE INDEXdopo aver eseguito tutti gli inserti. Questo è significativamente più veloce della creazione dell'indice e quindi degli inserimenti.
  5. Devi fare molta attenzione se hai accesso simultaneo a SQLite, poiché l'intero database viene bloccato quando vengono eseguite le scritture e, sebbene siano possibili più lettori, le scritture verranno bloccate. Questo è stato leggermente migliorato con l'aggiunta di un WAL nelle versioni più recenti di SQLite.
  6. Approfitta del risparmio di spazio ... database più piccoli vanno più veloci. Ad esempio, se si dispone di coppie di valori chiave, provare a rendere la chiave un INTEGER PRIMARY KEYse possibile, che sostituirà la colonna del numero di riga univoco implicita nella tabella.
  7. Se si utilizzano più thread, è possibile provare a utilizzare la cache della pagina condivisa , che consentirà di condividere le pagine caricate tra i thread, evitando così costose chiamate I / O.
  8. Non usare !feof(file)!

Ho anche fatto domande simili qui e qui .


9
I documenti non conoscono un PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld

4
È passato un po 'di tempo, i miei suggerimenti si applicavano alle versioni precedenti prima dell'introduzione di un WAL. Sembra che DELETE sia la nuova impostazione normale, e ora ci sono anche le impostazioni OFF e MEMORY. Suppongo che OFF / MEMORY migliorerà le prestazioni di scrittura a spese dell'integrità del database e OFF disabiliterà completamente i rollback.
Snazzer,

4
per # 7, hai un esempio su come abilitare la cache delle pagine condivise usando il wrapper c # system.data.sqlite?
Aaron Hudon,

4
Il n. 4 ha riportato alla memoria ricordi antichi - C'era almeno un caso nei tempi precedenti in cui il rilascio di un indice prima di un gruppo di aggiunte e la sua creazione successiva velocizzava gli inserti in modo significativo. Potrebbe ancora funzionare più rapidamente sui sistemi moderni per alcuni elementi in cui sai di avere accesso esclusivo alla tabella per il periodo.
Bill K,

Complimenti per # 1: ho avuto molta fortuna con le transazioni da solo.
Enno,

146

Prova a utilizzare SQLITE_STATICanziché SQLITE_TRANSIENTper quegli inserti.

SQLITE_TRANSIENT farà in modo che SQLite copi i dati della stringa prima di tornare.

SQLITE_STATICgli dice che l'indirizzo di memoria che gli hai dato sarà valido fino a quando la query non sarà stata eseguita (cosa che in questo ciclo è sempre il caso). Ciò consentirà di risparmiare diverse operazioni di allocazione, copia e deallocazione per ciclo. Forse un grande miglioramento.


109

Evitare sqlite3_clear_bindings(stmt).

Il codice nel test imposta le associazioni ogni volta che dovrebbe essere sufficiente.

L' introduzione dell'API C dai documenti di SQLite dice:

Prima di chiamare sqlite3_step () per la prima volta o immediatamente dopo sqlite3_reset () , l'applicazione può richiamare le interfacce sqlite3_bind () per associare i valori ai parametri. Ogni chiamata a sqlite3_bind () sovrascrive i bind precedenti sullo stesso parametro

Non c'è nulla nei documenti per sqlite3_clear_bindingsdire che devi chiamarlo oltre a impostare semplicemente i binding.

Maggiori dettagli: Avoid_sqlite3_clear_bindings ()


5
Meravigliosamente giusto: "Contrariamente all'intuizione di molti, sqlite3_reset () non reimposta i binding su un'istruzione preparata. Utilizzare questa routine per ripristinare tutti i parametri host su NULL." - sqlite.org/c3ref/clear_bindings.html
Francis Straccia

63

Su inserti sfusi

Ispirato da questo post e dalla domanda Stack Overflow che mi ha portato qui: è possibile inserire più righe contemporaneamente in un database SQLite? - Ho pubblicato il mio primo repository Git :

https://github.com/rdpoor/CreateOrUpdate

che alla rinfusa carica un array di ActiveRecords nei database MySQL , SQLite o PostgreSQL . Include un'opzione per ignorare i record esistenti, sovrascriverli o generare un errore. I miei benchmark rudimentali mostrano un miglioramento della velocità di 10 volte rispetto alle scritture sequenziali - YMMV.

Lo sto usando nel codice di produzione dove ho spesso bisogno di importare grandi set di dati e ne sono abbastanza soddisfatto.


4
@Jess: se segui il link, vedrai che intendeva la sintassi dell'inserimento batch.
Alix Axel,

48

Le importazioni in blocco sembrano avere le migliori prestazioni se puoi bloccare le tue istruzioni INSERT / UPDATE . Un valore di circa 10.000 ha funzionato bene per me su una tabella con solo poche righe, YMMV ...


22
Si desidera ottimizzare x = 10.000 in modo che x = cache [= cache_size * page_size] / dimensione media dell'inserto.
Alix Axel,

43

Se ti interessa solo leggere, la versione un po 'più veloce (ma potrebbe leggere dati non aggiornati) è leggere da più connessioni da più thread (connessione per thread).

Per prima cosa trova gli articoli, nella tabella:

SELECT COUNT(*) FROM table

quindi leggi nelle pagine (LIMIT / OFFSET):

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

dove e sono calcolati per thread, in questo modo:

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

per ogni thread:

int offset = thread_index * limit

Per il nostro db di piccole dimensioni (200mb) questo ha consentito una velocità del 50-75% (3.8.0.2 64-bit su Windows 7). Le nostre tabelle sono fortemente non normalizzate (1000-1500 colonne, circa 100.000 o più righe).

Troppi o troppo piccoli thread non lo faranno, devi fare un benchmark e profilare te stesso.

Anche per noi, SHAREDCACHE ha rallentato le prestazioni, quindi ho inserito manualmente PRIVATECACHE (perché è stato abilitato a livello globale per noi)


29

Non posso ottenere alcun guadagno dalle transazioni fino a quando non ho aumentato cache_size a un valore più alto, ad es PRAGMA cache_size=10000;


Si noti che l'utilizzo di un valore positivo per cache_sizeimposta il numero di pagine da memorizzare nella cache , non la dimensione totale della RAM. Con le dimensioni di pagina predefinite di 4 KB, questa impostazione conterrà fino a 40 MB di dati per file aperto (o per processo, se in esecuzione con cache condivisa ).
Groo

21

Dopo aver letto questo tutorial, ho provato a implementarlo nel mio programma.

Ho 4-5 file che contengono indirizzi. Ogni file ha circa 30 milioni di record. Sto usando la stessa configurazione che stai suggerendo, ma il mio numero di INSERT al secondo è molto basso (~ 10.000 record al secondo).

Qui è dove il tuo suggerimento fallisce. Si utilizza una singola transazione per tutti i record e un singolo inserimento senza errori / errori. Diciamo che stai dividendo ogni record in più inserti su tabelle diverse. Cosa succede se il record è rotto?

Il comando ON CONFLICT non si applica, perché se hai 10 elementi in un record e hai bisogno di ogni elemento inserito in una tabella diversa, se l'elemento 5 ottiene un errore VINCOLARE, allora devono andare anche tutti i 4 inserti precedenti.

Quindi qui è dove arriva il rollback. L'unico problema con il rollback è che si perdono tutti gli inserti e si inizia dall'alto. Come puoi risolverlo?

La mia soluzione era quella di utilizzare più transazioni. Inizio e termino una transazione ogni 10.000 record (non chiedo perché quel numero, è stato il più veloce che ho testato). Ho creato un array di dimensioni 10.000 e ho inserito i record di successo lì. Quando si verifica l'errore, eseguo un rollback, inizio una transazione, inserisco i record dal mio array, eseguo il commit e quindi inizio una nuova transazione dopo il record non funzionante.

Questa soluzione mi ha aiutato a bypassare i problemi che ho quando ho a che fare con file contenenti record errati / duplicati (avevo quasi il 4% di record errati).

L'algoritmo che ho creato mi ha aiutato a ridurre il mio processo di 2 ore. Processo di caricamento finale del file 1 ora e 30 minuti che è ancora lento ma non confrontato con le 4 ore inizialmente impiegate. Sono riuscito a velocizzare gli inserti da 10.000 / sa ~ 14.000 / s

Se qualcuno ha altre idee su come accelerarlo, sono aperto ai suggerimenti.

AGGIORNAMENTO :

In aggiunta alla mia risposta sopra, dovresti tenere presente che gli inserimenti al secondo dipendono anche dal disco rigido che stai utilizzando. L'ho testato su 3 diversi PC con diversi dischi rigidi e ho avuto enormi differenze nei tempi. PC1 (1 ora e 30 minuti), PC2 (6 ore) PC3 (14 ore), quindi ho iniziato a chiedermi perché sarebbe.

Dopo due settimane di ricerche e controllo di più risorse: disco rigido, RAM, cache, ho scoperto che alcune impostazioni sul disco rigido possono influire sulla velocità di I / O. Facendo clic su Proprietà sull'unità di output desiderata, è possibile visualizzare due opzioni nella scheda Generale. Opt1: comprime questa unità, Opt2: consente ai file di questa unità di avere contenuti indicizzati.

Disabilitando queste due opzioni, tutti e 3 i PC ora impiegano all'incirca lo stesso tempo per terminare (1 ora e 20 - 40 minuti). Se si verificano inserimenti lenti, verificare se il disco rigido è configurato con queste opzioni. Ti farà risparmiare un sacco di tempo e mal di testa cercando di trovare la soluzione


Suggerirò quanto segue. * Utilizzare SQLITE_STATIC vs SQLITE_TRANSIENT per evitare una copia della stringa, è necessario assicurarsi che la stringa non venga modificata prima dell'esecuzione della transazione * Utilizzare l'inserimento di massa INSERT INTO stop_times VALUES (NULL,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?) * Mmap il file per ridurre il numero di chiamate di sistema.
rouzier,

In questo modo sono in grado di importare 5.582.642 record in 11,51 secondi
rouzier


-1

Utilizzare ContentProvider per inserire i dati di massa in db. Il metodo seguente utilizzato per l'inserimento di dati in blocco nel database. Ciò dovrebbe migliorare le prestazioni INSERT al secondo di 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();

}

Chiama metodo bulkInsert:

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

Link: https://www.vogella.com/tutorials/AndroidSQLite/article.html verifica utilizzando la sezione ContentProvider per maggiori dettagli

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.