← CC3084

DuckDB

Semestre 02, 2026

De dónde venimos

  • Hadoop distribuye bloques replicados sobre HDFS y coordina recursos con YARN.
  • Spark mantiene los resultados intermedios en memoria y organiza el cómputo como un grafo dirigido acíclico.
  • Ambos exigen un clúster, un administrador de recursos y un costo de operación permanente.
La franja intermedia

Cincuenta gigabytes en Parquet no necesitan veinte máquinas, pero tampoco caben en un DataFrame de pandas. Ese es el territorio de DuckDB.

DuckDB

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.

Corre dentro del proceso que lo usa, sin servidor
Almacena y procesa por columnas
Procesa datos mayores que la memoria
Consulta Parquet y CSV en su lugar

Características

  • Embebido, en el sentido de que no corre como proceso separado sino dentro del programa que lo invoca.
  • Analítico, construido para consultas que recorren millones de filas y agregan columnas completas.
  • Sin dependencias externas, se compila en cualquier sistema operativo y hasta en el navegador.
  • Transaccional, con garantías ACID mediante control de concurrencia multiversión.
  • Portable, con interfaces para Python, R, Java, C, C++, Go, Rust y Node.js.
  • Libre, bajo licencia MIT y con la propiedad intelectual en la DuckDB Foundation.

Ser embebido

Sin infraestructura

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.

Sin transferencia

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.

OLTP y OLAP

OLTP

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.

OLAP

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.

Comparación de OLTP y OLAP

AspectoOLTPOLAP
Carga típicaMuchas operaciones pequeñasPocas consultas que recorren todo
AccesoFila completa por identificadorPocas columnas, millones de filas
AlmacenamientoPor filasPor columnas
Optimizado paraEscritura concurrenteLectura y agregación
La diferencia que importa

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.

Motor vectorizado por columnas

Un motor tradicional procesa una fila a la vez. DuckDB procesa vectores de unos dos mil valores de la misma columna.

  • El costo de interpretar la operación se reparte entre miles de valores en lugar de pagarse por fila.
  • Los valores de una columna comparten tipo y quedan contiguos en memoria, lo que aprovecha la caché del procesador.
  • El trabajo se reparte por porciones del archivo, así que todos los núcleos atienden la misma consulta.
Consecuencia

Una agregación sobre cien millones de filas termina en segundos en una laptop.

Instalación


brew install duckdb

pip install duckdb
Versiones

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.

Modos de trabajo


import duckdb

con = duckdb.connect()

con = duckdb.connect("ventas.duckdb")
En memoria: exploración y consulta de archivos externos
Persistente: tablas y vistas en un solo archivo comprimido

SQL directamente sobre archivos


SELECT * FROM 'ventas.csv';

SELECT * FROM 'ventas.parquet';

SELECT * FROM read_csv('ventas.csv', header = true);
Sin carga previa

DuckDB reconoce la extensión, infiere los tipos y trata el archivo como una tabla. No hace falta declarar el esquema.

Grandes volúmenes de datos

El aporte central

Los mecanismos que siguen permiten consultar conjuntos de datos mucho más grandes que la memoria disponible en la máquina.

Consultar sin cargar
Proyección y filtro empujados
Particionado tipo Hive
Volcado a disco del estado intermedio

Consultar sin cargar


CREATE VIEW ventas AS
SELECT * FROM read_parquet('datos/ventas.parquet');
La vista no copia nada

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.

Proyección y filtro empujados

Proyección empujada: de ciento veinte columnas, una consulta que pide tres lee solo esas tres
Filtro empujado: Parquet guarda mínimo y máximo por grupo de filas y DuckDB descarta grupos completos

SELECT region, SUM(monto)
FROM 'ventas.parquet'
WHERE anio = 2025
GROUP BY region;
Efecto medible

De un archivo de cincuenta columnas y mil millones de filas, esta consulta puede terminar leyendo menos del dos por ciento de los bytes.

Varios archivos como una sola tabla


SELECT COUNT(*) FROM 'datos/ventas/*.parquet';

SELECT * FROM read_parquet(['enero.parquet', 'febrero.parquet']);

SELECT filename, COUNT(*)
FROM 'datos/ventas/*.parquet'
GROUP BY filename;
Mil archivos, una tabla

La columna virtual filename indica el archivo de origen de cada fila y está disponible sin configuración desde la versión 1.3.

Particionado tipo Hive

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

Poda de particiones


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;
'ventas/**/*.parquet'

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;
hive_partitioning = true

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;
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.

Escribir datos particionados


COPY ventas TO 'salida'
    (FORMAT parquet, PARTITION_BY (anio, mes));
Restricción

PARTITION_BY no admite expresiones, así que una partición derivada de una fecha requiere crear antes la columna correspondiente.

Procesamiento mayor que la memoria

El escenario

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.

OperadorVuelca a discoEjemplo
AgrupaciónSíGROUP BY cliente
UniónSíJOIN por llave hash
OrdenSíORDER BY fecha
VentanaSíOVER (PARTITION BY region)

