SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-07

DuckDB frente a SQLite en un servidor: ¿cuál usar?

SQLite gestiona el estado transaccional y DuckDB analiza Parquet y CSV. Descubre por qué un VPS suele ejecutar ambos, con un ejemplo práctico de cada uno.

DuckDB frente a SQLite en un servidor: la respuesta en una frase

SQLite es un motor OLTP (procesamiento de transacciones en línea): almacena los datos en filas y está diseñado para leer y escribir unas pocas a la vez de forma segura y rápida. DuckDB es un motor OLAP (procesamiento analítico en línea): almacena los datos en columnas y está diseñado para recorrer millones de filas y devolver un único agregado. Ambos son bibliotecas integradas, ambos abren un archivo normal y ninguno ejecuta un proceso de servidor que tenga que supervisar.

Por tanto, la respuesta sincera a «cuál elegir» casi siempre es «ambos, en el mismo VPS». La aplicación mantiene su estado operativo en SQLite. Los informes leen archivos Parquet y CSV con DuckDB. No compiten porque no realizan el mismo trabajo.

Por qué el almacenamiento por filas y por columnas cambia la respuesta

SQLite escribe una fila como una pieza contigua de una página. Obtener un pedido mediante su clave primaria toca una página de índice y una página de datos, lo que equivale a dos lecturas. Eso es exactamente lo que una aplicación hace miles de veces por segundo: leer este usuario, actualizar esta sesión e insertar este pedido.

DuckDB escribe cada columna por separado y la comprime. Sumar amount_cents en cinco millones de filas sólo lee la columna amount_cents, omite todos los demás bytes del archivo y ejecuta la suma mediante código vectorizado sobre lotes de valores. Las demás columnas nunca se leen del disco. Ahí se origina la velocidad.

Ahora ejecute cada motor con la carga de trabajo del otro. Para sumar una columna, SQLite tiene que recorrer todas las filas y extraer la fila completa de la página para llegar a un campo, por lo que lee mucho más del disco de lo necesario. Para insertar un pedido, DuckDB tiene que tocar el almacenamiento de todas las columnas para escribir un solo valor y toma un bloqueo de escritura sobre todo el archivo de base de datos. Ninguno de los dos motores está averiado. Cada uno responde a una consulta para la que no fue diseñado.

Dónde destaca SQLite: estado transaccional de la aplicación

Elija SQLite cuando las escrituras sean pequeñas, frecuentes y no deban perderse. Sesiones, pedidos, filas de colas, ajustes y cualquier dato que cree una petición web.

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

Cree la tabla y active el registro de escritura anticipada en el mismo paso.

sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
  id           INTEGER PRIMARY KEY,
  customer     TEXT    NOT NULL,
  placed_at    TEXT    NOT NULL,
  amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
  VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQL

La primera línea de la salida es wal. Ese es el PRAGMA que informa del modo al que cambió, y es el ajuste más útil en un servidor. En el modo predeterminado de diario de reversión, una escritura bloquea a todos los lectores. En el modo WAL, los lectores siguen leyendo el último estado confirmado mientras una escritura añade datos. Así, un informe lento ya no retrasa la petición web que espera detrás.

Compruebe que la fila se haya recuperado:

sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"

Obtendrá 1|ana|2026-07-30T09:14:00Z|4200. Aparecieron dos archivos más junto a la base de datos: app.db-wal y app.db-shm. Ambos forman parte de ella. Copiar sólo app.db mientras la aplicación está en ejecución genera una copia de seguridad incoherente. Esto se explica más adelante.

SQLite sigue permitiendo un solo escritor a la vez. Ese límite se implementa mediante un bloqueo, no mediante una cola. Por ello, un segundo escritor que espere demasiado tiempo falla con database is locked en lugar de bloquearse indefinidamente. Aumente el tiempo de espera con PRAGMA busy_timeout = 5000; en cada conexión que abra la aplicación. Cinco segundos de espera eliminan la mayoría de estos errores en una carga web normal.

Dónde destaca DuckDB: análisis sobre archivos que ya tiene

Elija DuckDB cuando la pregunta empiece por «cuántos», «cuánto» o «cuáles son los diez primeros», y la entrada sea un conjunto de archivos CSV o Parquet. Instale el cliente de línea de comandos, versión 1.5.5 en julio de 2026:

curl https://install.duckdb.org | sh

El script instala el binario en ~/.duckdb/cli/latest/duckdb e imprime la línea que lo añade a PATH. Compruebe que se ejecuta:

