Melhorar o desempenho de inserção por segundo do SQLite?

Otimizar o SQLite é complicado. O desempenho do Bulk-insert de uma aplicação C pode variar de 85 inserções por segundo a mais de 96.000 inserções por segundo!

fundo: estamos a usar o SQLite como parte de uma aplicação desktop. Temos grandes quantidades de dados de configuração armazenados em arquivos XML que são processados e carregados em um banco de dados SQLite para processamento posterior quando a aplicação é inicializada. O SQLite é ideal para esta situação porque é rápido, requer nenhuma configuração especializada, e o banco de dados é armazenado em disco como um único arquivo.

fundamentação: inicialmente fiquei desapontado com o desempenho que estava vendo. acontece que o desempenho do SQLite pode variar significativamente (tanto para inserções em massa como para seleções) dependendo de como a base de dados está configurada e como você está usando a API. Não era um assunto trivial para descobrir quais eram todas as opções e técnicas, então eu pensei que era prudente criar este entrada comunitária wiki para compartilhar os resultados com os leitores de Stack Overflow, a fim de salvar outros o problema das mesmas investigações.

The Experiment: Rather than simply talking about performance tips in the general sense (i.e. " Use a transaction!"), eu achei melhor escrever algum código C e realmente medir o impacto de várias opções. Vamos começar com alguns dados simples:

  • um ficheiro de texto delimitado por tabulações de 28 MB (aproximadamente 865.000 registos) do plano de trânsito completo para a cidade de Toronto
  • a minha máquina de testes é uma janela XP de 3,60 GHz P4 em execução.
  • o código é compilado comVisual C++ 2005 como "Release" com "Full Optimization" (/Ox) E Favor Fast Code (/Ot).
  • Estou a usar a "amalgamação" SQLite, compilada directamente na minha aplicação de teste. A versão SQLite que eu tenho é um pouco mais velha( 3.6.7), mas eu suspeito que estes resultados serão comparável ao último lançamento (por favor deixe um comentário se você pensar de outra forma).

vamos escrever um código!

o código: UM programa C simples que lê o ficheiro de texto linha-a-linha, divide o texto em valores e, em seguida, irá inserir os dados numa base de dados SQLite. Nesta versão "baseline" do código, a base de dados é criada, mas não vamos inserir 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 "Controlo"

a correr o código tal como está na verdade não executa nenhuma operação de banco de dados, mas nos dará uma idéia de quão rápido são as operações de processamento de arquivo C raw e string.

importou 864913 registos em 0,94. segundos

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


O "Pior Cenário"

vamos gerar o texto SQL usando os valores lidos do ficheiro e invocar 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);

isto vai ser lento porque o SQL será compilado em código VDBE para cada sequência inserida e cada sequência irá acontecer na sua própria transacção. Quão lento?

importou 864913 registos em 9933.61 segundos

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

usando uma transacção

por omissão, o SQLite irá avaliar todas as instruções de inserção / actualização dentro de uma instrução única transaccao. Se efectuar um grande número de inserções, é aconselhável terminar a sua operação numa transacçã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);

importou 864913 registos em 38.03 segundos

Assim está melhor. Simplesmente embrulhar todas as nossas inserções em uma única transação melhorou o nosso desempenho para 23.000 inserções por segundo.

utilizando uma declaração preparada

usar uma transacção foi uma grande melhoria, mas recompilar a declaração SQL para cada inserção não faz sentido se usarmos o mesmo SQL vezes sem conta. Vamos usar sqlite3_prepare_v2 para compilar a nossa declaração SQL uma vez e depois ligar os nossos parâmetros a essa Declaraçã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;

importou 864913 registos em 16.27 segundos

Boa! Há um pouco mais de código (não se esqueça de chamar sqlite3_clear_bindings e sqlite3_reset), mas nós mais do que duplicamos o nosso desempenho para 53.000 inserções por segundo.

PRAGMA síncrono = OFF

