Rendimiento
DuckDB es rapido "de serie" en analitica de un solo nodo, pero el orden de magnitud depende de formato, predicados, tipos, paralelismo y si materializas o relees bruto cada vez. Este capitulo da palancas concretas y como medir.
Modelo mental
- Columnar: cuanto menos columnas leas, mejor.
- Vectorizado: procesa lotes; evita UDFs fila a fila en Python.
- Un proceso: threads y RAM de esta maquina; no hay shuffle entre nodos.
- Late materialization: filtros y proyecciones deben empujarse a Parquet/CSV readers.
Si tu cuello de botella es red (S3) o disco, optimizar solo el SQL CPU no basta.
Medir antes de opinar
EXPLAIN
SELECT pais, SUM(importe)
FROM read_parquet('lake/pedidos/**/*.parquet', hive_partitioning = true)
WHERE fecha = DATE '2026-01-02'
GROUP BY pais;
EXPLAIN ANALYZE
SELECT pais, SUM(importe)
FROM pedidos
WHERE fecha = DATE '2026-01-02'
GROUP BY pais;En Python:
import time
import duckdb
con = duckdb.connect()
t0 = time.perf_counter()
con.execute("""
SELECT pais, SUM(importe)
FROM read_parquet('pedidos.parquet')
GROUP BY pais
""").fetchall()
print("segundos:", time.perf_counter() - t0)Cambia una variable por prueba: formato, filtro, columnas, threads.
Threads y memoria
SET threads TO 8;
SET memory_limit = '8GB';
SET temp_directory = 'D:/tmp/duckdb'; -- spill a disco si hace faltacon.execute("SET threads TO 4")
con.execute("SET memory_limit = '4GB'")Guias:
threads: cerca del numero de cores fisicos utiles; subir sin medida no siempre ayuda (contencion, IO).memory_limit: deja margen al OS y a Pandas si conviven en el mismo proceso.- Si hay spill excesivo a
temp_directory, o reduces el working set o subes RAM / materializas menos ancho.
Proyeccion y predicados
Malo:
SELECT *
FROM read_parquet('s3://bucket/eventos/**/*.parquet')
-- luego filtras en PandasMejor:
SELECT user_id, event_type, ts
FROM read_parquet(
's3://bucket/eventos/**/*.parquet',
hive_partitioning = true
)
WHERE fecha = DATE '2026-01-02'
AND event_type = 'purchase';Checklist:
- Columnas explicitas en el
SELECT. - Filtros sobre particiones Hive primero (
fecha, ...). - Filtros selectivos tempranos en CTEs.
Tipos: baratos vs caros
| Tipo | Notas de rendimiento |
|---|---|
INTEGER / BIGINT | Ideales para ids y conteos |
DECIMAL(p,s) | Correcto para dinero; mas caro que DOUBLE |
DOUBLE | Rapido; no para dinero contable |
VARCHAR | Caro en agrupaciones masivas; normaliza claves |
DATE / TIMESTAMP | Mejor que strings de fecha |
-- Evita agrupar por string sucio
SELECT upper(trim(pais)) AS pais, SUM(importe)
FROM pedidos
GROUP BY 1;
-- Mejor: normaliza en staging una vezJoins
- Asegura tipos alineados a ambos lados (
VARCHARvsINTrompe o castiga). - Reduce filas antes del join con CTEs filtradas.
- Cuidado con explosiones many-to-many: valida
COUNT(*).
WITH pedidos_dia AS (
SELECT pedido_id, customer_id, importe
FROM pedidos
WHERE fecha = DATE '2026-01-02'
),
clientes_es AS (
SELECT customer_id, segmento
FROM clientes
WHERE pais = 'ES'
)
SELECT c.segmento, SUM(p.importe) AS total
FROM pedidos_dia p
JOIN clientes_es c USING (customer_id)
GROUP BY 1;Agregaciones y ventanas
- Ventanas con
PARTITION BYde alta cardinalidad + orden amplio pueden ser caras: acota el set primero. - Para top-N,
QUALIFY ROW_NUMBER() ... <= Nsuele ser claro y eficiente. - Prefiere
SUM(...) FILTER (WHERE ...)a multiples scans cuando aplica.
Preparar datos: Parquet bien hecho
Factores que mas mueven el aguja:
- Parquet vs CSV: Parquet gana en casi cualquier agregado repetido.
- Tamano de archivo: evita miles de ficheros de pocos KB; apunta a decenas/centenas de MB por archivo (orden de magnitud; depende del lake).
- Particiones: baja/media cardinalidad (
fecha,pais), nouser_id. - Row group / compresion: Snappy o ZSTD; no regeneres el lake entero por micro-ajustes sin medir.
- Estadisticas: Parquet con min/max ayuda al skip de row groups.
COPY (
SELECT * FROM stg_pedidos
) TO 'lake/pedidos'
(
FORMAT PARQUET,
PARTITION_BY (fecha),
COMPRESSION 'ZSTD',
OVERWRITE_OR_IGNORE
);Indexes y tablas persistentes
DuckDB soporta indexes art (y variantes segun version) para busquedas puntuales en tablas persistentes. Para scans analiticos anchos sobre Parquet, el diseno columnar + particiones suele importar mas que indexar como en OLTP.
CREATE TABLE hechos AS SELECT * FROM read_parquet('lake/pedidos/**/*.parquet');
-- Consultas repetidas sobre el mismo subset: materializa marts
CREATE TABLE mart_diario AS
SELECT fecha, pais, SUM(importe) AS total
FROM hechos
GROUP BY 1, 2;Materializar un mart pequeno usado cien veces casi siempre gana a releer raw.
Evitar el puente lento con Python
Anti-patron:
df = con.execute("SELECT * FROM grandes").df()
for _, row in df.iterrows(): # muerte por mil cortes
...Patron:
# Deja el trabajo en SQL
res = con.execute("""
SELECT pais, SUM(importe) AS total
FROM grandes
GROUP BY pais
""").df() # resultado pequenoSi necesitas UDF, valora si existe funcion SQL nativa. Las UDFs Python rompen el pipeline vectorizado.
Profiling rapido de IO vs CPU
Senales:
- CPU baja + disco/red altos -> formato, globs, small files, S3.
- CPU al 100% en todos los cores -> quizas el plan es CPU-bound (joins, hashes); revisa cardinalidad y tipos.
- Un solo core al 100% -> poco paralelismo o etapa serial; revisa
threadsy el plan.
Errores habituales
- Optimizar SQL y seguir leyendo CSV gigante cada vez.
SELECT *+ filtrar en Pandas "porque es mas comodo".- Particionar por columna casi unica (millones de carpetas).
- Subir
threadsa 128 en una maquina de 8 cores y declarar victoria. - Materializar tablas anchas innecesarias ocupando disco y cache.
- Comparar tiempos en caliente vs frio (cache OS/S3) sin control.
Buenas practicas
- Mide con
EXPLAIN ANALYZEy timers simples. - Normaliza a Parquet particionado en staging.
- Proyecta columnas y filtra temprano.
- Materializa marts de consultas calientes.
- Manten ficheros Parquet de tamano razonable.
- Deja la agregacion en DuckDB; Python solo orquesta.
- Ajusta
memory_limitytemp_directoryen jobs batch largos.
Ejercicios
- Toma un CSV y el mismo contenido en Parquet; compara el tiempo de un
GROUP BYidentico. - Ejecuta la misma agregacion con
SELECT *subquery vs columnas explicitas; mira el plan. - Prueba
SET threads TO 1vs4vs8en un agregado local grande. - Crea un mart diario materializado y compara N consultas al mart vs N al raw.
- Genera a proposito muchos Parquet diminutos y luego unifica a menos archivos; vuelve a medir.
Siguiente paso
Cierra el manual con Buenas practicas: checklist operativo, limites del motor y como encajar DuckDB en un stack real.