~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"

Cree un archivo realista para consultar. Esto escribe cinco millones de filas de pedidos en Parquet, comprimidas con zstd:

mkdir -p /srv/data
duckdb :memory: "
COPY (
  SELECT i                                                     AS id,
         'cust_' || (i % 5000)                                 AS customer,
         TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
         (i * 37) % 20000                                      AS amount_cents
  FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"

Ahora formule la pregunta analítica. Abra el shell, active el temporizador y consulte el archivo directamente, sin un paso de importación:

.timer on
SELECT customer,
       count(*)                AS orders,
       sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;

Lea su propio resultado en .timer en lugar de confiar en uno publicado, porque el resultado depende de su disco y del número de núcleos. Lo importante es la forma del proceso. No hubo CREATE TABLE, ni INSERT ni un paso de carga: DuckDB leyó el footer de Parquet, determinó qué fragmentos de columnas necesitaba la consulta y leyó sólo esos fragmentos. Un directorio completo funciona igual con un patrón glob, FROM '/srv/data/orders-*.parquet', que es la forma de convertir un mes de exportaciones diarias en una sola consulta.

La velocidad del disco establece el límite inferior de todo este proceso, y un recorrido de columnas es una lectura secuencial larga. Por eso, la diferencia entre almacenamiento NVMe y SATA antiguo en un VPS se aprecia aquí con más claridad que con las pequeñas lecturas aleatorias de SQLite.

Lectura de la base de datos SQLite desde DuckDB

Los dos motores se integran mediante la extensión sqlite de DuckDB. Conecte la base de datos de la aplicación en modo de solo lectura para que una consulta analítica nunca pueda modificar el estado activo:

INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;

Esto lee las filas del archivo SQLite en el momento de la consulta, sin crear una copia. Es práctico, pero no es rápido, porque los datos del disco siguen almacenados por filas y DuckDB debe recorrerlos. Úselo para la exportación, no para un panel que se recarga cada treinta segundos:

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

Esa instrucción contiene todo el patrón. SQLite conserva las filas activas recientes. Una exportación programada convierte los periodos cerrados en Parquet. DuckDB responde a las consultas que abarcan meses y la base de datos de la aplicación se mantiene pequeña, lo que permite que sus escrituras sean rápidas.

Ejecute la exportación según una programación en lugar de hacerlo manualmente. Un par de servicio y temporizador de systemd tiene el tamaño adecuado para este caso: una unidad que ejecuta COPY y un temporizador que la activa cada noche.

Ejecución de ambos en un mismo VPS

Aquí no se necesita ningún contenedor ni ningún puerto. Ambos motores son bibliotecas, por lo que la instalación consiste en un paquete y una ruta de archivo. Si el resto de la pila ya se ejecuta mediante Docker Compose en el mismo VPS, monte el directorio de datos en el contenedor que lo necesite en lugar de añadir un servicio de base de datos, porque no hay ningún servicio que añadir.

Dos reglas evitan problemas con esta configuración.

Asigne a cada motor su propio directorio: /srv/app para el archivo SQLite que escribe la aplicación y /srv/data para los archivos Parquet que lee el sistema de análisis. Si comparten un directorio, una tarea de copia de seguridad que toma una instantánea de uno puede entrar en conflicto con el otro.

No haga que dos procesos apunten al mismo archivo de base de datos DuckDB en modo de lectura y escritura. Sólo un proceso puede mantener abierto un archivo DuckDB para escritura, y el segundo proceso no podrá abrirlo. Se permiten muchos lectores si todos establecen access_mode = 'READ_ONLY'. Esto puede sorprender a quienes llegan desde SQLite, donde varios procesos comparten habitualmente un archivo. Si el sistema de análisis sólo lee archivos Parquet, la cuestión no se plantea. Esta es otra razón para mantener el estado persistente en SQLite.

Las copias de seguridad difieren, y la diferencia provoca problemas

Una base de datos SQLite en ejecución consta de tres archivos. Si los copia con cp mientras se están modificando, obtiene un archivo que se abre, pero contiene datos incorrectos. Use el comando de copia de seguridad del propio motor. Este crea una instantánea coherente mientras la aplicación sigue escribiendo:

sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"

integrity_check muestra ok cuando la copia es correcta. Cualquier otro resultado significa que debe descartar esa instantánea y crear otra.

Los archivos Parquet no cambian después de escribirse, por lo que no requieren un tratamiento especial: haga una copia de seguridad del directorio. Envíe ambas rutas fuera del servidor mediante copias de seguridad de restic desde su VPS. De este modo, toda la capa de datos consta de dos directorios dentro de un único trabajo de copia de seguridad.

Modos de fallo y las cadenas exactas que verá

Error: database is locked de SQLite significa que otra conexión mantuvo el bloqueo de escritura durante más tiempo del permitido por el tiempo de espera. No indica corrupción. Establezca PRAGMA busy_timeout en cada conexión y busque una transacción larga que debería haberse dividido en varias transacciones cortas.

Error: unable to open database file después de cambiar los permisos suele significar que el proceso puede escribir en el archivo, pero no en su directorio. SQLite crea app.db-wal y app.db-shm junto a la base de datos, por lo que el directorio debe tener permisos de escritura, no sólo el archivo .db.

IO Error: Could not set lock on file de DuckDB significa que otro proceso ya tiene abierta esa base de datos para escritura. Cierre el otro shell o abra el suyo en modo de sólo lectura.

Out of Memory Error de DuckDB en un VPS pequeño significa que una consulta necesitó más memoria de trabajo de la disponible. DuckDB usa el disco como almacenamiento temporal cuando puede, así que proporciónele un lugar donde hacerlo abriendo un archivo de base de datos en disco en lugar de :memory:, y limite su consumo con SET memory_limit = '2GB';. En un equipo que ejecuta otros servicios, ese límite evita que una consulta ad hoc deje a su aplicación sin memoria RAM.

Binder Error: Referenced column "amount" not found al consultar Parquet casi siempre significa que el esquema del archivo no es el que recuerda. Ejecute DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; y lea los nombres de columna reales.

Cómo elegir en la práctica

Pregunte cuál es el patrón de escritura. Muchas escrituras pequeñas que deben sobrevivir a un corte de energía apuntan a SQLite. Pregunte cuál es el patrón de lectura. Los análisis completos con agregaciones sobre un historial extenso apuntan a DuckDB. La mayoría de los sistemas reales responden afirmativamente a ambas preguntas. La respuesta adecuada es asignar a cada motor la parte que resuelve bien, en lugar de obligar a uno de ellos a cubrir las limitaciones del otro.

La migración que debe evitar es trasladar el estado activo de la aplicación a DuckDB porque un informe era lento. El informe era lento debido a la organización del almacenamiento. Por tanto, la solución es una exportación, no reescribir la ruta de escritura.

FAQ

¿Puede DuckDB sustituir a SQLite como base de datos de mi aplicación?

No si la aplicación escribe con frecuencia. DuckDB bloquea todo el archivo de base de datos para escritura, permite un solo proceso de lectura y escritura a la vez y está optimizado para cambios masivos, no para inserciones de una sola fila. Mantenga el estado transaccional en SQLite y deje que DuckDB lo lea con ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); cuando necesite generar un informe.

