Aprimore o desempenho do INSERT por segundo do SQLite

2975

Otimizar o SQLite é complicado. O desempenho de insertos em massa de um aplicativo C pode variar de 85 inserções por segundo a mais de 96.000 inserções por segundo!

Antecedentes: estamos usando o SQLite como parte de um aplicativo de desktop. Temos grandes quantidades de dados de configuração armazenados em arquivos XML que são analisados ​​e carregados em um banco de dados SQLite para processamento adicional quando o aplicativo é inicializado. O SQLite é ideal para essa situação porque é rápido, não requer configuração especializada e o banco de dados é armazenado em disco como um único arquivo.

Justificativa: Inicialmente, fiquei decepcionado com o desempenho que estava vendo. Acontece que o desempenho do SQLite pode variar significativamente (tanto para inserções em massa quanto para seleções), dependendo de como o banco de dados está configurado e de como você está usando a API. Não era uma questão trivial descobrir quais eram todas as opções e técnicas; portanto, achei prudente criar essa entrada no wiki da comunidade para compartilhar os resultados com os leitores do Stack Overflow, a fim de evitar que outras pessoas tenham problemas com as mesmas investigações.

A experiência: em vez de simplesmente falar sobre dicas de desempenho no sentido geral (por exemplo, "Use uma transação!" ), Achei melhor escrever um código C e realmente medir o impacto de várias opções. Vamos começar com alguns dados simples:

  • Um arquivo de texto delimitado por TAB de 28 MB (aproximadamente 865.000 registros) da programação completa de trânsito para a cidade de Toronto
  • Minha máquina de teste é um P4 de 3,60 GHz executando o Windows XP.
  • O código é compilado com o Visual C ++ 2005 como "Release" com "Full Optimization" (/ Ox) e Favor Fast Code (/ Ot).
  • Estou usando o SQLite "Amalgamation", compilado diretamente no meu aplicativo de teste. A versão do SQLite que eu tenho é um pouco mais antiga (3.6.7), mas eu suspeito que esses resultados serão comparáveis ​​aos da versão mais recente (por favor, deixe um comentário se você pensa em contrário).

Vamos escrever um código!

O código: um programa C simples que lê o arquivo de texto linha por linha, divide a string em valores e insere os dados em um banco de dados SQLite. Nesta versão "de linha de base" do código, o banco de dados é criado, mas na verdade não inseriremos dados:

/*************************************************************
    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;
}

O controle"

A execução do código como está realmente não executa nenhuma operação do banco de dados, mas nos dará uma idéia de quão rápidas são as operações de E / S do arquivo C bruto e de processamento de cadeia.

864913 registros importados em 0,94 segundos

Ótimo! Podemos fazer 920.000 inserções por segundo, desde que não façamos nenhuma inserção :-)


O "cenário de pior caso"

Vamos gerar a string SQL usando os valores lidos no arquivo e chamar essa operação 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);

Isso será lento porque o SQL será compilado no código VDBE para cada inserção e cada inserção ocorrerá em sua própria transação. Quão lento?

864913 registros importados em 9933,61 segundos

Caramba! 2 horas e 45 minutos! São apenas 85 inserções por segundo.

Usando uma transação

Por padrão, o SQLite avaliará todas as instruções INSERT / UPDATE em uma transação exclusiva. Se você estiver executando um grande número de inserções, é recomendável agrupar sua operação em uma transação:

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 registros importados em 38,03 segundos

Isso é melhor. O empacotamento simples de todas as nossas pastilhas em uma única transação melhorou nosso desempenho para 23.000 inserções por segundo.

Usando uma declaração preparada

Usar uma transação foi uma grande melhoria, mas recompilar a instrução SQL para cada inserção não faz sentido se usarmos o mesmo SQL repetidamente. Vamos usar sqlite3_prepare_v2para compilar nossa instrução SQL uma vez e depois vincular nossos parâmetros a essa instrução 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 registros importados em 16,27 segundos

Agradável! Há um pouco mais de código (não se esqueça de ligar sqlite3_clear_bindingse sqlite3_reset), mas mais que dobramos nosso desempenho para 53.000 inserções por segundo.

PRAGMA síncrono = DESLIGADO

Por padrão, o SQLite fará uma pausa após emitir um comando de gravação no nível do SO. Isso garante que os dados sejam gravados no disco. Ao definir synchronous = OFF, estamos instruindo o SQLite a simplesmente transferir os dados para o SO para gravação e continuar. É possível que o arquivo do banco de dados seja corrompido se o computador sofrer uma falha catastrófica (ou falha de energia) antes que os dados sejam gravados no prato:

/* 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 registros importados em 12,41 segundos

As melhorias agora são menores, mas temos até 69.600 inserções por segundo.

PRAGMA journal_mode = MEMORY

Considere armazenar o diário de reversão na memória avaliando PRAGMA journal_mode = MEMORY. Sua transação será mais rápida, mas se você perder energia ou seu programa travar durante uma transação, o banco de dados poderá ser deixado em um estado corrompido com uma transação parcialmente concluída:

/* 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 registros importados em 13,50 segundos

Um pouco mais lento que a otimização anterior, com 64.000 inserções por segundo.

PRAGMA síncrono = OFF e PRAGMA journal_mode = MEMORY

Vamos combinar as duas otimizações anteriores. É um pouco mais arriscado (no caso de uma falha), mas estamos apenas importando dados (não executando um banco):

/* 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 registros importados em 12,00 segundos

Fantástico! Podemos fazer 72.000 inserções por segundo.

Usando um banco de dados na memória

Só para começar, vamos aproveitar todas as otimizações anteriores e redefinir o nome do arquivo do banco de dados, de modo que estamos trabalhando inteiramente na RAM:

#define DATABASE ":memory:"

864913 registros importados em 10,94 segundos

Não é super prático armazenar nosso banco de dados na RAM, mas é impressionante que possamos executar 79.000 inserções por segundo.

Refatorando o Código C

Embora não seja especificamente uma melhoria do SQLite, não gosto das char*operações de atribuição extra no whileloop. Vamos refatorar rapidamente esse código para transmitir strtok()diretamente a saída sqlite3_bind_text()e deixar o compilador tentar acelerar as coisas para nós:

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: Voltamos a usar um arquivo de banco de dados real. Os bancos de dados na memória são rápidos, mas não necessariamente práticos

864913 registros importados em 8,94 segundos

Uma leve refatoração do código de processamento de string usado em nossa ligação de parâmetro nos permitiu realizar 96.700 inserções por segundo. Eu acho que é seguro dizer que isso é muito rápido . Quando começamos a ajustar outras variáveis ​​(por exemplo, tamanho da página, criação de índice etc.), este será o nosso parâmetro de comparação.


Resumo (até agora)

Espero que você ainda esteja comigo! A razão pela qual começamos nesse caminho é que o desempenho de inserção em massa varia muito com o SQLite, e nem sempre é óbvio que mudanças precisam ser feitas para acelerar nossa operação. Usando o mesmo compilador (e opções de compilador), a mesma versão do SQLite e os mesmos dados, otimizamos nosso código e nosso uso do SQLite para passar de um cenário de pior caso de 85 inserções por segundo para mais de 96.000 inserções por segundo!


CRIAR INDEX e INSERT vs. INSERT e CREATE INDEX

Antes de começarmos a medir o SELECTdesempenho, sabemos que criaremos índices. Foi sugerido em uma das respostas abaixo que, ao fazer inserções em massa, é mais rápido criar o índice após a inserção dos dados (em vez de criar o índice primeiro e depois inserir os dados). Vamos tentar:

Criar índice e depois inserir dados

sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...

864913 registros importados em 18,13 segundos

Inserir dados e criar índice

...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);

864913 registros importados em 13,66 segundos

Como esperado, as inserções em massa são mais lentas se uma coluna for indexada, mas faz diferença se o índice for criado após a inserção dos dados. Nossa linha de base sem índice é de 96.000 inserções por segundo. Criar o índice primeiro e depois inserir dados fornece 47.700 inserções por segundo, enquanto inserir os dados primeiro e depois criar o índice fornece 63.300 inserções por segundo.


Eu ficaria feliz em sugerir sugestões para outros cenários para tentar ... E compilaremos dados semelhantes para consultas SELECT em breve.

Mike Willekes
fonte
8
Bom ponto! No nosso caso, estamos lidando com aproximadamente 1,5 milhão de pares de chave / valor lidos de arquivos de texto XML e CSV em 200k registros. Pequeno em comparação com bancos de dados que executam sites como SO - mas grande o suficiente para que o desempenho do SQLite se torne importante.
Mike Willekes
51
"Temos grandes quantidades de dados de configuração armazenados em arquivos XML que são analisados ​​e carregados em um banco de dados SQLite para processamento adicional quando o aplicativo é inicializado." por que você não mantém tudo no banco de dados sqlite em primeiro lugar, em vez de armazenar em XML e carregar tudo no momento da inicialização?
CAFxX
14
Você já tentou não ligar sqlite3_clear_bindings(stmt);? Você define as ligações toda vez que isso deve ser suficiente: Antes de chamar sqlite3_step () pela primeira vez ou imediatamente após sqlite3_reset (), o aplicativo pode chamar uma das interfaces sqlite3_bind () para anexar valores aos parâmetros. Cada chamada para sqlite3_bind () substitui as ligações anteriores no mesmo parâmetro (consulte: sqlite.org/cintro.html ). Não há nada nos documentos para essa função dizendo que você deve chamá-la.
ahcox
21
Você fez medições repetidas? O 4s "ganha" por evitar 7 indicadores locais é estranho, mesmo assumindo um otimizador confuso.
Peterchen
5
Não use feof()para controlar a finalização do seu loop de entrada. Use o resultado retornado por fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson

Respostas:

785

Várias dicas:

  1. Coloque inserções / atualizações em uma transação.
  2. Para versões mais antigas do SQLite - considere um modo de diário menos paranóico ( pragma journal_mode). Existe NORMAL, e existe OFF, o que pode aumentar significativamente a velocidade da inserção, se você não estiver muito preocupado com a possibilidade de o banco de dados ser corrompido se o sistema operacional travar. Se o seu aplicativo travar, os dados devem estar bem. Observe que nas versões mais recentes, as OFF/MEMORYconfigurações não são seguras para falhas no nível do aplicativo.
  3. Brincar com tamanhos de página também faz a diferença ( PRAGMA page_size). Ter tamanhos de página maiores pode tornar as leituras e gravações um pouco mais rápidas à medida que páginas maiores são mantidas na memória. Observe que mais memória será usada para o seu banco de dados.
  4. Se você tiver índices, considere ligar CREATE INDEXdepois de fazer todas as suas inserções. Isso é significativamente mais rápido do que criar o índice e depois fazer suas inserções.
  5. Você precisa ter muito cuidado se tiver acesso simultâneo ao SQLite, pois todo o banco de dados é bloqueado quando as gravações são concluídas e, embora vários leitores sejam possíveis, as gravações serão bloqueadas. Isso foi melhorado com a adição de um WAL nas versões mais recentes do SQLite.
  6. Aproveite a economia de espaço ... bancos de dados menores são mais rápidos. Por exemplo, se você tiver pares de valores de chave, tente transformar a chave em INTEGER PRIMARY KEYse possível, o que substituirá a coluna de número de linha exclusivo implícita na tabela.
  7. Se você estiver usando vários threads, tente usar o cache da página compartilhada , o que permitirá que as páginas carregadas sejam compartilhadas entre os threads, o que pode evitar chamadas de E / S caras.
  8. Não use !feof(file)!

Eu também fiz perguntas semelhantes aqui e aqui .

Snazzer
fonte
9
Os documentos não conhecem um PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld
4
Já faz um tempo, minhas sugestões se aplicavam a versões mais antigas antes da introdução de um WAL. Parece que DELETE é a nova configuração normal, e agora também há as configurações OFF e MEMORY. Suponho que OFF / MEMORY melhorará o desempenho de gravação às custas da integridade do banco de dados, e OFF desativa completamente as reversões.
Snazzer
4
para o nº 7, você tem um exemplo de como habilitar o cache de páginas compartilhadas usando o wrapper c # system.data.sqlite?
Aaron Hudon
4
O nº 4 trouxe de volta memórias antigas - Houve pelo menos um caso nos tempos anteriores em que remover um índice antes de um grupo de adições e recriá-lo depois acelerava significativamente as inserções. Ainda pode funcionar mais rapidamente em sistemas modernos para algumas adições, onde você sabe que tem acesso exclusivo à tabela durante o período.
Bill K
Polegares para o # 1: Eu tive muita sorte com as transações.
Enno
146

Tente usar em SQLITE_STATICvez de SQLITE_TRANSIENTpara essas inserções.

SQLITE_TRANSIENT fará com que o SQLite copie os dados da string antes de retornar.

SQLITE_STATICinforma que o endereço de memória que você forneceu será válido até que a consulta seja executada (que nesse loop é sempre o caso). Isso economizará várias operações de alocação, cópia e desalocação por loop. Possivelmente uma grande melhoria.

Alexander Farber
fonte
109

Evite sqlite3_clear_bindings(stmt).

O código no teste define as ligações todas as vezes pelas quais deve ser suficiente.

A introdução da API C dos documentos SQLite diz:

Antes de chamar sqlite3_step () pela primeira vez ou imediatamente após sqlite3_reset () , o aplicativo pode chamar as interfaces sqlite3_bind () para anexar valores aos parâmetros. Cada chamada para sqlite3_bind () substitui as ligações anteriores no mesmo parâmetro

Não há nada nos documentos para sqlite3_clear_bindingsdizer que você deve chamá-lo, além de simplesmente definir as ligações.

Mais detalhes: Avoid_sqlite3_clear_bindings ()

ahcox
fonte
5
Maravilhosamente certo: "Ao contrário da intuição de muitos, sqlite3_reset () não redefine as ligações em uma instrução preparada. Use esta rotina para redefinir todos os parâmetros do host como NULL." # sqlite.org/c3ref/clear_bindings.html
Francis Straccia
63

Em pastilhas a granel

Inspirado neste post e na pergunta Stack Overflow que me levou aqui - É possível inserir várias linhas por vez em um banco de dados SQLite? - Publiquei meu primeiro repositório Git :

https://github.com/rdpoor/CreateOrUpdate

que carrega em massa uma matriz de ActiveRecords nos bancos de dados MySQL , SQLite ou PostgreSQL . Inclui uma opção para ignorar registros existentes, substituí-los ou gerar um erro. Meus benchmarks rudimentares mostram uma melhoria de velocidade de 10x em comparação com gravações seqüenciais - YMMV.

Estou usando-o no código de produção, onde frequentemente preciso importar grandes conjuntos de dados e estou muito feliz com isso.

fearless_fool
fonte
4
@ Jess: Se você seguir o link, verá que ele se referiu à sintaxe de inserção em lote.
Alix Axel
48

As importações em massa parecem ter um desempenho melhor se você puder agrupar suas instruções INSERT / UPDATE . Um valor de 10.000 ou mais funcionou bem para mim em uma tabela com apenas algumas linhas, YMMV ...

Leon
fonte
22
Você deseja ajustar x = 10.000 para que x = cache [= cache_size * page_size] / tamanho médio da sua inserção.
Alix Axel
43

Se você se importa apenas com a leitura, a versão um pouco mais rápida (mas pode ler dados obsoletos) é ler de várias conexões de vários threads (conexão por thread).

Primeiro encontre os itens, na tabela:

SELECT COUNT(*) FROM table

depois leia nas páginas (LIMIT / OFFSET):

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

onde e são calculados por thread, assim:

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

para cada thread:

int offset = thread_index * limit

Para nosso pequeno db (200mb), isso acelerou de 50 a 75% (3.8.0.2 de 64 bits no Windows 7). Nossas tabelas são altamente não normalizadas (1000-1500 colunas, aproximadamente 100.000 ou mais linhas).

Threads em excesso ou em excesso não o farão; você precisa fazer um benchmark e criar um perfil.

Também para nós, o SHAREDCACHE tornou o desempenho mais lento, então eu coloquei manualmente o PRIVATECACHE (porque ele foi ativado globalmente para nós)

malkia
fonte
29

Não obtive nenhum ganho com as transações até aumentar o cache_size para um valor maior, ou seja, PRAGMA cache_size=10000;

anefeletos
fonte
Observe que o uso de um valor positivo para cache_sizedefine o número de páginas a serem armazenadas em cache , não o tamanho total da RAM. Com o tamanho de página padrão de 4kB, essa configuração armazena até 40 MB de dados por arquivo aberto (ou por processo, se estiver executando com cache compartilhado ).
Groo 23/01
21

Depois de ler este tutorial, tentei implementá-lo no meu programa.

Eu tenho 4-5 arquivos que contêm endereços. Cada arquivo possui aproximadamente 30 milhões de registros. Estou usando a mesma configuração que você está sugerindo, mas meu número de INSERTs por segundo é muito baixo (~ 10.000 registros por segundo).

Aqui é onde sua sugestão falha. Você usa uma única transação para todos os registros e uma única inserção sem erros / falhas. Digamos que você esteja dividindo cada registro em várias inserções em tabelas diferentes. O que acontece se o registro for quebrado?

O comando ON CONFLICT não se aplica, pois se você tiver 10 elementos em um registro e precisar de cada elemento inserido em uma tabela diferente, se o elemento 5 receber um erro CONSTRAINT, todas as 4 inserções anteriores também deverão ser executadas.

Então aqui é onde a reversão vem. O único problema com a reversão é que você perde todas as inserções e começa do topo. Como você pode resolver isso?

Minha solução foi usar várias transações. Começo e termino uma transação a cada 10.000 registros (não pergunte por que esse número foi o mais rápido que testei). Criei uma matriz com tamanho 10.000 e insira os registros de sucesso lá. Quando o erro ocorre, faço uma reversão, inicio uma transação, insiro os registros da minha matriz, confirmamos e inicio uma nova transação após o registro quebrado.

Essa solução me ajudou a contornar os problemas que tenho ao lidar com arquivos contendo registros inválidos / duplicados (eu tinha quase 4% de registros incorretos).

O algoritmo que criei me ajudou a reduzir meu processo em 2 horas. Processo final de carregamento do arquivo 1hr 30m, que ainda é lento, mas não comparado às 4 horas que ele levou inicialmente. Consegui acelerar as pastilhas de 10.000 / sa ~ 14.000 / s

Se alguém tiver outras idéias sobre como acelerar isso, estou aberto a sugestões.

ATUALIZAÇÃO :

Além da minha resposta acima, você deve ter em mente que as inserções por segundo, dependendo do disco rígido que você está usando também. Testei-o em 3 PCs diferentes com discos rígidos diferentes e obtive grandes diferenças nos tempos. PC1 (1hr 30m), PC2 (6hrs) PC3 (14hrs), então comecei a me perguntar por que isso seria.

Após duas semanas de pesquisa e verificação de vários recursos: Disco rígido, RAM, Cache, descobri que algumas configurações no disco rígido podem afetar a taxa de E / S. Ao clicar em propriedades na unidade de saída desejada, você pode ver duas opções na guia geral. Opt1: Compactar esta unidade, Opt2: Permitir que os arquivos desta unidade tenham o conteúdo indexado.

Ao desabilitar essas duas opções, todos os 3 computadores agora levam aproximadamente o mesmo tempo para concluir (1 hora e 20 a 40 minutos). Se você encontrar pastilhas lentas, verifique se o seu disco rígido está configurado com essas opções. Você economizará muito tempo e dores de cabeça tentando encontrar a solução

Jimmy_A
fonte
Vou sugerir o seguinte. * Use SQLITE_STATIC vs SQLITE_TRANSIENT para evitar uma cópia da string. Você deve garantir que a string não seja alterada antes da transação ser executada. * Use a inserção em massa INSERT INTO stop_times VALUES (NULL,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?) * Mmap o arquivo para reduzir o número de syscalls.
Rouzier
Fazendo que eu sou capaz de importar 5,582,642 registros em 11,51 segundos
Rouzier
-1

Use o ContentProvider para inserir os dados em massa no banco de dados. O método abaixo usado para inserir dados em massa no banco de dados. Isso deve melhorar o desempenho do INSERT por segundo do 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();

}

Chame o método bulkInsert:

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

Link: https://www.vogella.com/tutorials/AndroidSQLite/article.html verifique usando a seção ContentProvider para obter mais detalhes

vishnuc156
fonte