Semestre 02, 2026
Cincuenta gigabytes en Parquet no necesitan veinte máquinas, pero tampoco caben en un DataFrame de pandas. Ese es el territorio de DuckDB.
Sistema de gestión de bases de datos relacional, embebido y orientado al análisis, para ejecutar SQL sobre grandes volúmenes de datos en una sola máquina.
No hay servidor que instalar, ni puerto que abrir, ni credenciales que administrar. La base de datos es una biblioteca que se carga dentro del programa.
Cuando el motor vive en el mismo proceso que el análisis, los datos no viajan por un socket, así que una consulta puede leer un DataFrame de Python sin copiarlo.
Online Transaction Processing, o procesamiento de transacciones en línea. Registra la operación diaria de un negocio, donde cada instrucción toca pocas filas y la prioridad es escribir de forma segura y concurrente. Corresponde a PostgreSQL, MySQL u Oracle.
Online Analytical Processing, o procesamiento analítico en línea. Responde preguntas sobre el histórico completo, donde cada consulta recorre millones de filas y agrega columnas enteras. Corresponde a DuckDB, BigQuery o Snowflake.
| Aspecto | OLTP | OLAP |
|---|---|---|
| Carga típica | Muchas operaciones pequeñas | Pocas consultas que recorren todo |
| Acceso | Fila completa por identificador | Pocas columnas, millones de filas |
| Almacenamiento | Por filas | Por columnas |
| Optimizado para | Escritura concurrente | Lectura y agregación |
Promediar una columna en un almacén por filas obliga a leer todas las columnas de todas las filas. En un almacén por columnas se lee únicamente esa columna.
Un motor tradicional procesa una fila a la vez. DuckDB procesa vectores de unos dos mil valores de la misma columna.
Una agregación sobre cien millones de filas termina en segundos en una laptop.
brew install duckdb
pip install duckdb
La serie 1.5 lleva el nombre Variegata y la serie 1.4 es la de soporte extendido. La versión 2.0, prevista para finales de 2026, introduce entrada y salida asíncrona para Parquet y CSV.
import duckdb
con = duckdb.connect()
con = duckdb.connect("ventas.duckdb")
SELECT * FROM 'ventas.csv';
SELECT * FROM 'ventas.parquet';
SELECT * FROM read_csv('ventas.csv', header = true);
DuckDB reconoce la extensión, infiere los tipos y trata el archivo como una tabla. No hace falta declarar el esquema.
Los mecanismos que siguen permiten consultar conjuntos de datos mucho más grandes que la memoria disponible en la máquina.
CREATE VIEW ventas AS
SELECT * FROM read_parquet('datos/ventas.parquet');
Cada consulta contra la vista lee el archivo en el momento, así que un archivo de doscientos gigabytes se consulta sin ocupar doscientos gigabytes adicionales.
SELECT region, SUM(monto)
FROM 'ventas.parquet'
WHERE anio = 2025
GROUP BY region;
De un archivo de cincuenta columnas y mil millones de filas, esta consulta puede terminar leyendo menos del dos por ciento de los bytes.
SELECT COUNT(*) FROM 'datos/ventas/*.parquet';
SELECT * FROM read_parquet(['enero.parquet', 'febrero.parquet']);
SELECT filename, COUNT(*)
FROM 'datos/ventas/*.parquet'
GROUP BY filename;
La columna virtual filename indica el archivo de origen de cada fila y está disponible sin configuración desde la versión 1.3.
Los valores de una columna se codifican en los nombres de los directorios, con el formato clave igual valor.
ventas/
├── anio=2024/
│ ├── mes=1/datos.parquet
│ └── mes=2/datos.parquet
└── anio=2025/
├── mes=1/datos.parquet
└── mes=2/datos.parquet
SELECT SUM(monto)
FROM read_parquet(
'ventas/**/*.parquet',
hive_partitioning = true
)
WHERE anio = 2025 AND mes = 1;
SELECT SUM(monto)
FROM read_parquet(
'ventas/**/*.parquet',
hive_partitioning = true
)
WHERE anio = 2025 AND mes = 1;
El patrón de dos asteriscos recorre el árbol completo de directorios y reúne todos los archivos Parquet que encuentra en una sola tabla lógica.
SELECT SUM(monto)
FROM read_parquet(
'ventas/**/*.parquet',
hive_partitioning = true
)
WHERE anio = 2025 AND mes = 1;
Los nombres de directorio se convierten en columnas consultables. Las columnas anio y mes no existen dentro de los archivos, se deducen de la ruta.
SELECT SUM(monto)
FROM read_parquet(
'ventas/**/*.parquet',
hive_partitioning = true
)
WHERE anio = 2025 AND mes = 1;
El filtro se aplica sobre las columnas de partición antes de abrir un solo archivo. De los cuatro del ejemplo se abre uno, y con particiones diarias se lee un archivo en lugar de trescientos sesenta y cinco.
COPY ventas TO 'salida'
(FORMAT parquet, PARTITION_BY (anio, mes));
PARTITION_BY no admite expresiones, así que una partición derivada de una fecha requiere crear antes la columna correspondiente.
Agrupar mil millones de filas construye una tabla de hash que puede no caber en memoria. En pandas eso termina en un error y el trabajo se pierde. DuckDB escribe el exceso a un archivo temporal y continúa.
| Operador | Vuelca a disco | Ejemplo |
|---|---|---|
| Agrupación | Sí | GROUP BY cliente |
| Unión | Sí | JOIN por llave hash |
| Orden | Sí | ORDER BY fecha |
| Ventana | Sí | OVER (PARTITION BY region) |
La consulta tarda más, pero termina. Esa es la diferencia entre un análisis entregado y un proceso que muere a la mitad.
SET memory_limit = '8GB';
SET temp_directory = '/ruta/disco/rapido/tmp';
SET threads = 8;
SET preserve_insertion_order = false;
Pedir menos columnas, filtrar lo antes posible y particionar los datos de forma razonable.
INSTALL httpfs;
LOAD httpfs;
SELECT COUNT(*) FROM read_parquet('s3://bucket/ventas/*.parquet');
Permite consultar archivos en S3 o en cualquier servidor HTTP. La proyección y el filtro siguen aplicando, así que se descargan únicamente los fragmentos necesarios.
DuckDB detecta los nombres que no existen en su catálogo y los busca entre las variables del programa.
import duckdb
import pandas as pd
ventas = pd.read_csv("ventas.csv")
resultado = duckdb.sql("""
SELECT region, SUM(monto) AS total
FROM ventas GROUP BY region ORDER BY total DESC
""").df()
| Método | Devuelve |
|---|---|
| .df() | DataFrame de pandas |
| .arrow() | Tabla de Apache Arrow |
| .pl() | DataFrame de Polars |
| .fetchall() | Lista de tuplas de Python |
El DataFrame se lee sin copiarlo, y el resultado de una consulta se puede escribir directamente a Parquet o CSV con COPY.
| Aspecto | pandas | DuckDB | Spark |
|---|---|---|---|
| Modelo | En memoria | Mayor que la memoria | Distribuido |
| Infraestructura | Ninguna | Ninguna | Clúster |
| Lenguaje | API de Python | SQL | SQL y varias API |
| Unión de cien millones de filas | Suele fallar | Resuelve en disco | Resuelve en el clúster |
| Costo de operación | Nulo | Nulo | Alto |
| Volumen | Herramienta razonable |
|---|---|
| Menos de un gigabyte | pandas, por comodidad |
| Un gigabyte a un terabyte | DuckDB en una máquina |
| Decenas de terabytes o más | Spark en un clúster |
El error habitual consiste en levantar un clúster para un problema de cincuenta gigabytes. Conviene empezar por la máquina completa y recurrir al clúster solo cuando esa máquina ya no alcanza.