¿DuckDB es realmente más rápido que SQLite para análisis?

Sí, al recorrer y agregar datos de una tabla grande. La razón es la distribución del almacenamiento, no un ajuste específico. DuckDB lee sólo las columnas que indica la consulta y procesa los valores en lotes, mientras que SQLite debe recorrer filas completas para acceder a un campo. Para obtener una sola fila mediante la clave primaria, el resultado se invierte: SQLite accede a dos páginas y DuckDB accede al almacenamiento de todas las columnas.

¿Necesito mucha RAM para ejecutar DuckDB en un VPS?

No, pero asígnele un límite y espacio en disco. Abra un archivo de base de datos en lugar de :memory: para que DuckDB pueda volcar los resultados intermedios al disco y establezca SET memory_limit = '2GB'; en un valor que su VPS pueda reservar. Sin un límite, un GROUP BY grande puede provocar Out of Memory Error o expulsar otros servicios de la RAM.

¿Cómo paso mis datos de SQLite a Parquet?

Adjunte el archivo de SQLite desde DuckDB y copie directamente una consulta con COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Ejecútelo según una programación para los periodos cerrados, como las filas del mes pasado, y deje las filas recientes en SQLite, donde la aplicación todavía escribe.

¿Cuál debo respaldar y cómo?

Ambos, de formas diferentes. Cree instantáneas de SQLite con sqlite3 app.db ".backup '/srv/backup/app.db'" en lugar de cp, porque una base de datos en ejecución también es un -wal y un archivo -shm, y una copia simple puede quedar incompleta. Los archivos Parquet no cambian una vez escritos, por lo que basta con copiar el directorio.