2012-01-24 14 views
25

Tengo un problema de rendimiento en SQLite con SELECT COUNT (*) en tablas grandes.SQLite: COUNT lento en tablas grandes

Como todavía no recibí una respuesta utilizable e hice algunas pruebas adicionales, edité mi pregunta para incorporar mis nuevos hallazgos.

tengo 2 tablas:

CREATE TABLE Table1 (
Key INTEGER NOT NULL, 
... several other fields ..., 
Status CHAR(1) NOT NULL, 
Selection VARCHAR NULL, 
CONSTRAINT PK_Table1 PRIMARY KEY (Key ASC)) 

CREATE Table2 (
Key INTEGER NOT NULL, 
Key2 INTEGER NOT NULL, 
... a few other fields ..., 
CONSTRAINT PK_Table2 PRIMARY KEY (Key ASC, Key2 ASC)) 

Tabla 1 tiene alrededor de 8 millones de discos y Tabla2 tiene alrededor de 51 millones de registros, y la DatabaseFile es de más de 5 GB.

Tabla 1 tiene 2 índices más:

CREATE INDEX IDX_Table1_Status ON Table1 (Status ASC, Key ASC) 
CREATE INDEX IDX_Table1_Selection ON Table1 (Selection ASC, Key ASC) 

campo "Estado" se requiere, pero tiene sólo 6 valores distintos, "Selección" no es necesaria y tiene sólo alrededor de 1,5 millones de valores diferentes de cero y sólo alrededor 600k valores distintos

Hice algunas pruebas en ambas tablas, puede ver los tiempos a continuación, y agregué el "plan de consulta de explicación" para cada solicitud (QP). Puse el archivo de la base de datos en una memoria USB para poder eliminarlo después de cada prueba y obtener resultados confiables sin interferencia de la memoria caché del disco. Algunas solicitudes son más rápidas en USB (supongo que debido a la falta de tiempo de búsqueda), pero algunas son más lentas (escaneos de tabla).

SELECT COUNT(*) FROM Table1 
    Time: 105 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~1000000 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 153 sec 
    QP: SCAN TABLE Table1 (~1000000 rows) 
SELECT * FROM Table1 WHERE Key = 5123456 
    Time: 5 ms 
    QP: SEARCH TABLE Table1 USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 16 sec 
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows) 
SELECT * FROM Table1 WHERE Selection = 'SomeValue' AND Key > 5123456 LIMIT 1 
    Time: 9 ms 
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Selection (Selection=?) (~3 rows) 

Como se puede ver los recuentos son muy lentos, pero son rápidos selecciona normales (excepto para el segundo uno, que tomó 16 segundos).

Lo mismo vale para la Tabla2:

SELECT COUNT(*) FROM Table2 
    Time: 528 sec 
    QP: SCAN TABLE Table2 USING COVERING INDEX sqlite_autoindex_Table2_1(~1000000 rows) 
SELECT COUNT(Key) FROM Table2 
    Time: 249 sec 
    QP: SCAN TABLE Table2 (~1000000 rows) 
SELECT * FROM Table2 WHERE Key = 5123456 AND Key2 = 0 
    Time: 7 ms 
    QP: SEARCH TABLE Table2 USING INDEX sqlite_autoindex_Table2_1 (Key=? AND Key2=?) (~1 rows) 

¿Por qué no usar SQLite el índice creado automáticamente en la clave principal en la tabla 1? ¿Y por qué, cuando usa el índice automático en la Tabla 2, todavía toma mucho tiempo?

Creé las mismas tablas con el mismo contenido y los mismos índices en SQL Server 2008 R2 y allí los recuentos son casi instantáneos.

Uno de los siguientes comentarios sugería ejecutar ANALYZE en la base de datos. Lo hice y me llevó 11 minutos completarlo. Después de eso, me encontré con algunas de las pruebas de nuevo:

SELECT COUNT(*) FROM Table1 
    Time: 104 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~7848023 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 151 sec 
    QP: SCAN TABLE Table1 (~7848023 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 5 ms 
    QP: SEARCH TABLE Table1 USING INTEGER PRIMARY KEY (rowid>?) (~196200 rows) 
SELECT COUNT(*) FROM Table2 
    Time: 529 sec 
    QP: SCAN TABLE Table2 USING COVERING INDEX sqlite_autoindex_Table2_1(~51152542 rows) 
SELECT COUNT(Key) FROM Table2 
    Time: 249 sec 
    QP: SCAN TABLE Table2 (~51152542 rows) 

Como se puede ver, las consultas tomaron al mismo tiempo (excepto el plan de consulta que ahora se muestra el número real de filas), sólo el más lento de selección es ahora también rápido.

A continuación, creo un índice dan adicional en el campo Clave de la Tabla1, que debe corresponder al índice automático. Hice esto en la base de datos original, sin los datos de ANALIZAR. Tomó más de 23 minutos crear este índice (recuerde, esto está en una memoria USB).

CREATE INDEX IDX_Table1_Key ON Table1 (Key ASC) 

Entonces me encontré de nuevo las pruebas:

SELECT COUNT(*) FROM Table1 
    Time: 4 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Key(~1000000 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 167 sec 
    QP: SCAN TABLE Table2 (~1000000 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 17 sec 
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows) 

Como se puede ver, el índice ayudó con la cuenta (*), pero no con el recuento (Key).

Finaly, que crea la tabla utilizando una restricción de columna en lugar de una restricción de tabla:

CREATE TABLE Table1 (
Key INTEGER PRIMARY KEY ASC NOT NULL, 
... several other fields ..., 
Status CHAR(1) NOT NULL, 
Selection VARCHAR NULL) 

Entonces me encontré de nuevo las pruebas:

SELECT COUNT(*) FROM Table1 
    Time: 6 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~1000000 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 28 sec 
    QP: SCAN TABLE Table1 (~1000000 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 10 sec 
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows) 

Aunque los planes de consulta son los mismos, la los tiempos son mucho mejores Por qué es esto ?

El problema es que ALTER TABLE no permite convertir una tabla existente y tengo muchas bases de datos existentes que no puedo convertir a esta forma. Además, usar un contraint de columna en lugar de restricción de tabla no funcionará para Table2.

¿Alguien tiene alguna idea de lo que estoy haciendo mal y cómo resolver este problema?

Utilicé System.Data.SQLite versión 1.0.74.0 para crear las tablas y para ejecutar las pruebas usé SQLiteSpy 1.9.1.

Gracias,

Marc

+4

Si tiene problemas de rendimiento con SQLite, la solución suele ser pasar a un servidor de bases de datos más grande (recomiendo Postgres en lugar de MS SQL). – Borealid

+0

No tengo otros problemas de rendimiento, todas las otras selecciones son rápidas (y uso los índices correctos), las inserciones y las actualizaciones son rápidas, solo el recuento me molesta. – Marc

+0

Lo que es realmente extraño, porque (para DB2, al menos) la mayoría de los RDBMS probablemente usan información efectivamente almacenada en caché: si solicita el recuento de _todas las filas (o está restringido por algo en un índice), generalmente puede leer esa información el índice en sí: el índice conoce el número de entradas. Es doblemente extraño en el sentido de que dices que todos los demás 'SELECT's son rápidos; ¡necesitan saber el recuento de registros para poder optimizarlos correctamente! A menos que ocurra algo extraño, y usted está bloqueando la tabla (nivel de transacción de lectura repetible, o algo así?) ... –

Respuesta

1

Ésta no pueden ayudar mucho, pero se puede ejecutar el comando ANALYZE para reconstruir estadísticas acerca de su base de datos. Intente ejecutar "ANALYZE;" para reconstruir las estadísticas sobre la base de datos completa, luego vuelva a ejecutar su consulta y vea si es más rápida.

+0

Ejecuté el comando ANALIZAR, tardó mucho tiempo en completarse, pero no cambió el resultado, el recuento aún era lento. – Marc

+0

'ANALYZE' corrigió el problema para mi DB al hacer una' LEFT JOIN' –

0

En lo que respecta a la restricción de columna, SQLite correlaciona las columnas declaradas INTEGER PRIMARY KEY con la Id. De fila interna (que a su vez admite una serie de optimizaciones internas). Teóricamente, podría hacer lo mismo para una restricción de clave primaria declarada por separado, pero parece no hacerlo en la práctica, al menos con la versión de SQLite en uso. (System.Data.SQLite 1.0.74.0 corresponde al core SQLite 3.7.7.1. Es posible que desee volver a verificar sus cifras con 1.0.79.0; no debería necesitar cambiar su base de datos para hacer eso, solo la biblioteca).

+0

Probé ambas consultas (count (*) y count (key)) con la última versión de System.Data.SQlite (1.0.79.0) y yo obtuvo los mismos resultados que antes. – Marc

+0

Como tuve que escribir un pequeño programa de prueba (porque SQLIteSpy usa una versión anterior de SQLite, 3.7.8), lo intenté tanto en 32 como en 64bits, pero obtuve los mismos resultados para ambos. – Marc

0

El resultado de las consultas rápidas comienza con el texto "QP: SEARCH". Mientras que los de las consultas lentas comienzan con el texto "QP: SCAN", lo que sugiere que sqlite está realizando un escaneo de toda la tabla para generar el recuento.

Google para el "conteo de escaneos de tablas sqlite" encuentra the following, lo que sugiere que utilizar un escaneo completo de tablas para recuperar un conteo es simplemente la forma en que funciona sqlite, y por lo tanto probablemente sea inevitable.

Como solución alternativa, y dado que el estado tiene solo ocho valores, me pregunté si podría obtener un conteo rápidamente usando una consulta como la siguiente?

seleccione 1, donde el estado = 1 unión seleccionar 1, donde el estado = 2 ...

luego contar las filas en el resultado. Esto es claramente feo, pero podría funcionar si persuade a sqlite para que ejecute la consulta como una búsqueda en lugar de como un escaneo. La idea de devolver "1" cada vez es evitar la sobrecarga de devolver datos reales.

+0

Ya encontré esta [publicación] (http://www.mail-archive.com/[email protected]/msg10279.html) del autor de SQLite, así que ya abandoné la esperanza, ya que la adición de desencadenantes ser demasiado penalizador en inserciones y eliminaciones. Pero probé tu sugerencia. Primero intenté 'SELECT COUNT (*) FROM table1 donde Status en (1,2,3,4,5,6)', se ejecutó en 86 segundos (un poco más rápido), QP: 'SEARCH TABLE Table1 USING COVERING INDEX IDX_Table1_Status (Estado =?) (~ 60 filas); EXECUTE LIST SUBQUERY 1'. Mejor pero no lo suficientemente bueno. – Marc

+0

Probé la sugerencia de su unión. 'SELECT COUNT (*) FROM (SELECCIONA 1 FROM Table1 WHERE Estado = 1 UNION SELECT 1 FROM Table1 WHERE Estado = 2 UNION ...)' devolvió 1, lo mismo para SUM (*), supongo que debido a las características a Unión. 'SELECT COUNT (*) FROM (SELECCIONA 1 FROM Table1 WHERE Estado = 1 UNION SELECT 2 FROM Table1 DONDE Status = 2 UNION ...)' devolvió 6. Entonces finalmente intenté 'SELECT COUNT (*) FROM (SELECCIONE la tecla FROM Table1 DÓNDE Estado = 1 UNIÓN SELECCIONA tecla DESDE Tabla1 DONDE Estado = 2 UNIÓN ...) 'que devolvió el resultado correcto, pero muy lento (116 segundos). Gracias por la sugerencia, sin embargo. – Marc

+0

Y mi primer intento ('SELECT COUNT (*) FROM table1 donde Status en (1,2,3,4,5,6)') que era un poco mejor, no funcionaría para mi otra tabla (Table2). – Marc

0

Aquí hay una posible solución para mejorar el rendimiento de la consulta. Desde el contexto, parece que su consulta tarda aproximadamente un minuto y medio en ejecutarse.

Suponiendo que tiene una columna date_created (o puede agregar una), ejecute una consulta en segundo plano todos los días a medianoche (digamos a las 00:05 a.m.) y persista el valor en algún lugar junto con la última fecha actualizada que se calculó (I ' Volveré a eso en un momento).

Luego, al ejecutar contra su columna date_created (con un índice), puede evitar un escaneo completo de la tabla haciendo una consulta como SELECT COUNT (*) FROM TABLE WHERE date_updated> "[TODAY] 00:00:05".

Agregue el valor de conteo de esa consulta a su valor persistente, y tiene un recuento razonablemente rápido que generalmente es preciso.

El único inconveniente es que de 12:05 a.m. a 12:07 a.m. (el tiempo durante el cual se ejecuta la consulta de recuento total) tiene una condición de carrera que puede verificar el último valor actualizado de su conteo de exploración de tabla completa(). Si es> 24 horas, su consulta de recuento incremental necesita extraer el recuento de un día completo más el tiempo transcurrido en la actualidad. Si es < las 24 horas, su consulta de recuento incremental necesita obtener un recuento parcial de días (solo el tiempo transcurrido hoy).

+0

Perdón por la respuesta tardía, estuve enfermo unos días. SQLite no es un servidor SQL, es un motor de base de datos independiente. Por lo tanto, no puede programar tareas, a menos que use el programador de Windows (u otro sistema operativo). De todos modos, solo se permite una conexión en ese momento para una base de datos, por lo que mientras se ejecuta el recuento programado, la base de datos se bloqueará para todos los demás accesos. No es una solución que funcione para mí. – Marc

18

De http://old.nabble.com/count(*)-slow-td869876.html

SQLite siempre hace un escaneo completo de tabla de recuento (*). Es
no mantiene meta información en las tablas para acelerar este proceso
.

El no mantener la meta información es un diseño deliberado
decisión. Si cada tabla almacenaba un recuento (o mejor, cada nodo
del btree almacenado un recuento), entonces tendría que ocurrir mucha más actualización
en cada INSERTAR o ELIMINAR. Este
ralentizará INSERTAR y ELIMINAR, incluso en el caso
donde la velocidad del conteo (*) no es importante.

Si realmente necesita un COUNT rápido, entonces usted puede crear
un disparador en INSERT y DELETE que actualiza un
cuenta corriente en una tabla separada y luego consulta que separar
tabla para encontrar el último recuento.

Por supuesto, no vale la pena mantener un recuento de filas completo si
necesita COUNT depende de las cláusulas WHERE (es decir, donde campo1> 0 y campo2 < 1000000000).

+1

Lo siento por la respuesta tardía, he estado enfermo unos días. Ya publiqué un enlace a la misma publicación en uno de mis comentarios. Creo que agregar activadores sería demasiado penalizador en las inserciones masivas y eliminaciones. Creo que lo mejor sería hacer un seguimiento del recuento de la tabla al final de cada inserción y/o eliminar la transacción, por lo que el contador solo se actualiza una vez y no en cada inserción/eliminación. – Marc

+0

Además, 'COUNT (1)' debe ser más rápido que 'COUNT (*)' e incluso 'COUNT (" id ")'. –

+0

@AlixAxel en todas mis pruebas 'COUNT()' y 'COUNT (*)' son las más rápidas, 'COUNT (1)' toma el doble y 'COUNT (ROWID)' toma el triple de tiempo. – springy76

0

Tuve el mismo problema, en mi situación el comando VACUUM me ayudó. Después de su ejecución en la base de datos, la velocidad de COUNT (*) aumentó cerca de 100 veces. Sin embargo, el comando en sí necesita algunos minutos en mi base de datos (20 millones de registros). Resolví este problema ejecutando VACUUM cuando mi software sale después de la destrucción de la ventana principal, por lo que el retraso no causa problemas al usuario.

+3

VACUUM obligará a leer y escribir todo el archivo, por lo que llenará el contenido del disco en la memoria caché. Por eso es más rápido. Si reinicia su PC, lo encontrará lento de nuevo, supongo. –

18

Si no has DELETE d cualquier registro, haciendo:

SELECT MAX(_ROWID_) FROM "table" LIMIT 1; 

evitará el escaneo completo de tabla. Tenga en cuenta que _ROWID_ is a SQLite identifier.

+0

Debe ser la mejor respuesta. Vuelve al instante y da una buena aproximación (por lo general todo lo que quiere) – easytiger

+1

Confirmando, esto devuelve un valor en unos pocos ms para mi DB con 115 millones registros en una tabla. Hacer un COUNT completo (*) en realidad nunca compl eted (dejé de esperar después de 4 horas). –

+1

Esto es bueno, pero tenga en cuenta lo que Alix ya ha dicho, incluso si ha borrado un solo registro en esta tabla, obtendrá resultados incorrectos (ya que _ROWID_ es un ID de registro cada vez mayor, y 'eliminar' lo haría no causa que _ROWID_ disminuya). – strangetimes

2

¡No cuente las estrellas, cuente los registros!O en otro idioma, nunca emita

SELECT COUNT (*) FROM tablename;

uso

SELECT COUNT (ROWID) de nombre de tabla;

Llame a EXPLAIN QUERY PLAN para que ambos puedan ver la diferencia. Asegúrese de tener un índice en su lugar que contenga todas las columnas mencionadas en la cláusula WHERE.

+0

No parecía hacer una diferencia para mí. – Fidel

+0

@Fidel Depende del modelo y la configuración de su base de datos. En mi experimento, SQLite hizo un escaneo completo para búsqueda de asterisco en lugar de búsqueda de índice cuando se usa con ROWID para el recuento completo de la tabla. Tal vez también he pasado por alto algo más, no pretendo ser perfecto. Sin embargo, ¡todavía recomiendo usar ** explicar el plan de consulta **! Simplemente obligue al DB a utilizar el índice en PK en lugar de un análisis completo y tenga cuidado con el efecto de almacenamiento en caché del sistema operativo como los comentarios de Arnaud a continuación. ¡Que tus consultas siempre sean rápidas! – Thinkeye

+1

Esto está mal en la mayoría de los casos. Al menos en todas mis tablas, la búsqueda por rowid lleva 22 segundos, frente a una cuenta de 4 segundos – easytiger

Cuestiones relacionadas