sql >> Base de Datos >  >> RDS >> SQLite

Trampas y peligros de SQLite

SQLite es una base de datos relacional popular que usted integra en su aplicación. Sin embargo, hay muchas trampas y escollos que debes evitar. Este artículo analiza varios peligros (y cómo evitarlos), como el uso de ORM, cómo recuperar espacio en disco, tener en cuenta la cantidad máxima de variables de consulta, tipos de datos de columna y cómo manejar números enteros grandes.

Introducción

SQLite es un popular sistema de base de datos relacional (DB) . Tiene un conjunto de funciones muy similar al de sus hermanos mayores, como MySQL , que son sistemas basados ​​en cliente/servidor. Sin embargo, SQLite es un incrustado base de datos . Puede incluirse en su programa como una biblioteca estática (o dinámica). Esto simplifica la implementación , porque no es necesario un proceso de servidor independiente. Las bibliotecas de enlaces y contenedores le permiten acceder a SQLite en la mayoría de los lenguajes de programación .

He estado trabajando mucho con SQLite mientras desarrollaba BSync como parte de mi tesis doctoral. Este artículo es una lista (aleatoria) de trampas y escollos que encontré durante el desarrollo . Espero que los encuentre útiles y evite cometer los mismos errores que yo cometí una vez.

Trampas y escollos

Use las bibliotecas ORM con precaución

Las bibliotecas de mapeo relacional de objetos (ORM) extraen los detalles de los motores de bases de datos concretos y su sintaxis (como declaraciones SQL específicas) en una API orientada a objetos de alto nivel. Existen muchas bibliotecas de terceros (ver Wikipedia). Las bibliotecas ORM tienen algunas ventajas:

  • Ellos ahorran tiempo durante el desarrollo , porque asignan rápidamente su código/clases a estructuras de bases de datos,
  • Son a menudo multiplataforma , es decir, permitir la sustitución de la tecnología DB concreta (por ejemplo, SQLite con MySQL),
  • Ofrecen código de ayuda para la migración de esquemas .

Sin embargo, también tienen varias desventajas graves debe tener en cuenta:

  • Hacen que el trabajo con bases de datos aparezca fácil . Sin embargo, en realidad, los motores de base de datos tienen detalles intrincados que simplemente debe conocer . Una vez que algo sale mal, p. cuando la biblioteca ORM arroja excepciones que no comprende, o cuando el rendimiento del tiempo de ejecución se degrada, el tiempo de desarrollo que ahorró al usar ORM se consumirá rápidamente por los esfuerzos necesarios para depurar el problema . Por ejemplo, si no sabe qué índices es decir, le resultará difícil solucionar los cuellos de botella de rendimiento causados ​​por el ORM, cuando no crea automáticamente todos los índices necesarios. En esencia:no hay almuerzo gratis.
  • Debido a la abstracción del proveedor de base de datos concreto, la funcionalidad específica del proveedor es difícil de acceder o no accesible en absoluto .
  • Hay algo de sobrecarga computacional en comparación con escribir y ejecutar consultas SQL directamente. Sin embargo, diría que este punto es discutible en la práctica, ya que es común que pierda rendimiento una vez que cambia a un nivel más alto de abstracción.

Al final, usar una biblioteca ORM es una cuestión de preferencia personal. Si lo hace, solo prepárese para aprender sobre las peculiaridades de las bases de datos relacionales (y las advertencias específicas del proveedor), una vez que ocurra un comportamiento inesperado o cuellos de botella en el rendimiento.

Incluir una tabla de migraciones desde el principio

Si no al usar una biblioteca ORM, deberá encargarse de la migración del esquema de la base de datos . Esto implica escribir un código de migración que altere los esquemas de sus tablas y transforme los datos almacenados de alguna manera. Le recomiendo que cree una tabla llamada "migraciones" o "versión", con una sola fila y columna, que simplemente almacene la versión del esquema, p. usando un entero monótonamente creciente. Esto permite que su función de migración detecte qué migraciones aún deben aplicarse. Cada vez que un paso de migración se completa con éxito, su código de herramientas de migración incrementa este contador a través de una UPDATE Sentencia SQL.

Columna de ID de fila creada automáticamente

