¿Mejora el rendimiento INSERT por segundo de SQLite?

Optimizar SQLite es complicado. ¡El rendimiento de inserción en bloque de una aplicación C puede variar de 85 inserciones por segundo a más de 96,000 inserciones por segundo!

Antecedentes: estamos utilizando 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 la aplicación se inicializa. SQLite es ideal para esta situación porque es rápido, no requiere configuración especializada y la base de datos se almacena en el disco como un único archivo.

Justificación: Inicialmente, me decepcionó 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 se usa la API. No fue una cuestión trivial averiguar cuáles eran todas las opciones y técnicas, así que pensé que era prudente crear esta entrada en la wiki de la comunidad 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 de consejos de rendimiento en el sentido general (es decir, “¡Usa una transacción!” ), Pensé que era mejor escribir un código C y realmente 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 calendario de tránsito completo de la ciudad de Toronto
  • Mi máquina de prueba es un P4 de 3.60 GHz con Windows XP.
  • El código se comstack con Visual C ++ 2005 como “Versión” con “Optimización completa” (/ Ox) y Favor de código rápido (/ Ot).
  • Estoy usando el SQLite “Amalgamation”, comstackdo 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 con la última versión (por favor, deje un comentario si piensa lo contrario).

Vamos a escribir un código!

El Código: un simple progtwig 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 de “línea de base” del código, se crea la base de datos, pero en realidad no insertamos 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  #include  #include  #include  #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á no realiza en realidad ninguna operación de base de datos, pero nos dará una idea de qué tan rápido son las operaciones de E / S de archivo C en bruto y de procesamiento de cadenas.

Se importaron 864913 registros en 0.94 segundos

¡Estupendo! Podemos hacer 920,000 inserciones por segundo, siempre que no hagamos insertos 🙂


El “peor escenario de caso”

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 comstackrá en el código VDBE para cada inserción y cada inserción ocurrirá en su propia transacción. ¿Qué tan lento?

Se importaron 864913 registros en 9933.61 segundos

¡Ay! 2 horas y 45 minutos! Eso es solo 85 insertos por segundo.

Usando una transacción

De forma predeterminada, SQLite evaluará cada instrucción INSERT / UPDATE dentro de una transacción única. Si realiza una gran cantidad de insertos, 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); 

Se importaron 864913 registros en 38.03 segundos

Eso es mejor. Simplemente envolver todos nuestros insertos en una sola transacción mejoró nuestro rendimiento a 23,000 inserciones por segundo.

Usando una statement preparada

Usar una transacción fue una gran mejora, pero recomstackr la statement SQL para cada inserción no tiene sentido si usamos el mismo SQL repetidamente. sqlite3_prepare_v2 para comstackr nuestra statement SQL una vez y luego unir nuestros parámetros a esa statement 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; 

Se importaron 864913 registros en 16.27 segundos

¡Bonito! Hay un poco más de código (no olvides llamar a sqlite3_clear_bindings y sqlite3_reset ), pero hemos más que duplicado nuestro rendimiento a 53,000 inserciones por segundo.

PRAGMA sincrónico = DESACTIVADO

De forma predeterminada, SQLite se pausará después de emitir un comando de escritura de nivel del sistema operativo. Esto garantiza que los datos se escriben en el disco. Al configurar synchronous = OFF , estamos instruyendo a SQLite que simplemente transfiera los datos al sistema operativo para que los escriba y luego continúe. Existe la posibilidad de que el archivo de la base de datos se corrompa si la computadora sufre un locking catastrófico (o falla de energía) antes de que los datos se escriban en el plato:

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

Se importaron 864913 registros en 12.41 segundos

Las mejoras son ahora más pequeñas, pero tenemos hasta 69.600 inserciones por segundo.

PRAGMA journal_mode = MEMORIA

Considere almacenar el diario de reversión en memoria evaluando PRAGMA journal_mode = MEMORY . Su transacción será más rápida, pero si pierde energía o el progtwig 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); 

Se importaron 864913 registros en 13.50 segundos

Un poco más lento que la optimización anterior en 64,000 inserciones por segundo.

PRAGMA synchronous = OFF y PRAGMA journal_mode = MEMORIA

Combinemos las dos optimizaciones anteriores. Es un poco más arriesgado (en caso de un locking), pero solo estamos importando datos (no ejecutando 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); 

Se importaron 864913 registros en 12.00 segundos

¡Fantástico! Podemos hacer 72,000 inserciones por segundo.

Usando una base de datos en memoria

Solo por diversión, construyamos sobre todas las optimizaciones anteriores y redefiniremos el nombre de archivo de la base de datos para que trabajemos completamente en la RAM:

 #define DATABASE ":memory:" 

Se importaron 864913 registros en 10.94 segundos

