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.
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 por fgets()
. stackoverflow.com/a/15485689/827263