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_v2
para 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_bindings
e 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 while
loop. 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 SELECT
desempenho, 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.
fonte
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.feof()
para controlar a finalização do seu loop de entrada. Use o resultado retornado porfgets()
. stackoverflow.com/a/15485689/827263Respostas:
Várias dicas:
pragma journal_mode
). ExisteNORMAL
, e existeOFF
, 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, asOFF/MEMORY
configurações não são seguras para falhas no nível do aplicativo.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.CREATE INDEX
depois de fazer todas as suas inserções. Isso é significativamente mais rápido do que criar o índice e depois fazer suas inserções.INTEGER PRIMARY KEY
se possível, o que substituirá a coluna de número de linha exclusivo implícita na tabela.!feof(file)
!Eu também fiz perguntas semelhantes aqui e aqui .
fonte
Tente usar em
SQLITE_STATIC
vez deSQLITE_TRANSIENT
para essas inserções.SQLITE_TRANSIENT
fará com que o SQLite copie os dados da string antes de retornar.SQLITE_STATIC
informa 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.fonte
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:
Não há nada nos documentos para
sqlite3_clear_bindings
dizer que você deve chamá-lo, além de simplesmente definir as ligações.Mais detalhes: Avoid_sqlite3_clear_bindings ()
fonte
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/CreateOrUpdateque 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.
fonte
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 ...
fonte
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:
depois leia nas páginas (LIMIT / OFFSET):
onde e são calculados por thread, assim:
para cada thread:
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)
fonte
Não obtive nenhum ganho com as transações até aumentar o cache_size para um valor maior, ou seja,
PRAGMA cache_size=10000;
fonte
cache_size
define 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 ).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
fonte
A resposta para sua pergunta é que o SQLite 3 mais recente melhorou o desempenho, use isso.
Esta resposta Por que o SQLAlchemy insert com sqlite é 25 vezes mais lento que o sqlite3 diretamente? por SqlAlchemy Orm O autor tem 100k inserções em 0,5 s, e eu tenho visto resultados semelhantes com python-sqlite e SqlAlchemy. O que me leva a acreditar que o desempenho melhorou com o SQLite 3.
fonte
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.
Chame o método bulkInsert:
Link: https://www.vogella.com/tutorials/AndroidSQLite/article.html verifique usando a seção ContentProvider para obter mais detalhes
fonte