Degradación gradual

  • El administrador de búferes trabaja por bloques y decide qué fragmentos del estado intermedio van a disco.
  • Usa particionamiento por radix, de modo que cada fragmento se puede procesar por separado al volver a memoria.
  • El rendimiento cae de forma progresiva en lugar de colapsar cuando la memoria se agota.
Lo que cambia en la práctica

La consulta tarda más, pero termina. Esa es la diferencia entre un análisis entregado y un proceso que muere a la mitad.

Configuración de memoria


SET memory_limit = '8GB';
SET temp_directory = '/ruta/disco/rapido/tmp';
SET threads = 8;
SET preserve_insertion_order = false;
  • memory_limit: tope de memoria, por omisión el ochenta por ciento de la memoria física.
  • temp_directory: dónde se escribe el volcado, idealmente un disco de estado sólido local.
  • threads: límite de hilos, útil cuando demasiada concurrencia degrada el rendimiento.
  • preserve_insertion_order: permite reordenar el resultado y reduce el consumo al importar y exportar.

Límites del volcado a disco

  • Las agregaciones que acumulan colecciones, como list y string_agg, mantienen todo su estado en memoria.
  • Las agregaciones que requieren orden necesitan el conjunto completo antes de empezar.
  • Una consulta con varios operadores bloqueantes simultáneos todavía puede agotar la memoria.
Medida preventiva

Pedir menos columnas, filtrar lo antes posible y particionar los datos de forma razonable.

Datos remotos


INSTALL httpfs;
LOAD httpfs;

SELECT COUNT(*) FROM read_parquet('s3://bucket/ventas/*.parquet');
httpfs

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.

Sustitución de referencias

Replacement scan

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()

Interoperabilidad

MétodoDevuelve
.df()DataFrame de pandas
.arrow()Tabla de Apache Arrow
.pl()DataFrame de Polars
.fetchall()Lista de tuplas de Python
En ambas direcciones

El DataFrame se lee sin copiarlo, y el resultado de una consulta se puede escribir directamente a Parquet o CSV con COPY.

DuckDB frente a otras herramientas

AspectopandasDuckDBSpark
ModeloEn memoriaMayor que la memoriaDistribuido
InfraestructuraNingunaNingunaClúster
LenguajeAPI de PythonSQLSQL y varias API
Unión de cien millones de filasSuele fallarResuelve en discoResuelve en el clúster
Costo de operaciónNuloNuloAlto

La escala decide la herramienta

VolumenHerramienta razonable
Menos de un gigabytepandas, por comodidad
Un gigabyte a un terabyteDuckDB en una máquina
Decenas de terabytes o másSpark en un clúster
Regla práctica

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.

Rendimiento reportado

  • Entre cinco y diez veces más rápido que pandas en agrupaciones sobre más de cien millones de filas.
  • Resuelve uniones de cien millones de filas en memoria o en disco, donde pandas suele agotar la memoria.
  • Spark en modo local pierde frente a DuckDB, porque paga la coordinación de un clúster sin tener uno.

Cuándo no usar DuckDB

Cargas transaccionales con escritura concurrente intensa
Un archivo de base de datos admite un proceso escritor a la vez
Decenas de terabytes con crecimiento sostenido
Servicio de consultas a muchos usuarios simultáneos

Errores comunes

  • Cargar todo a una tabla antes de consultar, cuando la vista sobre Parquet basta y evita duplicar el almacenamiento.
  • Filtrar después de leer, que obliga a abrir las trescientas sesenta y cinco particiones para conservar una.
  • Particionar en exceso, porque miles de archivos diminutos degradan la lectura más de lo que ayuda la poda.

Errores comunes

  • Dejar el directorio temporal en un disco lento, lo que convierte el volcado en el cuello de botella.
  • Usar CSV para datos grandes, ya que sin mapas de zonas ni compresión por columna desaparecen las optimizaciones.
  • Esperar que list y string_agg vuelquen a disco, cuando su estado permanece completo en memoria.

Resumen

  • DuckDB es una base de datos analítica embebida que corre dentro del proceso que la usa, sin servidor ni administración.
  • Su almacenamiento por columnas y su ejecución vectorizada recorren millones de filas y agregan columnas completas.
  • Consulta archivos Parquet y CSV en su lugar, mediante vistas y patrones glob.
  • La proyección y el filtro empujados reducen la lectura a las columnas y los grupos de filas necesarios.

Resumen

  • El particionado tipo Hive traslada el filtro al sistema de archivos, así que las particiones irrelevantes nunca se abren.
  • La agrupación, la unión, el orden y las funciones de ventana vuelcan a disco cuando el estado intermedio excede la memoria.
  • memory_limit, temp_directory y threads gobiernan ese comportamiento, con un tope por omisión del ochenta por ciento de la memoria física.
  • En la franja de un gigabyte a un terabyte supera a pandas y evita el costo de un clúster de Spark.