por omissão, o SQLite irá pausa após a emissão de um comando de escrita ao nível do sistema operacional. Isto garante que os dados são escritos no disco. Ao definir synchronous = OFF, estamos a instruir o SQLite para simplesmente transferir os dados para o SO para escrita e depois continuar. Há uma chance de que o arquivo de banco de dados pode se corromper se o computador sofrer um desastre catastrófico (ou falha de energia) antes que os dados sejam escritos no platter:

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

importou 864913 registos em 12.41 segundos

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

PRAGMA journal_mode = memória

considere armazenar o diário de rollback em memória, avaliando PRAGMA journal_mode = MEMORY. A sua transacção será mais rápida, mas se perder energia ou o seu programa falhar durante uma transacção, a sua base de dados poderá ficar num estado corrompido com uma transacçã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);

importou 864913 registos em 13,50 segundos

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

PRAGMA synchronous = OFF and PRAGMA journal_mode = MEMORY

Vamos combinar as duas optimizações anteriores. É um pouco mais arriscado (em caso de acidente), 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);

importou 864913 registos às 12h00. segundos

Fantástico! Somos capazes de fazer 72 mil inserções por segundo.

usando uma memória Base de dados

só por gozo, vamos construir sobre todas as optimizações anteriores e redefinir o nome do ficheiro de base de dados para que estejamos a trabalhar inteiramente em RAM:

#define DATABASE ":memory:"

importou 864913 registos em 10.94 segundos

Não é muito prático armazenar a nossa base de dados em MEMÓRIA RAM, mas é impressionante que possamos realizar 79.000 inserções por segundo.

Refactoring C Código

embora não seja especificamente uma melhoria SQLite, eu não gosto da operações de atribuição extra char* no circuito while. Vamos rapidamente refazer esse código para passar a saída de strtok() diretamente em 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: estamos de volta a usar um arquivo de banco de dados real. As bases de dados na memória são rápidas, mas não necessariamente práticas.

importou 864913 registos em 8.94 segundos

um ligeiro ajuste ao código de processamento de texto usado na nossa ligação de parâmetros permitiu-nos realizar 96.700 inserções por segundo.Acho que é seguro dizer que isto é muito rápido. À medida que começamos a ajustar outras variáveis (ou seja, tamanho da página, criação de índice, etc.) esta será a nossa referência.


resumo (até agora)

espero que ainda estejas comigo! a razão pela qual começamos por este caminho é que o desempenho do bulk-insert varia tão loucamente com o SQLite, e nem sempre é óbvio que mudanças precisam ser feitas para acelerar o nosso operacao. Usando o mesmo compilador (e opções de compilador), a mesma versão do SQLite e os mesmos dados que otimizamos o nosso código e o nosso uso do SQLite para ir de um pior cenário de 85 inserções por segundo para mais de 96.000 inserções por segundo!


criar um índice depois inserir vs. inserir depois criar um índice

Antes de começarmos a medir o desempenho, sabemos que vamos criar índices. Foi sugerido em uma das respostas abaixo que ao fazer inserções em massa, é mais rápido criar o índice após os dados terem sido inseridos (ao contrário de criar o índice primeiro, em seguida, inserir os dados). Vamos tentar.

criar índice e inserir Dados

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

importou 864913 registos em 18.13 segundos

inserir dados e criar um índice

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

importou 864913 registos em 13.66 segundos

Como esperado, as pastilhas a granel são mais lentas se uma coluna for indexado, mas faz diferença se o índice é criado após os dados serem inseridos. A nossa linha de base sem índice é de 96 mil inserções por segundo. criar o índice primeiro, depois inserir dados dá-nos 47.700 inserções por segundo, enquanto inserir os dados primeiro, depois criar o índice, dá-nos 63.300 inserções por segundo.


Aceitaria sugestões para outros cenários... E será compilar dados semelhantes para selecionar consultas em breve.

Author: Mike Willekes, 2009-11-11

9 answers

Várias dicas:

  1. Coloque inserções / actualizações numa transacção.
  2. para versões mais antigas do SQLite-considere um modo de revista menos paranóico ({[[0]}). Há NORMAL, e depois há OFF, o que pode aumentar significativamente a velocidade de inserção se não estiver muito preocupado com a possibilidade de a base de dados ficar corrompida se o sistema operacional falhar. Se a sua aplicação falhar, os dados devem ser bons. Note que nas versões mais recentes, a configuração OFF/MEMORY não é segura para o nível da aplicação falha.
  3. brincar com o tamanho das páginas também faz diferença (PRAGMA page_size). Ter tamanhos de página maiores pode fazer as leituras e as escritas ir um pouco mais rápido como as páginas maiores são mantidos na memória. Note que mais memória será usada para o seu banco de dados.
  4. Se você tem índices, considere ligar CREATE INDEX Depois de fazer todas as suas inserções. Isto é significativamente mais rápido do que criar o índice e, em seguida, fazer suas inserções.
  5. Tem de ter muito cuidado se tem acesso simultâneo ao SQLite, como o toda a base de dados é bloqueada quando as escritas são feitas, e embora vários leitores são possíveis, as escritas serão bloqueadas. Isso foi melhorado um pouco com a adição de um WAL em versões SQLite mais recentes.
  6. Aproveita para poupar espaço...bases de dados mais pequenas são mais rápidas. Por exemplo, se você tiver pares de valores chave, tente fazer da chave um INTEGER PRIMARY KEY se possível, que irá substituir a coluna de número de linha única implícita na tabela.
  7. Se estiver a usar vários tópicos, pode tentar usar o 'cache' de páginas partilhadas , que permitirá que as páginas carregadas sejam partilhadas entre os tópicos, o que poderá evitar chamadas de E/S dispendiosas.
  8. Não uses !feof(file)!

Também fiz perguntas semelhantes aquie aqui.

 682
Author: Snazzer, 2018-06-04 15:07:47

Tente usar SQLITE_STATIC em vez de SQLITE_TRANSIENT para essas inserções.

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

SQLITE_STATIC diz - lhe que o endereço de memória que lhe deu será válido até que a consulta tenha sido realizada (o que neste loop é sempre o caso). Isto irá poupar - lhe várias operações de alocar, copiar e desalocar por ciclo. Possivelmente uma grande melhoria.

 108
Author: Alexander Farber, 2015-08-28 16:54:11

Evitar as combinações sqlite3_ clear_ bindings(stmt);

O código no teste define as ligações todas as vezes através das 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 (), a aplicação pode invocar um dos interfaces sqlite3_bind () para anexar valores aos parâmetros. Um a chamada para o sqlite3_ Bend () sobrepõe-se a ligações anteriores no mesmo parâmetro

(ver: sqlite.org/cintro.html). Não há nada no google docs para essa função dizendo que você deve chamá-lo de, além de simplesmente definir as ligações.

Mais detalhes: http://www.hoogli.com/blogs/micro/index.html#Avoid_sqlite3_clear_bindings()

 85
Author: ahcox, 2012-08-08 14:44:20

Em pastilhas a granel

Inspirado por este post e pela questão do fluxo de pilha que me trouxe aqui -- é possível inserir várias linhas de cada vez numa base de dados SQLite? -- eu postei meu primeiro repositório git:

https://github.com/rdpoor/CreateOrUpdate

Qual o volume que carrega um conjunto de acordes de Acção em MySQL, SQLite ou PostgreSQL} bases de dados. Inclui uma opção para ignorar registos existentes, sobrepô-los ou criar um erro. Os meus padrões rudimentares mostram uma melhoria de velocidade de 10x em comparação com as letras sequenciais ... MMMV.

Estou a usá-lo em código de produção, onde frequentemente preciso de importar grandes conjuntos de dados, e estou muito feliz com isso.
 49
Author: fearless_fool, 2017-05-23 12:02:48

As importações a granel parecem ter o melhor desempenho se conseguir encaixar as suas declarações Inserir/actualizar. Um valor de cerca de 10.000 funcionou bem para mim em uma mesa com apenas algumas linhas, YMMV...

 42
Author: Leon, 2012-02-21 08:30:20

Se só se importa com a leitura, a versão um pouco mais rápida (mas poderá ler dados obsoletos) é ler a partir de várias ligações de vários tópicos (ligação por tópico).

Primeiro encontra os itens, na Tabela:

 SELECT COUNT(*) FROM table

Depois ler nas páginas (limite/deslocamento)

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

