Back to flin
flin

FlinDB vs SQLite: por qué construimos el nuestro

Una comparación detallada de FlinDB y SQLite -- dónde se solapan, dónde divergen, y por qué construimos un motor de base de datos personalizado para el lenguaje de programación FLIN en lugar de embeber SQLite.

Thales & Claude | March 30, 2026 9 min flin
EN/ FR/ ES
flinrust

"¿Por qué no simplemente usar SQLite?"

Escuchamos esta pregunta desde el primer día que anunciamos FlinDB. Y es una pregunta justa. SQLite es el motor de base de datos más desplegado del mundo. Es embebido, basado en archivos, sin configuración (más o menos), probado en batalla en miles de millones de dispositivos, y de código abierto. Si necesitas una base de datos embebida, SQLite es la opción obvia.

Elegimos construir la nuestra de todos modos. Este artículo explica por qué -- no para desprestigiar a SQLite (que es una obra maestra de ingeniería) sino para articular los problemas específicos que SQLite no puede resolver para FLIN, y las decisiones de diseño que tomamos al construir FlinDB.

Dónde se solapan

FlinDB y SQLite comparten características fundamentales:

FuncionalidadSQLiteFlinDB
Embebido
Basado en archivos
Compatible ACID
Soporte WAL
Transacciones
Claves foráneas
Sin servidor

Ambos se ejecutan en proceso. Ambos almacenan datos en archivos locales. Ambos proporcionan garantías ACID. Ambos soportan logging de escritura anticipada. Para el caso de uso de "necesito una base de datos embebida simple para una aplicación de un solo usuario", ambos funcionarían.

La divergencia comienza cuando miras para qué fue diseñada cada base de datos.

El problema del acceso remoto

La queja número uno de los usuarios de SQLite que lo superan: sin acceso remoto.

SQLite es una biblioteca. Se ejecuta dentro del proceso de tu aplicación. No hay protocolo de red. No hay puerto TCP. Si necesitas una app móvil y un panel web que accedan a los mismos datos, no puedes apuntar ambos a un archivo SQLite.

La solución estándar es construir una API REST delante de SQLite. Esto no es culpa de SQLite. SQLite fue diseñado para ser una base de datos a nivel de aplicación, no un servidor. Pero la consecuencia es que cada aplicación SQLite que necesita acceso de red debe construir su propia capa de API -- duplicando esfuerzo en millones de proyectos.

En FLIN, las rutas SON la API:

flinentity Task {
    title: string @required @min(1)
    completed: bool = false
    created_at: timestamp = now()
}

api GET /tasks {
    @doc "List all tasks"
    @returns [Task]
    return json(Task.all())
}

api POST /tasks {
    @doc "Create a new task"
    @body { title: string }
    @returns Task
    task = Task.create(body)
    return json(task, 201)
}

Ejecutas flin serve y tienes un servidor HTTP con una API REST, documentación auto-generada, validación de tipos y respuestas JSON. La base de datos y la API son lo mismo.

El problema de escrituras concurrentes

SQLite usa bloqueo a nivel de base de datos. Un escritor a la vez. Punto. El modo WAL ayuda con lecturas pero no con escrituras. Bajo carga pesada de escritura, SQLite se convierte en un cuello de botella.

FlinDB implementa bloqueo a nivel de fila con detección de deadlocks:

rustpub struct LockManager {
    row_locks: DashMap<EntityRowKey, LockInfo>,
    wait_graph: RwLock<HashMap<TransactionId, HashSet<TransactionId>>>,
}

Las actualizaciones concurrentes a diferentes filas proceden en paralelo. Solo las actualizaciones concurrentes a la misma fila se serializan. Para una aplicación web manejando múltiples solicitudes simultáneas, esta es la diferencia entre una API responsiva y un cuello de botella.

El problema del viaje en el tiempo

SQLite no rastrea historial. Una sentencia UPDATE sobrescribe el valor anterior. Una sentencia DELETE elimina la fila. Desaparecida. Construir consultas temporales sobre SQLite requiere cientos de líneas de triggers SQL para cada tabla.

En FlinDB, el viaje en el tiempo está integrado en el modelo de almacenamiento:

flin// Get the user's state at any point in time
user_then = User.find(1) @ "2025-06-15"
user_now = User.find(1)

// Compare states
if user_then.email != user_now.email {
    log("Email changed since June 15th")
}

// Get entity history
history = User.find(1).history

El operador @ no es una funcionalidad añadida. Es un operador a nivel de lenguaje que mapea al event store EAVT. Cada guardado crea una nueva versión. Cada versión se preserva. El viaje en el tiempo es una búsqueda en índice, no una reconstrucción.

El problema de la búsqueda semántica

SQLite tiene FTS5 para búsqueda de texto completo. Es bueno para coincidencia de palabras clave. No entiende significado.

FlinDB tiene búsqueda semántica integrada con embeddings vectoriales:

flinentity Product {
    name: text
    description: semantic text
}

// Finds products by meaning, not just keywords
results = search "comfortable seating for work" in Product by description
// Returns: "Ergonomic Office Chair with lumbar support"
// Even though no words match exactly

El tipo semantic text genera automáticamente embeddings vectoriales, los almacena en un índice vectorial interno y habilita búsqueda por similitud coseno. El modo de búsqueda híbrida combina ranking de palabras clave BM25 con similitud vectorial usando Reciprocal Rank Fusion.

