SSD Nodes Learn 8GB de RAM — $66/año
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-02

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

SQLite gestiona transacciones y estado de la aplicación; DuckDB analiza millones de filas en Parquet y CSV. En un VPS, normalmente conviene usar ambos.

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 analizar 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 deba supervisar.

Por tanto, la respuesta honesta a «cuál elegir» casi siempre es «ambos, en el mismo VPS». La aplicación mantiene su estado activo 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 por su clave principal toca una página de índice y una página de datos: son dos lecturas. Eso es exactamente lo que una aplicación hace miles de veces por segundo: leer este usuario, actualizar esta sesión, insertar este pedido.

DuckDB escribe cada columna por separado y la comprime. Sumar amount_cents en cinco millones de filas solo 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. De ahí proviene la velocidad.

Ahora ejecute cada motor con la carga de trabajo del otro. Para sumar una columna, SQLite debe recorrer todas las filas y extraer la fila completa de la página para acceder a un solo campo. Por eso lee mucho más del disco de lo necesario. Para insertar un pedido, DuckDB debe acceder al 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á dañado. Cada uno intenta responder a una pregunta para la que no fue diseñado.

Cuándo gana SQLite: estado transaccional de la aplicación

Elige SQLite cuando las escrituras sean pequeñas, frecuentes y no deban perderse. Sesiones, pedidos, filas de cola, configuración y cualquier dato que cree una solicitud web.

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

Crea la tabla y activa 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. Es PRAGMA, que informa del modo al que cambió, y es la configuración 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 un escritor agrega datos. Así, un informe lento ya no retrasa la solicitud web que espera detrás.

Comprueba que la fila se devolvió:

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

Obtienes 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, y ambos forman parte de ella. Copiar solo app.db mientras la aplicación está en ejecución produce una copia de seguridad inconsistente. Esto se explica más adelante.

SQLite sigue permitiendo un solo escritor a la vez. Ese límite es un bloqueo, no una cola. Por tanto, un segundo escritor que espera demasiado falla con database is locked en lugar de bloquearse indefinidamente. Aumenta 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.

Donde DuckDB destaca: análisis sobre archivos que ya tiene

Elija DuckDB cuando la pregunta empiece por "cuántos", "cuánto" o "cuáles son los diez principales", y la entrada sea un conjunto de archivos CSV o Parquet. Instale el cliente de línea de comandos, versión 1.5.5 a fecha de 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. Confirme 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 consulta 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 un valor publicado, porque el resultado depende de su disco y del número de núcleos. Lo importante es la forma del resultado. 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 solo leyó esos fragmentos. Un directorio completo funciona de la misma manera con un glob, FROM '/srv/data/orders-*.parquet', que convierte un mes de exportaciones diarias en una sola consulta.

La velocidad del disco es el límite inferior de todo esto, y un análisis de columnas es una lectura secuencial larga. Por eso, la diferencia entre NVMe y el almacenamiento 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. Adjunte la base de datos de la aplicación en modo de solo lectura, para que una consulta de análisis nunca pueda escribir en 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 dashboard 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 mantiene las filas activas recientes. Una exportación programada convierte los períodos cerrados en Parquet. DuckDB responde a todas las consultas que abarcan meses y la base de datos de la aplicación se mantiene pequeña, lo que acelera sus escrituras.

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 esto: una unidad que ejecuta COPY y un temporizador que la activa cada noche.

Ejecutar ambos en un VPS

Nada de esto necesita un contenedor ni un 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 con Docker Compose en el mismo VPS, monte el directorio de datos en el contenedor que lo necesite en lugar de agregar un servicio de base de datos, porque no hay ningún servicio que agregar.

Dos reglas evitan problemas en 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, un trabajo de copia de seguridad que toma una instantánea de uno termina compitiendo con el otro.

No apunte dos procesos al mismo archivo de base de datos DuckDB en modo de lectura y escritura. Solo un proceso puede mantener un archivo DuckDB abierto para escritura; el segundo no puede abrirlo. Varios lectores funcionan si todos establecen access_mode = 'READ_ONLY'. Esto sorprende a quienes vienen de SQLite, donde es habitual que varios procesos compartan un archivo. Si el sistema de análisis solo lee archivos Parquet, la cuestión no se plantea, lo que constituye otra razón para mantener el estado persistente en SQLite.

Las copias de seguridad difieren, y la diferencia causa problemas

Una base de datos SQLite en ejecución consta de tres archivos. Si los copia con cp durante una escritura, 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 en una copia correcta. Cualquier otro resultado indica que debe descartar esa instantánea y crear otra.

Los archivos Parquet no cambian después de escribirse. Por tanto, no requieren un tratamiento especial: haga una copia de seguridad del directorio. Envíe ambas rutas fuera del servidor con copias de seguridad de restic desde su VPS. Así, toda la capa de datos consta de dos directorios en un solo 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 normalmente significa 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 solo 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 solo 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 cuando puede, así que proporciónele un lugar donde hacerlo abriendo un archivo de base de datos en el 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 expulse la aplicación de la 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 reales de las columnas.

Cómo elegir en la práctica

Pregunte cuál es el patrón de escritura. Muchas escrituras pequeñas que deben conservarse tras un corte de energía indican que debe usar SQLite. Pregunte cuál es el patrón de lectura. Los análisis completos con agregaciones sobre un historial extenso indican que debe usar DuckDB. En la mayoría de los sistemas reales, la respuesta es afirmativa para ambas preguntas. La opción correcta es asignar a cada motor la parte que resuelve mejor, en lugar de obligar a uno de ellos a cubrir la función del otro.

La migración que debe evitar es mover el estado activo de la aplicación a DuckDB porque un informe era lento. El informe era lento por la disposició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 un informe lo necesite.

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

Para realizar exploraciones y agregaciones sobre una tabla grande, sí. La razón es la disposición del almacenamiento, no un ajuste específico. DuckDB lee solo las columnas que especifica la consulta y procesa los valores en lotes, mientras que SQLite debe recorrer filas completas para llegar a un campo. Para obtener una sola fila por 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 aumentar 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 un calendario para períodos 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 después de escribirse, por lo que basta con copiar el directorio.

#duckdb#sqlite#database#analytics#parquet#self-hosting