Onde e são calculados por fio, assim:

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

Para cada fio:

int offset = thread_index * limit

Para o nosso pequeno (200mb) db isto fez 50-75% de aumento de velocidade (3.8.0.2 64 bits no Windows 7). As nossas mesas são fortemente não normalizado (1000-1500 colunas, cerca de 100 000 ou mais linhas).

Demasiados ou poucos fios não o fazem, você precisa de se comparar e traçar o seu perfil.

Também para nós, o SHAREDCACHE fez o desempenho mais lento, por isso coloquei manualmente o PRIVATECACHE (porque foi activado globalmente para nós)

 34
Author: malkia, 2014-06-23 05:49:58

Não consegui obter qualquer ganho com as transacções até ter aumentado a escala para um valor mais elevado, isto é,PRAGMA cache_size=10000;

 22
Author: anefeletos, 2015-04-15 09:47:49

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

Tenho 4-5 ficheiros que contêm endereços. Cada arquivo tem cerca de 30 milhões de registros. Estou usando a mesma configuração que você está sugerindo, mas meu número de inserções por segundo é muito baixo (~10.000 registros por segundo). Aqui é onde a tua sugestão falha. Você usa uma única transação para todos os registros e uma única inserção sem erros/falhas. Digamos que estás a dividir cada disco em múltiplas inserções em tabelas diferentes. O que acontece se o disco for quebrado?

O comando ON CONFLICT não se aplica, porque se você tem 10 elementos em um registro e você precisa de cada elemento inserido em uma tabela diferente, se o elemento 5 recebe um erro de restrição, então todos os 4 inserções anteriores precisam ir também.

Então é aqui que vem o rollback. O único problema com o rollback é que você perde todas as suas inserções e começar do início. Como podes resolver isto? A minha solução foi: para utilizar múltiplas transacções. Eu 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 eu testei). Criei uma matriz dimensionada em 10.000 e inseri os registos de sucesso lá. Quando o erro ocorre, eu faço uma rollback, começo uma transação, insiro os registros de meu array, commit e, em seguida, iniciar uma nova transação após o registro quebrado. Esta solução ajudou-me a ultrapassar os problemas que tenho ao lidar com ficheiros que contêm má / duplicada. registros (eu tinha quase 4% de registros ruins). O algoritmo que criei ajudou-me a reduzir o meu processo em 2 horas. Processo de carregamento Final do arquivo 1hr 30m que ainda é lento, mas não em comparação com os 4hrs que inicialmente tomou. Consegui acelerar as inserções de 10.000 / s para ~ 14.000 / s Se alguém tem outras ideias para acelerar, estou aberto a sugestões.

Actualizar:

Além da minha resposta acima, deve ter em mente que insere por segundo dependendo do disco rígido que você está usando também. Testei-o em 3 computadores diferentes com discos rígidos diferentes e tive grandes diferenças nos tempos. PC1 (1hr 30m), PC2 (6hrs) PC3 (14hrs), então eu 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 seu disco rígido podem afetar a taxa de E / S. Ao carregar nas propriedades da sua unidade de saída desejada, poderá ver duas opções na página Geral. Opt1: Comprimir esta unidade, Opt2: permitir que os arquivos desta unidade para ter conteúdo indexado. Ao desativar estas duas opções, todos os 3 PCs levam aproximadamente o mesmo tempo para terminar (1hr e 20 a 40min). Se encontrar inserções lentas, verifique se o seu disco rígido está configurado com estas opções. Vai poupar-lhe muito tempo e dores de cabeça a tentar encontrar a solução.
 14
Author: Jimmy_A, 2018-01-06 11:07:06

A resposta à sua pergunta é que o sqlite3 mais recente melhorou o desempenho, use isso.

Esta resposta porque é que a inserção do SQLAlchemy com o sqlite é 25 vezes mais lenta do que a utilização directa do sqlite3? by SqlAlchemy ORM Author has 100k inserts in 0.5 sec, and I have seen similar results with python-sqlite and SqlAlchemy. O que me leva a crer que o desempenho melhorou com o sqlite3

 6
Author: doesnt_matter, 2017-06-15 20:31:47