SQLite también necesita ediciones al estilo Rust
(mort.coffee)- SQLite es un RDBMS autocontenido muy usado en almacenamiento local y proyectos embebidos, pero por compatibilidad hacia atrás mantiene valores predeterminados que perjudican la seguridad y el rendimiento
- De forma predeterminada no aplica las restricciones de claves foráneas y puede reutilizar
ROWID, lo que puede causar problemas de integridad de datos donde las publicaciones de un usuario eliminado quedan asociadas a otro usuario que recibió el mismo ID - Las tablas normales también almacenan tipos distintos a los declarados en sus columnas, y las tablas STRICT lo evitan, pero hay que especificar
stricten cada tabla y esto entra en conflicto con la forma de usar nombres de tipos definidos por el usuario - En escrituras concurrentes, el
SQLITE_BUSYinmediato, el WAL desactivado y una configuración conservadora de sincronización pueden mejorarse con las pragmasbusy_timeout,journal_modeysynchronous, respectivamente - Si se pudiera elegir un paquete de valores seguros mediante una edición basada en año como
PRAGMA edition = 2026, SQLite podría seguir evolucionando sus configuraciones predeterminadas sin romper el comportamiento existente
Fortalezas de SQLite y valores predeterminados anticuados
- SQLite puede considerarse el estándar de la industria como base de datos para proyectos embebidos y almacenamiento local, y hasta cierto software de servidor como lobste.rs ya corre sobre SQLite
- A diferencia de un RDBMS tradicional que corre en un proceso separado, es un RDBMS en forma de biblioteca, lo que permite mantener el software como una unidad autocontenida
- Como no hace falta escribir serializadores y parsers personalizados como si fuera un formato de archivo común, ofrece a la vez las ventajas de un RDBMS y del almacenamiento basado en archivos
- Sin embargo, sigue existiendo el problema de que varios valores predeterminados son inadecuados y afectan la integridad de datos, la concurrencia y el rendimiento
Problemas de integridad por no aplicar claves foráneas
- Las claves foráneas son un mecanismo esencial para mantener la consistencia de la base de datos y evitar referencias rotas, pero SQLite no las aplica por defecto
- Aunque se declare que
posts.user_idreferencia ausers.id, con la configuración predeterminada no se impiden acciones como las siguientes- Crear una publicación con un ID de usuario no válido
- Eliminar a un usuario dejando sus publicaciones atrás
INTEGER PRIMARY KEYse convierte en un alias delROWIDde la tabla, y el algoritmo de asignación deROWIDde SQLite reutiliza IDs en algunas situaciones- Si Bob crea publicaciones con el ID
1y luego solo se elimina su cuenta, no se produce ningún error de clave foránea - Después, si Alice recibe el ID reutilizado
1, las publicaciones anteriores de Bob quedan asociadas a Alice - Como las consultas siguen funcionando normalmente, este problema puede ser más difícil de detectar que una simple referencia rota
- Si Bob crea publicaciones con el ID
- Se puede aplicar el uso de claves foráneas activando la siguiente pragma en cada conexión
PRAGMA foreign_keys = ON;
- Si esto se hubiera activado desde el principio, eliminar a un usuario que todavía tiene publicaciones se habría detenido con el error
FOREIGN KEY constraint failed
Tablas normales que permiten tipos distintos a los declarados
- Los valores en SQLite son uno de
NULL,INTEGER,REAL,TEXToBLOB, pero la declaración de columnas en una tabla normal no limita estrictamente los tipos que pueden almacenarse, sino que determina la afinidad de tipo (type affinity) - A una columna con afinidad
INTEGERse le aplican las siguientes reglas de almacenamiento- Un
TEXTque pueda interpretarse como entero se convierte aINTEGER - Un
TEXTque pueda interpretarse como número real se convierte aREAL, es decir, punto flotante de doble precisión - Los demás valores se almacenan con su tipo original
- Un
- Otras afinidades también tienen sus propias reglas de conversión
- La afinidad
BLOBalmacena el valor tal como está - La afinidad
TEXTdejaBLOB,TEXTyNULLcomo están, y convierte números aTEXT - La afinidad
REALfunciona como la afinidadINTEGER, pero convierte enteros aREAL
- La afinidad
- Por eso, incluso en una columna
duration_sec INTEGERse puede guardar una cadena no numérica como'Way too long, I mean come on' - En proyectos reales incluso hubo casos donde, en vez de guardarse números booleanos
1y0, se registraron las cadenas'1'y'0', obligando a limpiar y depurar los datos
Conflicto entre tablas STRICT y tipos definidos por el usuario
- Las tablas STRICT generan un error de tipo como
cannot store TEXT value in INTEGER columnsi se intenta insertar un tipo incorrecto en una columna
CREATE TABLE music (
id INTEGER PRIMARY KEY,
name TEXT,
duration_sec INTEGER
) strict;
-
No existe una pragma para volver estrictas todas las tablas globalmente, así que hay que agregar
strictsin falta en cada sentenciaCREATE TABLE -
SQLite sostiene que los tipos flexibles son útiles, pero incluso en tablas STRICT se puede crear una columna que acepte cualquier tipo de valor si se declara el tipo
ANY- Eso permite distinguir explícitamente entre columnas que aceptan tipos arbitrarios y columnas que deben forzar un tipo específico
-
Un problema de compatibilidad más realista es que las tablas STRICT no solo fuerzan tipos, sino que también cambian las reglas de interpretación de los especificadores de tipo
-
Cómo interpretan los nombres de tipo las tablas normales
- Una tabla normal de SQLite determina la afinidad según la cadena de tipo declarada
- Si contiene
"INT", se trata comoINTEGER - Si contiene alguna de
"CHAR","CLOB"o"TEXT", pasa a serTEXT - Si contiene
"BLOB"o se omite el tipo, se trata comoBLOB - Si contiene alguna de
"REAL","FLOA"o"DOUB", pasa a serREAL - Si no entra en ninguno de esos casos, se trata como
NUMERIC - Al combinar tipos flexibles con estas reglas, se pueden usar nombres de tipos definidos por el usuario como
DATETIME,KEY_VALUE_SEToCOLORen las columnas - Los conectores o wrappers de base de datos pueden mirar el nombre del tipo para serializar y deserializar automáticamente
- Incluso si no hay una función de procesamiento especial, el propio nombre del tipo documenta los datos esperados en la columna
- Si STRICT se convirtiera tal cual en el valor predeterminado, habría que renunciar a esta forma de uso, así que se necesitarían alias explícitos de tipos definidos por el usuario
CREATE TYPE KEY_VALUE_SET = TEXT;- Si en tablas estrictas se pudiera usar un alias como
KEY_VALUE_SET, el esquema también podría registrar el patrón esperado de datos para una columnaTEXTque la aplicación debe parsear - También sería útil una función para asociar restricciones
CHECKa tipos definidos por el usuario - El estándar SQL:1999 ya incluye
CREATE DOMAIN, una sintaxis de alias de tipos que incluso admite restricciones, así que SQLite podría implementarlo soportando esa sentencia estándar
Escrituras concurrentes y SQLITE_BUSY
- SQLite permite múltiples lecturas concurrentes, pero solo procesa una escritura a la vez
- En la configuración predeterminada, cuando dos procesos intentan obtener al mismo tiempo un bloqueo de escritura, uno de ellos recibe inmediatamente el error
SQLITE_BUSYen lugar de esperar- Como una escritura que realiza E/S de disco puede tardar por naturaleza, es más natural esperar un tiempo a que se libere el bloqueo
- En sistemas reales hubo casos donde el software se detenía de forma intermitente y obligaba a escribir bucles de reintento manualmente
- La siguiente configuración hace que solo se devuelva
SQLITE_BUSYdespués de reintentar adquirir el bloqueo durante un máximo de 5 segundos
PRAGMA busy_timeout = 5000;
- En operación normal, lo adecuado es concentrar todas las escrituras en un solo proceso y, si es posible, en un solo hilo; las escrituras concurrentes no pueden ser rápidas
- Aun así, hay casos perfectamente normales donde se necesitan escrituras concurrentes
- Limpiar interactivamente la base de datos con la herramienta de línea de comandos
sqlite3 - Ejecutar scripts para tareas administrativas poco frecuentes sin un frontend aparte
- Limpiar interactivamente la base de datos con la herramienta de línea de comandos
- Debido a la configuración predeterminada, estas tareas pueden provocar
SQLITE_BUSYinesperados y detener el software en ejecución
WAL y valores predeterminados de rendimiento
- SQLite, si se configura correctamente, puede ser lo bastante rápido como para asumir parte de las funciones que antes recaían en grandes servidores como PostgreSQL o MySQL, pero su rendimiento predeterminado no es bueno
- Puede verse más sobre ajuste fino para entornos de servidor en Optimizing SQLite for servers
- El problema más importante entre los valores predeterminados es que Write-Ahead Log, es decir, WAL está desactivado
PRAGMA journal_mode = WAL;
- WAL mejora mucho la velocidad de escritura en la mayoría de los casos
- Si además se usa la siguiente configuración, se puede reducir mucho la cantidad de sincronizaciones a disco sin riesgo de corrupción de datos
PRAGMA synchronous = NORMAL;
- El comportamiento concreto de
synchronousestá resumido en la documentación de pragmas de SQLite
Ediciones de SQLite basadas en año
- Si se cambiaran directamente ahora los valores predeterminados existentes, podría romperse software antiguo y hacer que los usuarios teman futuras actualizaciones de SQLite
- Manteniendo la compatibilidad hacia atrás, podría agregarse una super pragma para elegir un conjunto mejorado de valores predeterminados
PRAGMA edition = 2026;
- La edición
2026funcionaría al menos como alias de la siguiente configuración
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
- En esta edición, el modo
strictpara tablas nuevas también se aplicaría de forma predeterminada - Sería un enfoque similar al de las ediciones de Rust, donde se elige explícitamente un comportamiento nuevo sin alterar el del código existente
- A diferencia de un modo único como
"use strict";de JavaScript, una edición basada en año puede ir cambiando con el tiempo un conjunto razonable de valores predeterminados - Por ejemplo, si WAL2 de Hctree entrara a la rama principal en 2034, un futuro
PRAGMA edition = 2034podría configurarse para elegirjournal_mode = WAL2 - Un sistema de ediciones podría ser una solución intermedia para que SQLite siga evolucionando sus valores predeterminados de seguridad y rendimiento sin romper el comportamiento predeterminado de los programas existentes
2 comentarios
La teoría suena bien, pero considerando los conflictos de compatibilidad y la sobrecarga, creo que saldría perdiendo.
SQLite es un DBMS que nació para funcionar en entornos embebidos como Aegis,
y el mundo embebido es de andar reventando
void**, declarar__weak__y luego sobrescribir funciones. En ese mundo, me queda la duda de si realmente hace falta eso.Opiniones en Lobste.rs
Escribí este texto ayer después de ver la noticia de que lobste.rs había migrado a SQLite (https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite)
Mientras leía esa conversación, recordé pequeñas molestias de los valores predeterminados de SQLite y las reuní en una entrada de blog
Si tienes tiempo, valdría la pena compartir este texto también en el foro de SQLite: https://sqlite.org/forum/forum
En SQL,
CREATE TYPEse usa para crear tipos compuestos, y Postgres admite ahí rangos, enumeraciones, arreglos e incluso tipos base bastante complejosLa funcionalidad deseada se parece más a
CREATE DOMAIN. Permite crear un newtype a partir de un tipo concreto base y también especificar valores predeterminados y restricciones, lo que hace mucho más concisos los esquemas que repiten la misma restricciónCHECKen varias tablas. Por ejemplo, se puede escribircreate domain mything as text;ocreate domain mything as integer not null check (VALUE < 5);También hace falta cubrir una trampa bastante peligrosa: solo con
busy_timeoutno se pueden evitar los erroresSQLITE_BUSYque ocurren de inmediatoSi después de iniciar una transacción con una consulta de solo lectura se ejecuta una consulta de escritura para intentar elevar el bloqueo que ya se tiene, y hay otra operación de escritura, falla de inmediato sin importar la configuración de
busy_timeout. Para resolverlo hay que usarbegin immediateHay que considerar que algunas de las configuraciones incluidas en la edición propuesta son propiedades de la conexión en el modelo de SQLite, mientras que las demás son propiedades del archivo de base de datos o de tablas individuales
PRAGMA journal_mode = WALse mantiene en el archivo de base de datos, peroPRAGMA foreign_keys = ONdebe configurarse cada vez para cada conexión.STRICTes una propiedad de cada tabla, así que dentro de una misma base de datos también se pueden mezclar tablas estrictas y no estrictas. Por lo tanto, hay que decidir si este PRAGMA unificado se definirá como una propiedad del archivo de base de datos o de la conexión. También me da curiosidad si existe algún mecanismo incorporado que advierta para que una biblioteca antigua no modifique y arruine un archivo que contiene funciones que no conoceEl enfoque por ediciones parece, en esencia, un buen punto medio entre el objetivo del equipo de SQLite de mantener la compatibilidad hacia atrás y valores predeterminados menos confusos para usuarios que quieren la configuración moderna recomendada
Últimamente estoy muy metido en RocksDB. Es una biblioteca de base de datos embebida como SQLite, pero considero que el hecho de que no tenga SQL es más bien una gran ventaja
Siento que la restricción de tener que expresar todas las operaciones en SQL solo estorba al trabajar con bases de datos
Me cuesta imaginar cómo migrar un sistema que usa SQL, como lobste.rs, a RocksDB u otra base de datos clave-valor
No estoy de acuerdo con que configurar
busy_timeoutsea la solución. SQLite tiene un rendimiento muy alto si se usa un único hilo de escritura y se procesan por lotes transacciones lógicas independientes entre síEl método concreto se explica en https://andersmurphy.com/2025/12/…
Pero también es común modificar directamente la base de datos para tareas de mantenimiento, o ejecutar por script tareas administrativas ocasionales que no ameritan crear una pantalla de administración aparte. En ese caso, no es bueno que el servidor o la aplicación pueda detenerse de forma aleatoria solo porque, por mala suerte, coincidió el momento de ejecución