No es super práctico almacenar nuestra base de datos en RAM, pero es impresionante que podamos realizar 79,000 inserciones por segundo.

Refactorización del código C

Aunque no específicamente una mejora de SQLite, no me gustan las operaciones de asignación de char* extra en el ciclo while. Reorganicemos rápidamente ese código para pasar la salida de strtok() directamente a sqlite3_bind_text() , y dejemos que el comstackdor intente acelerar nuestras cosas:

 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: volvemos a utilizar un archivo de base de datos real. Las bases de datos en memoria son rápidas, pero no necesariamente prácticas

Se importaron 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 . A medida que empecemos a modificar otras variables (es decir, el tamaño de la página, la creación del índice, etc.), este será nuestro punto de referencia.


Resumen (hasta ahora)

¡Espero que todavía estés conmigo! La razón por la que comenzamos por este camino es que el rendimiento de la inserción masiva varía mucho con SQLite, y no siempre es obvio qué cambios se deben realizar para acelerar nuestra operación. Usando el mismo comstackdor (y las mismas opciones de comstackción), 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 a 85 inserciones por segundo a más de 96,000 inserciones por segundo.


CREAR ÍNDICE luego INSERTAR versus INSERTAR luego CREAR ÍNDICE

Antes de comenzar a medir el rendimiento de SELECT , sabemos que vamos a crear í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 insertar los datos (en lugar de crear primero el índice 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); ... 

Se importaron 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); 

Se importaron 864913 registros en 13.66 segundos

Como era de esperar, las inserciones en bloque 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 base sin índice es de 96,000 inserciones 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é sugerencias para otros escenarios para intentar … y pronto recostackré datos similares para consultas SELECT.

Varios consejos:

  1. Poner insertos / actualizaciones en una transacción.
  2. Para versiones anteriores de SQLite: considere un modo de diario menos paranoico ( pragma journal_mode ). Hay NORMAL , y luego está OFF , lo que puede boost 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 se cuelga. Si su aplicación falla, la información debería estar bien. Tenga en cuenta que en las versiones más nuevas, las configuraciones OFF/MEMORY no son seguras para lockings de nivel de aplicación.
  3. Jugar con tamaños de página hace la diferencia también ( PRAGMA page_size ). Tener tamaños de página más grandes puede hacer que las lecturas y las escrituras sean más rápidas ya 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.
  4. Si tiene índices, considere llamar a 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.
  5. Debe tener mucho cuidado si tiene acceso concurrente a SQLite, ya que toda la base de datos está bloqueada cuando se realizan las escrituras, y aunque es posible tener varios lectores, las escrituras serán bloqueadas. Esto se ha mejorado un poco con la adición de un WAL en las nuevas versiones de SQLite.
  6. Aproveche el ahorro de espacio … las bases de datos más pequeñas son más rápidas. Por ejemplo, si tiene pares de valores clave, intente hacer que la clave sea INTEGER PRIMARY KEY si es posible, lo que reemplazará la columna de número de fila única implicada en la tabla.
  7. Si está utilizando varios subprocesos, puede intentar usar el caché de página compartido , que permitirá que las páginas cargadas se compartan entre subprocesos, lo que puede evitar costosas llamadas de E / S.
  8. No use !feof(file) !

También hice preguntas similares aquí y aquí .

Intente utilizar SQLITE_STATIC lugar de SQLITE_TRANSIENT para esas inserciones.

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 dio será válida hasta que se haya realizado la consulta (que en este ciclo siempre es el caso). Esto le ahorrará varias operaciones de asignación, copia y desasignación por ciclo. Posiblemente una gran mejora.

Evite sqlite3_clear_bindings (stmt);

El código en la prueba establece los enlaces cada vez que debería ser suficiente.

La introducción de la API de C desde los documentos de SQLite dice

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 las vinculaciones 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 además de simplemente configurar los enlaces.

Más detalles: http://www.hoogli.com/blogs/micro/index.html#Avoid_sqlite3_clear_bindings ()

En inserciones a granel

Inspirado por esta publicación y por la pregunta de Desbordamiento de stack que me condujo aquí: ¿es posible insertar varias filas a la vez en una base de datos SQLite? – He publicado mi primer repository de Git :

https://github.com/rdpoor/CreateOrUpdate

que a granel carga 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 10 veces la velocidad en comparación con las escrituras secuenciales – YMMV.

Lo estoy usando en código de producción donde con frecuencia necesito importar grandes conjuntos de datos, y estoy muy contento con él.

Las importaciones masivas parecen funcionar mejor si puede dividir sus instrucciones INSERT / UPDATE . Un valor de 10.000 o más me ha funcionado en una tabla con solo unas pocas filas, YMMV …

