Optimizar SQLite es complicado. ¡El rendimiento de inserción masiva de una aplicación C puede variar de 85 insertos por segundo a más de 96,000 insertos por segundo!
Antecedentes: estamos usando SQLite como parte de una aplicación de escritorio. Tenemos grandes cantidades de datos de configuración almacenados en archivos XML que se analizan y cargan en una base de datos SQLite para su posterior procesamiento cuando se inicializa la aplicación. SQLite es ideal para esta situación porque es rápido, no requiere una configuración especializada y la base de datos se almacena en el disco como un solo archivo.
Justificación: Inicialmente estaba decepcionado con el rendimiento que estaba viendo. Resulta que el rendimiento de SQLite puede variar significativamente (tanto para inserciones masivas como para selecciones) dependiendo de cómo esté configurada la base de datos y cómo esté utilizando la API. No era un asunto trivial descubrir cuáles eran todas las opciones y técnicas, por lo que pensé que era prudente crear esta entrada de la wiki comunitaria para compartir los resultados con los lectores de Stack Overflow con el fin de salvar a otros el problema de las mismas investigaciones.
El experimento: en lugar de simplemente hablar sobre consejos de rendimiento en el sentido general (es decir, "¡Use una transacción!" ), Pensé que era mejor escribir un código C y medir el impacto de varias opciones. Vamos a comenzar con algunos datos simples:
- Un archivo de texto delimitado por TAB de 28 MB (aproximadamente 865,000 registros) del horario de tránsito completo para la ciudad de Toronto
- Mi máquina de prueba es una P4 de 3.60 GHz con Windows XP.
- El código se compila con Visual C ++ 2005 como "Release" con "Optimización completa" (/ Ox) y Favorece el código rápido (/ Ot).
- Estoy usando la "Amalgamación" SQLite, compilada directamente en mi aplicación de prueba. La versión de SQLite que tengo es un poco más antigua (3.6.7), pero sospecho que estos resultados serán comparables a la última versión (deje un comentario si piensa lo contrario).
¡Escribamos un código!
El Código: un programa simple en C que lee el archivo de texto línea por línea, divide la cadena en valores y luego inserta los datos en una base de datos SQLite. En esta versión "básica" del código, se crea la base de datos, pero en realidad no insertaremos datos:
/*************************************************************
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;
}
El control"
Ejecutar el código tal como está en realidad no realiza ninguna operación de base de datos, pero nos dará una idea de cuán rápido son las operaciones de procesamiento de cadenas y E / S del archivo C sin procesar.
Importó 864913 registros en 0.94 segundos
¡Excelente! Podemos hacer 920,000 inserciones por segundo, siempre que no hagamos ninguna inserción :-)
El "peor de los casos"
Vamos a generar la cadena SQL usando los valores leídos del archivo e invocar esa operación 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);
Esto va a ser lento porque el SQL se compilará en el código VDBE para cada inserción y cada inserción ocurrirá en su propia transacción. Que lento
Importó 864913 registros en 9933.61 segundos
¡Ay! ¡2 horas y 45 minutos! Eso es solo 85 inserciones por segundo.
Usando una transacción
Por defecto, SQLite evaluará cada instrucción INSERT / UPDATE dentro de una transacción única. Si realiza una gran cantidad de inserciones, es recomendable ajustar su operación en una transacción:
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);
Importó 864913 registros en 38.03 segundos
Eso es mejor. Simplemente envolviendo todos nuestros insertos en una sola transacción mejoró nuestro rendimiento a 23,000 insertos por segundo.
Usar una declaración preparada
Usar una transacción fue una gran mejora, pero recompilar la instrucción SQL para cada inserción no tiene sentido si usamos el mismo SQL una y otra vez. Usemos sqlite3_prepare_v2
para compilar nuestra declaración SQL una vez y luego unir nuestros parámetros a esa declaración 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;
Importó 864913 registros en 16.27 segundos
¡Agradable! Hay un poco más de código (no olvides llamar sqlite3_clear_bindings
y sqlite3_reset
), pero hemos más que duplicado nuestro rendimiento a 53,000 inserciones por segundo.
PRAGMA síncrono = OFF
Por defecto, SQLite hará una pausa después de emitir un comando de escritura a nivel del sistema operativo. Esto garantiza que los datos se escriban en el disco. Al configurar synchronous = OFF
, le estamos ordenando a SQLite que simplemente entregue los datos al sistema operativo para escribir y luego continuar. Existe la posibilidad de que el archivo de la base de datos se corrompa si la computadora sufre un bloqueo catastrófico (o falla de energía) antes de que los datos se escriban en la bandeja:
/* 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);
Importó 864913 registros en 12.41 segundos
Las mejoras ahora son más pequeñas, pero tenemos hasta 69,600 inserciones por segundo.
PRAGMA journal_mode = MEMORIA
Considere almacenar el diario de reversión en la memoria mediante la evaluación PRAGMA journal_mode = MEMORY
. Su transacción será más rápida, pero si pierde energía o su programa se bloquea durante una transacción, su base de datos podría quedar en un estado corrupto con una transacción parcialmente completada:
/* 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);
Importó 864913 registros en 13.50 segundos
Un poco más lento que la optimización anterior a 64,000 inserciones por segundo.
PRAGMA síncrono = APAGADO y PRAGMA journal_mode = MEMORIA
Combinemos las dos optimizaciones anteriores. Es un poco más arriesgado (en caso de un colapso), pero solo estamos importando datos (no operando un 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);
Importó 864913 registros en 12.00 segundos
¡Fantástico! Podemos hacer 72,000 inserciones por segundo.
Usar una base de datos en memoria
Solo por diversión, desarrollemos todas las optimizaciones anteriores y redefinamos el nombre de archivo de la base de datos para que trabajemos completamente en RAM:
#define DATABASE ":memory:"
Importó 864913 registros en 10.94 segundos
No es muy práctico almacenar nuestra base de datos en RAM, pero es impresionante que podamos realizar 79,000 inserciones por segundo.
Refactorizando el Código C
Aunque no es específicamente una mejora de SQLite, no me gustan las char*
operaciones de asignación adicionales en el while
ciclo. Refactoricemos rápidamente ese código para pasar la salida strtok()
directamente sqlite3_bind_text()
y dejemos que el compilador intente acelerar las cosas por nosotros:
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: Hemos vuelto a usar un archivo de base de datos real. Las bases de datos en memoria son rápidas, pero no necesariamente prácticas.
Importó 864913 registros en 8.94 segundos
Una ligera refactorización del código de procesamiento de cadenas utilizado en nuestro enlace de parámetros nos ha permitido realizar 96.700 inserciones por segundo. Creo que es seguro decir que esto es bastante rápido . Cuando empecemos a ajustar otras variables (es decir, tamaño de página, creación de índice, etc.), este será nuestro punto de referencia.
Resumen (hasta ahora)
¡Espero que sigas conmigo! La razón por la que comenzamos en este camino es que el rendimiento de inserción masiva varía enormemente con SQLite, y no siempre es obvio qué cambios deben hacerse para acelerar nuestra operación. Usando el mismo compilador (y opciones de compilador), la misma versión de SQLite y los mismos datos hemos optimizado nuestro código y nuestro uso de SQLite para pasar del peor de los casos de 85 inserciones por segundo a más de 96,000 inserciones por segundo.
CREAR ÍNDICE luego INSERTAR vs INSERTAR luego CREAR ÍNDICE
Antes de comenzar a medir el SELECT
rendimiento, sabemos que crearemos índices. Se ha sugerido en una de las respuestas a continuación que cuando se realizan inserciones masivas, es más rápido crear el índice después de que se hayan insertado los datos (en lugar de crear el índice primero y luego insertar los datos). Intentemos:
Crear índice y luego insertar datos
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
Importó 864913 registros en 18.13 segundos
Insertar datos y luego crear índice
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
Importó 864913 registros en 13.66 segundos
Como se esperaba, las inserciones masivas son más lentas si se indexa una columna, pero sí hace una diferencia si el índice se crea después de insertar los datos. Nuestra línea de base sin índice es de 96,000 insertos por segundo. Crear el índice primero y luego insertar datos nos da 47,700 inserciones por segundo, mientras que insertar los datos primero y luego crear el índice nos da 63,300 inserciones por segundo.
Con gusto tomaría sugerencias para otros escenarios para probar ... Y pronto estaré compilando datos similares para consultas SELECT.
fuente
sqlite3_clear_bindings(stmt);
? Establece los enlaces cada vez que debería ser suficiente: antes de llamar a sqlite3_step () por primera vez o inmediatamente después de sqlite3_reset (), la aplicación puede invocar una de las interfaces sqlite3_bind () para adjuntar valores a los parámetros. Cada llamada a sqlite3_bind () anula los enlaces anteriores en el mismo parámetro (ver: sqlite.org/cintro.html ). No hay nada en los documentos para esa función que indique que debe llamarlo.feof()
para controlar la terminación de su ciclo de entrada. Use el resultado devuelto porfgets()
. stackoverflow.com/a/15485689/827263Respuestas:
Varios consejos:
pragma journal_mode
). HayNORMAL
, y luego estáOFF
, lo que puede aumentar significativamente la velocidad de inserción si no está demasiado preocupado por la posibilidad de que la base de datos se corrompa si el sistema operativo falla. Si su aplicación falla, los datos deberían estar bien. Tenga en cuenta que en las versiones más recientes, laOFF/MEMORY
configuración no es segura para bloqueos de nivel de aplicación.PRAGMA page_size
). Tener tamaños de página más grandes puede hacer que las lecturas y escrituras sean un poco más rápidas a medida que las páginas más grandes se guardan en la memoria. Tenga en cuenta que se usará más memoria para su base de datos.CREATE INDEX
después de hacer todas sus inserciones. Esto es significativamente más rápido que crear el índice y luego hacer sus inserciones.INTEGER PRIMARY KEY
sea posible, lo que reemplazará la columna de número de fila único implícito en la tabla.!feof(file)
!También he hecho preguntas similares aquí y aquí .
fuente
Intente usar en
SQLITE_STATIC
lugar deSQLITE_TRANSIENT
para esos insertos.SQLITE_TRANSIENT
hará que SQLite copie los datos de la cadena antes de regresar.SQLITE_STATIC
le dice que la dirección de memoria que le proporcionó será válida hasta que se haya realizado la consulta (que en este bucle siempre es el caso). Esto le ahorrará varias operaciones de asignación, copia y desasignación por ciclo. Posiblemente una gran mejora.fuente
Evitar
sqlite3_clear_bindings(stmt)
.El código en la prueba establece los enlaces cada vez a través de los cuales debería ser suficiente.
La introducción de C API de los documentos de SQLite dice:
No hay nada en los documentos para
sqlite3_clear_bindings
decir que debe llamarlo, además de simplemente establecer los enlaces.Más detalles: Evite_sqlite3_clear_bindings ()
fuente
En insertos a granel
Inspirado en esta publicación y en la pregunta de desbordamiento de pila que me llevó aquí: ¿es posible insertar varias filas a la vez en una base de datos SQLite? - Publiqué mi primer repositorio de Git :
https://github.com/rdpoor/CreateOrUpdateque carga masivamente una matriz de ActiveRecords en bases de datos MySQL , SQLite o PostgreSQL . Incluye una opción para ignorar los registros existentes, sobrescribirlos o generar un error. Mis puntos de referencia rudimentarios muestran una mejora de velocidad de 10 veces en comparación con las escrituras secuenciales: YMMV.
Lo estoy usando en el código de producción donde frecuentemente necesito importar grandes conjuntos de datos, y estoy bastante contento con él.
fuente
Las importaciones masivas parecen funcionar mejor si puede fragmentar sus instrucciones INSERT / UPDATE . Un valor de aproximadamente 10,000 me ha funcionado bien en una mesa con solo unas pocas filas, YMMV ...
fuente
Si solo le importa leer, una versión algo más rápida (pero podría leer datos obsoletos) es leer desde múltiples conexiones desde múltiples hilos (conexión por hilo).
Primero encuentre los artículos, en la tabla:
luego lea en las páginas (LIMIT / OFFSET):
donde y se calculan por subproceso, así:
para cada hilo:
Para nuestra pequeña base de datos (200mb) esto aceleró un 50-75% (3.8.0.2 64 bits en Windows 7). Nuestras tablas están muy no normalizadas (1000-1500 columnas, aproximadamente 100,000 o más filas).
Demasiados o muy pocos subprocesos no lo harán, debe realizar una evaluación comparativa y perfilarse.
También para nosotros, SHAREDCACHE hizo que el rendimiento fuera más lento, así que puse manualmente PRIVATECACHE (porque fue habilitado globalmente para nosotros)
fuente
No pude obtener ninguna ganancia de las transacciones hasta que aumenté cache_size a un valor más alto, es decir
PRAGMA cache_size=10000;
fuente
cache_size
establece el número de páginas en caché , no el tamaño total de RAM. Con el tamaño de página predeterminado de 4 KB, esta configuración contendrá hasta 40 MB de datos por archivo abierto (o por proceso, si se ejecuta con caché compartida ).Después de leer este tutorial, intenté implementarlo en mi programa.
Tengo 4-5 archivos que contienen direcciones. Cada archivo tiene aproximadamente 30 millones de registros. Estoy usando la misma configuración que sugiere, pero mi número de INSERTOS por segundo es muy bajo (~ 10,000 registros por segundo).
Aquí es donde falla su sugerencia. Utiliza una sola transacción para todos los registros y una sola inserción sin errores / fallas. Digamos que está dividiendo cada registro en múltiples inserciones en diferentes tablas. ¿Qué pasa si se rompe el récord?
El comando ON CONFLICT no se aplica, porque si tiene 10 elementos en un registro y necesita que cada elemento se inserte en una tabla diferente, si el elemento 5 obtiene un error CONSTRAINT, entonces todas las 4 inserciones anteriores también deben ir.
Así que aquí es donde viene la reversión. El único problema con la reversión es que pierde todas sus inserciones y comienza desde arriba. ¿Cómo puedes resolver esto?
Mi solución fue usar múltiples transacciones. Comienzo y finalizo una transacción cada 10.000 registros (no pregunte por qué ese número, fue el más rápido que probé). Creé una matriz de 10.000 e inserto los registros exitosos allí. Cuando se produce el error, hago una reversión, comienzo una transacción, inserto los registros de mi matriz, me comprometo y luego comienzo una nueva transacción después del registro roto.
Esta solución me ayudó a evitar los problemas que tengo al tratar con archivos que contienen registros incorrectos / duplicados (tenía casi un 4% de registros incorrectos).
El algoritmo que creé me ayudó a reducir mi proceso en 2 horas. Proceso de carga final del archivo 1 hora y 30 metros, que todavía es lento pero no se compara con las 4 horas que tomó inicialmente. Logré acelerar las inserciones de 10.000 / s a ~ 14.000 / s
Si alguien tiene alguna otra idea sobre cómo acelerarlo, estoy abierto a sugerencias.
ACTUALIZACIÓN :
Además de mi respuesta anterior, debe tener en cuenta que las inserciones por segundo también dependen del disco duro que esté utilizando. Lo probé en 3 PC diferentes con diferentes discos duros y obtuve grandes diferencias en los tiempos. PC1 (1 hora 30 m), PC2 (6 horas) PC3 (14 horas), así que comencé a preguntarme por qué sería eso.
Después de dos semanas de investigación y verificación de múltiples recursos: Disco Duro, Ram, Caché, descubrí que algunas configuraciones en su disco duro pueden afectar la velocidad de E / S. Al hacer clic en las propiedades de su unidad de salida deseada, puede ver dos opciones en la pestaña general. Opt1: comprima esta unidad, Opt2: permita que los archivos de esta unidad tengan contenido indexado.
Al deshabilitar estas dos opciones, las 3 PC ahora tardan aproximadamente el mismo tiempo en finalizar (1 hora y 20 a 40 minutos). Si encuentra inserciones lentas, compruebe si su disco duro está configurado con estas opciones. Le ahorrará mucho tiempo y dolores de cabeza al tratar de encontrar la solución.
fuente
La respuesta a su pregunta es que el nuevo SQLite 3 ha mejorado el rendimiento, úselo.
Esta respuesta ¿Por qué la inserción SQLAlchemy con sqlite es 25 veces más lenta que usar sqlite3 directamente? por SqlAlchemy Orm Author tiene 100k inserciones en 0.5 segundos, y he visto resultados similares con python-sqlite y SqlAlchemy. Lo que me lleva a creer que el rendimiento ha mejorado con SQLite 3.
fuente
Use ContentProvider para insertar los datos masivos en db. El siguiente método se utiliza para insertar datos masivos en la base de datos. Esto debería mejorar el rendimiento INSERT por segundo de SQLite.
Llame al método bulkInsert:
Enlace: https://www.vogella.com/tutorials/AndroidSQLite/article.html consulte Uso de la sección ContentProvider para obtener más detalles
fuente