Siempre que cree una tabla, SQLite creará automáticamente un INTEGER columna llamada rowid para ti – a menos que haya proporcionado el WITHOUT ROWID cláusula (pero es probable que no supiera sobre esta cláusula). El rowid fila es una columna de clave principal. Si también especifica una columna de clave principal de este tipo (por ejemplo, utilizando la sintaxis some_column INTEGER PRIMARY KEY ) esta columna será simplemente un alias para rowid . Consulte aquí para obtener más información, que describe lo mismo en palabras bastante crípticas. Tenga en cuenta que una tabla SELECT * FROM table declaración no incluir rowid de forma predeterminada:debe solicitar el rowid columna explícitamente.

Verificar que PRAGMA realmente funciona

Entre otras cosas, PRAGMA las declaraciones se utilizan para configurar los ajustes de la base de datos o para invocar diversas funciones (documentos oficiales). Sin embargo, hay efectos secundarios no documentados en los que, a veces, establecer una variable en realidad no tiene ningún efecto . En otras palabras, no funciona y falla silenciosamente.

Por ejemplo, si emite las siguientes sentencias en el orden indicado, la última declaración no tener algún efecto. Variable auto_vacuum todavía tiene valor 0 (NONE ), sin una buena razón.

PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)

Puede leer el valor de una variable ejecutando PRAGMA variableName y omitiendo el signo igual y el valor.

Para corregir el ejemplo anterior, use un orden diferente. Usar el orden de filas 3, 1, 2 funcionará como se esperaba.

Es posible que incluso desee incluir dichos controles en su producción código, porque estos efectos secundarios pueden depender de la versión concreta de SQLite y de cómo se creó. La biblioteca utilizada en producción puede diferir de la que utilizó durante el desarrollo.

Reclamación de espacio en disco para bases de datos grandes

De forma predeterminada, el tamaño de un archivo de base de datos SQLite crece de forma monótona . Eliminar filas solo marca páginas específicas como libre , para que puedan usarse para INSERT datos en el futuro. Para recuperar espacio en disco y acelerar el rendimiento, hay dos opciones:

  1. Ejecutar VACUUM declaración . Sin embargo, esto tiene varios efectos secundarios:
    • Bloquea toda la base de datos. No se pueden realizar operaciones simultáneas durante el VACUUM operación.
    • Lleva mucho tiempo (para bases de datos más grandes), porque internamente recrea la base de datos en un archivo temporal separado y finalmente elimina la base de datos original, reemplazándola con ese archivo temporal.
    • El archivo temporal consume adicional espacio en disco mientras se ejecuta la operación. Por lo tanto, no es una buena idea ejecutar VACUUM en caso de que tenga poco espacio en disco. Todavía podría hacerlo, pero tendría que verificar regularmente que (freeDiskSpace - currentDbFileSize) > 0 .
  2. Utilice PRAGMA auto_vacuum = INCREMENTAL al crear la base de datos. Haz esto PRAGMA el primero declaración después de crear el archivo! Esto permite un poco de mantenimiento interno, lo que ayuda a la base de datos a recuperar espacio cada vez que llama a PRAGMA incremental_vacuum(N) . Esta llamada reclama hasta N paginas Los documentos oficiales proporcionan más detalles y también otros valores posibles para auto_vacuum .
    • Nota:puede determinar cuánto espacio libre en disco (en bytes) se ganaría al llamar a PRAGMA incremental_vacuum(N) :multiplica el valor devuelto por PRAGMA freelist_count con PRAGMA page_size .

La mejor opción depende de su contexto. Para archivos de bases de datos muy grandes, recomiendo la opción 2 , porque la opción 1 molestaría a sus usuarios con minutos u horas de espera para que se limpie la base de datos. La opción 1 es adecuada para bases de datos más pequeñas . Su ventaja adicional es que el rendimiento de la base de datos mejorará (que no es el caso de la opción 2), porque la recreación elimina los efectos secundarios de la fragmentación de datos.

Cuidado con el número máximo de variables en las consultas

De forma predeterminada, el número máximo de variables ("parámetros de host") que puede usar en una consulta está codificado en 999 (ver aquí, sección Número máximo de parámetros de host en una sola instrucción SQL ). Este límite puede variar, ya que es un tiempo de compilación parámetro, cuyo valor predeterminado usted (o cualquier otra persona que compiló SQLite) puede haber alterado.

Esto es problemático en la práctica, porque no es raro que su aplicación proporcione una lista (arbitrariamente grande) al motor de base de datos. Por ejemplo, si desea eliminar en masa DELETE (o SELECT ) filas basadas en, por ejemplo, una lista de ID. Una declaración como

DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)Code language: SQL (Structured Query Language) (sql)

arrojará un error y no se completará.

Para solucionar esto, considere los siguientes pasos:

  • Analice sus listas y divídalas en listas más pequeñas,
  • Si fuera necesaria una división, asegúrese de usar BEGIN TRANSACTION y COMMIT para emular la atomicidad que habría tenido una sola declaración .
  • Asegúrese de considerar también otros ? variables que podría usar en su consulta que no están relacionadas con la lista entrante (por ejemplo, ? variables utilizadas en un ORDER BY condición), de modo que el total número de variables no excede el límite.

Una solución alternativa es el uso de tablas temporales. La idea es crear una tabla temporal, insertar las variables de consulta como filas y luego usar esa tabla temporal en una subconsulta, por ejemplo,

DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)Code language: SQL (Structured Query Language) (sql)

Cuidado con la afinidad de tipos de SQLite

Las columnas de SQLite no se escriben estrictamente y las conversiones no suceden necesariamente como cabría esperar. Los tipos que proporciona son solo sugerencias . SQLite a menudo almacenará datos de cualquier escriba su original type, y solo convierta los datos al tipo de la columna en caso de que la conversión no tenga pérdidas. Por ejemplo, puede simplemente insertar un "hello" cadena en un INTEGER columna. SQLite no se quejará ni le advertirá sobre las discrepancias de tipos. Por el contrario, no puede esperar que los datos devueltos por un SELECT declaración de un INTEGER la columna siempre es un INTEGER . Estas sugerencias de tipo se conocen como "afinidad de tipo" en SQLite-speak, ver aquí. Asegúrese de estudiar detenidamente esta parte del manual de SQLite para comprender mejor el significado de los tipos de columna que especifica al crear nuevas tablas.

Cuidado con los enteros grandes

SQLite admite firmado enteros de 64 bits , que puede almacenar o hacer cálculos con él. En otras palabras, solo números de -2^63 a (2^63) - 1 son compatibles, ¡porque se necesita un bit para representar el signo!

Eso significa que si espera trabajar con números más grandes, p. enteros de 128 bits (con signo) o enteros de 64 bits sin signo, usted debe convertir los datos a texto antes de insertarlo .

El horror comienza cuando ignoras esto y simplemente insertas números más grandes (como enteros). SQLite no se quejará y almacenará un redondeado ¡número en su lugar! Por ejemplo, si inserta 2^63 (que ya está fuera del rango admitido), SELECT el valor de ed será 9223372036854776000, y no 2^63=9223372036854775808. Sin embargo, según el lenguaje de programación y la biblioteca de enlace que utilice, el comportamiento puede diferir. Por ejemplo, el enlace sqlite3 de Python comprueba tales desbordamientos de enteros.

No utilice REPLACE() para rutas de archivos

Imagina que almacenas rutas de archivos relativas o absolutas en un TEXT columna en SQLite, p. para realizar un seguimiento de los archivos en el sistema de archivos real. Aquí hay un ejemplo de tres filas:

foo/test.txt
foo/bar/
foo/bar/x.y

Supongamos que desea cambiar el nombre del directorio "foo" a "xyz". ¿Qué comando SQL usarías? ¿Este?

REPLACE(path_column, old_path, new_path) Code language: SQL (Structured Query Language) (sql)

Esto es lo que hice, hasta que empezaron a pasar cosas raras. El problema con REPLACE() es que reemplazará a todos ocurrencias Si hubiera una fila con la ruta "foo/bar/foo/", entonces REPLACE(column_name, 'foo/', 'xyz/') causará estragos, ya que el resultado no será "xyz/bar/foo/", sino "xyz/bar/xyz/".

Una mejor solución es algo como

UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"Code language: SQL (Structured Query Language) (sql)

El 4 refleja la longitud de la ruta anterior ('foo/' en este caso). Tenga en cuenta que usé GLOB en lugar de LIKE para actualizar solo aquellas filas que comienzan con 'foo/'.

Conclusión

SQLite es un motor de base de datos fantástico, donde la mayoría de los comandos funcionan como se esperaba. Sin embargo, las complejidades específicas, como las que acabo de presentar, aún requieren la atención de un desarrollador. Además de este artículo, asegúrese de leer también la documentación oficial de advertencias de SQLite.

¿Ha encontrado otras advertencias en el pasado? Si es así, házmelo saber en los comentarios.