Si solo le interesa leer, la versión algo más rápida (pero puede leer datos obsoletos) es leer desde múltiples conexiones de múltiples hilos (conexión por hilo).

Primero encuentra los artículos, en la tabla:

  SELECT COUNT(*) FROM table 

luego lea en las páginas (LIMIT / OFFSET)

  SELECT * FROM table ORDER BY _ROWID_ LIMIT  OFFSET  

donde y se calculan por subproceso, como este:

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

para cada hilo:

 int offset = thread_index * limit 

Para nuestro db pequeño (200 mb), esto acertó un 50-75% de aceleración (3.8.0.2 de 64 bits en Windows 7). Nuestras tablas son muy no normalizadas (1000-1500 columnas, aproximadamente 100,000 o más filas).

Demasiados o muy pocos subprocesos no lo harán, necesita comparar y perfilarse.

También para nosotros, SHAREDCACHE hizo el rendimiento más lento, así que puse PRIVATECACHE manualmente (porque fue habilitado globalmente para nosotros)

No pude obtener ninguna ganancia de las transacciones hasta que levanté cache_size a un valor más alto, es decir, PRAGMA cache_size=10000;

Después de leer este tutorial, traté de implementarlo en mi progtwig.

Tengo 4-5 archivos que contienen direcciones. Cada archivo tiene aproximadamente 30 millones de registros. Estoy usando la misma configuración que estás sugiriendo, pero mi número de INSERT por segundo es muy bajo (~ 10.000 registros por segundo).

Aquí es donde su sugerencia falla. Utiliza una única transacción para todos los registros y una sola inserción sin errores / fallas. Digamos que está dividiendo cada registro en varias inserciones en diferentes tablas. ¿Qué pasa si el registro está roto?

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 los 4 insertos anteriores también deben ir.

Entonces aquí es donde viene el retroceso. El único problema con la reversión es que pierdes todas tus inserciones y comienzas desde arriba. ¿Cómo puedes resolver esto?

Mi solución fue usar múltiples transacciones. Comienzo y termino 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 con un tamaño de 10.000 e inserté los registros exitosos allí. Cuando se produce el error, realizo una reversión, comienzo una transacción, inserto los registros de mi matriz, confirmo y luego comienzo una nueva transacción después del registro roto.

Esta solución me ayudó a evitar los problemas que tengo cuando trato con archivos que contienen registros malos / duplicados (tenía casi un 4% de registros malos).

El algoritmo que creé me ayudó a reducir mi proceso en 2 horas. Proceso de carga final del archivo de 1 hora y 30 minutos, que sigue siendo lento pero no se compara con las 4 horas que inicialmente tomó. Logré acelerar las inserciones de 10.000 / s a ​​~ 14.000 / s

Si alguien tiene otras ideas sobre cómo acelerarlo, estoy abierto a sugerencias.

ACTUALIZAR :

Además de mi respuesta anterior, debe tener en cuenta las inserciones por segundo según el disco duro que esté utilizando. Lo probé en 3 PC diferentes con discos duros diferentes y obtuve enormes diferencias en los tiempos. PC1 (1 hora y 30 minutos), PC 2 (hora y media) PC3 (14 horas), así que comencé a preguntarme por qué sería eso.

Después de dos semanas de investigación y comprobación de múltiples recursos: Disco Duro, Ram, Caché, descubrí que algunas configuraciones en tu disco duro pueden afectar la velocidad de E / S. Al hacer clic en las propiedades en la unidad de salida deseada, puede ver dos opciones en la pestaña general. Opt1: comprime esta unidad, Opt2: permite que los archivos de esta unidad tengan contenidos indexados.

Al desactivar estas dos opciones, todas las 3 PC ahora tardan aproximadamente el mismo tiempo en finalizar (1 hora y 20 a 40 minutos). Si encuentra insertos lentos, verifique si su disco duro está configurado con estas opciones. Le ahorrará mucho tiempo y dolores de cabeza tratando de encontrar la solución

La respuesta a su pregunta es que el sqlite3 más nuevo ha mejorado el rendimiento, use eso.

Esta respuesta ¿Por qué SQLAlchemy se inserta con sqlite 25 veces más lento que con 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 pensar que el rendimiento ha mejorado con sqlite3

Hay una gran conferencia de Paul Betts sobre cómo hizo C # akavache tan rápido: https://www.youtube.com/watch?v=j7WnQhwBwqA

Tal vez puedas encontrar algunas pistas para ti. Es demasiado largo para hacer un breve resumen aquí

Desde 3.24, SQLite admite la sentencia UPSERT.

Consulte “SQL entendido por SQLite” 1 Cuando una fila no existe, se inserta de otra manera actualizada. Otros motores llaman a esto MERGE.

Use comenzar transacciones y finalizar transacciones para insertarlas en lote.