El problema de seguridad de tipos

SQLite tiene tipado notoriamente laxo. Una columna declarada como INTEGER puede contener una cadena. El sistema de tipos de SQLite es más una sugerencia que un mecanismo de aplicación.

FlinDB aplica tipos a nivel de esquema:

flinentity User {
    name: text
    age: int
    email: text @pattern(email, "^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$", "Invalid email")
}

// This fails with a type error:
save User { name: "Thales", age: "not a number", email: "bad" }
// Error 1: age must be int, got text
// Error 2: email does not match pattern: Invalid email

FlinDB valida en el momento del guardado, no en el momento de la consulta. Los datos inválidos nunca entran en la base de datos.

El problema del lenguaje de consulta

Esta es la diferencia más fundamental. SQLite habla SQL. FLIN habla FLIN.

sql-- SQLite
SELECT tasks.*, users.name as author_name
FROM tasks
JOIN users ON tasks.user_id = users.id
WHERE users.id = ?
ORDER BY tasks.created_at DESC
LIMIT 10;
flin// FLIN
Task.where(user: auth.user)
    .include(user)
    .order(created_at: desc)
    .limit(10)

SQL es una cadena embebida en otro lenguaje. Tiene su propia sintaxis, sus propios mensajes de error, su propio sistema de tipos. La inyección SQL es posible porque la consulta es una cadena que se interpola. Los ORMs existen únicamente para traducir entre el lenguaje de aplicación y SQL.

Las consultas de FlinDB son expresiones FLIN. Son verificadas de tipos por el compilador. No pueden ser inyectadas. No necesitan un ORM porque no hay desajuste de impedancia entre el lenguaje de consulta y el lenguaje de aplicación.

Comparación de rendimiento

Para cargas de trabajo de un solo hilo, SQLite y FlinDB rinden similarmente:

OperaciónSQLiteFlinDB
Lectura (una fila)~10 microsegundos~15 microsegundos
Lectura (1.000 filas)~1 ms~1,5 ms
Escritura (una fila)~50 microsegundos~60 microsegundos
Búsqueda de texto completo~1 ms~1 ms

FlinDB es ligeramente más lento para operaciones básicas porque mantiene historial de versiones e índices. La sobrecarga es insignificante para cargas de trabajo de aplicaciones.

Para cargas de trabajo concurrentes, FlinDB escala dramáticamente mejor:

OperaciónSQLiteFlinDB
100 escrituras concurrentes~5.000 microsegundos (serializado)~150 microsegundos (paralelo)
Búsqueda semánticaN/A~50 ms
Consulta de viaje en el tiempoN/A~20 microsegundos

La mejora de 33x en escrituras concurrentes viene del bloqueo a nivel de fila. SQLite serializa todas las escrituras a través de un solo bloqueo de base de datos. FlinDB solo serializa escrituras a la misma fila.

Cuándo usar cada uno

Usa SQLite cuando: - Necesitas una base de datos embebida simple sin acceso de red - Aplicaciones de escritorio de un solo usuario - Apps móviles (almacenamiento local) - Pruebas y prototipado - Cargas de trabajo con mucha lectura y escrituras mínimas - Estás construyendo una biblioteca, no una aplicación

Usa FlinDB cuando: - Estás construyendo una aplicación FLIN (obviamente) - Necesitas consultas de viaje en el tiempo - Necesitas búsqueda semántica/IA - Necesitas escrituras concurrentes - Quieres generación automática de API - Quieres seguridad de tipos y validación en la capa de datos - Necesitas suscripciones en tiempo real

Usa PostgreSQL cuando: - Necesitas funcionalidades SQL avanzadas (CTEs, funciones de ventana) - Tienes millones de conexiones concurrentes - Necesitas replicación de grado empresarial - Tienes un equipo DBA dedicado - Estás construyendo un data warehouse

La evaluación honesta

SQLite es mejor que FlinDB siendo una base de datos SQL embebida de propósito general. Tiene 25 años de pruebas en batalla, miles de millones de despliegues y una suite de especificación de millones de pruebas. FlinDB no puede igualar esta madurez.

FlinDB es mejor que SQLite siendo el motor de almacenamiento para aplicaciones FLIN. Habla el lenguaje nativamente. Proporciona funcionalidades que SQLite no tiene (versionado temporal, búsqueda semántica, suscripciones en tiempo real, bloqueo a nivel de fila). Elimina categorías completas de configuración que SQLite requiere (capa API, ORM, sistema de migraciones, seguimiento de historial).

No construimos FlinDB porque SQLite sea malo. Lo construimos porque FLIN necesita una base de datos que fue diseñada para FLIN -- una que comparta su filosofía de configuración cero, diseño centrado en entidades y conciencia temporal. SQLite fue diseñado para una era y un paradigma diferentes. FlinDB fue diseñado para FLIN.


Esta es la Parte 14 de la serie "Cómo construimos FlinDB", documentando cómo construimos un motor de base de datos embebido completo para el lenguaje de programación FLIN.

Navegación de la serie: - [067] Tree Traversal and Integration Testing - [068] FlinDB Hardening for Production - [069] FlinDB vs SQLite: Why We Built Our Own (estás aquí) - [070] Persistence in the Browser

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles