Saltar a contenido

05 · Parquet y Hive partitioning

Cuaderno asociado

cuadernos/04_parquet_hive.ipynb. Volver al 00 - Índice de la demo.

Parquet es el formato donde DuckDB despliega todo su potencial: lee solo las columnas y los row groups que necesita, y con la 2.0 además lo hace con E/S asíncrona.

Cómo se organiza un dataset Hive

flowchart TD
    ROOT["datos/ventas_hive/"]
    ROOT --> Y24["year=2024/"]
    ROOT --> Y25["year=2025/"]
    Y24 --> M241["month=11/"]
    Y24 --> M242["month=12/"]
    Y25 --> M251["month=01/"]
    M241 --> F1["part-0.parquet"]
    M242 --> F2["part-0.parquet"]
    M251 --> F3["part-0.parquet"]

    subgraph lectura["Lectura de datos con glob **"]
        direction TB
        G["read_parquet('.../**/*.parquet')"]
        G --> P1["Detecta year y month<br/>como columnas virtuales"]
        G --> P2["Pruning: si filtro<br/>year=2025, no abre 2024/"]
    end

La clave: el nombre de la carpeta es el dato. year=2024 no es un directorio, es la columna year con valor 2024.

Leer particiones

-- Con '**' en el glob, la detección de particiones es automática
SELECT year, month, count(*) AS filas, sum(importe) AS total
FROM read_parquet('datos/ventas_hive/**/*.parquet')
GROUP BY ALL
ORDER BY ALL;

A tener en cuenta

Con ** en la ruta, DuckDB 2.0 activa hive partitioning por defecto. Añadir hive_partitioning = true es explícito y no molesta; olvidarlo cuando usas * en un solo nivel falla.

Saber de qué fichero viene cada fila

SELECT year, month, id, filename
FROM read_parquet('datos/ventas_hive/**/*.parquet', filename = true)
LIMIT 5;

_metadata.file_path no está disponible en esta compilación

En DuckDB 2.0.0 la forma estable es la opción filename = true, que añade una columna filename. La ruta tipo _metadata.file_path provoca Binder Error: Referenced table "_metadata" not found. Usa filename.

Escribir particiones

COPY (SELECT * FROM ventas)
TO 'datos/ventas_hive'
(FORMAT PARQUET, PARTITION_BY (year, month), OVERWRITE_OR_IGNORE, COMPRESSION zstd);

DuckDB crea el árbol year=.../month=.../ y escribe un fichero por combinación. Es el patrón lakehouse mínimo viable, sin Spark ni nada parecido.

Inspeccionar el fichero, no solo los datos

SELECT row_group_id, row_group_num_rows, path_in_schema, total_compressed_size
FROM parquet_metadata('datos/ventas_hive/year=2025/month=01/part-0.parquet');

SELECT num_rows, format_version, created_by
FROM parquet_file_metadata('datos/ventas_hive/year=2025/month=01/part-0.parquet');

Esto es oro para depurar por qué una consulta va lenta: si hay un row group gigante o una columna inflada, lo ves aquí.

¿Por qué va rápido: el pushdown?

sequenceDiagram
    participant U as Usuario
    participant D as DuckDB
    participant P as Fichero Parquet
    U->>D: SELECT producto, sum(importe) WHERE year=2025
    D->>D: Lee metadatos (min/max por row group)
    D->>P: Descarta row groups de 2024
    D->>P: Lee SOLO columnas producto e importe
    P-->>D: Páginas comprimidas necesarias
    D->>D: Agrega (posible spilling a disco)
    D-->>U: Resultado

Tres capas de descarte: partición (carpeta), row group (estadísticas) y columna (layout columnar). Si tu consulta no mejora, revisa el orden: filtrar por la columna particionada es lo más barato.

La 2.0 añade E/S asíncrona

La concurrencia de E/S se separa de la de procesamiento: mientras se procesan páginas ya recibidas, hay más lecturas en vuelo. En pruebas sobre Overture Maps en S3, la mejora medida fue de ~3× frente a la 1.5. En local se nota menos; en almacenamiento de objetos es donde muerde.

Ejercicios

  1. Crea un dataset particionado por year y month con 2 años × 2 meses y compruébalo con filename = true.
  2. Consulta solo year=2025 y explica cómo saber que DuckDB no leyó 2024 (pista: EXPLAIN ANALYZE).
  3. Compara tamaño en disco con COMPRESSION snappy y zstd.
  4. ¿Por qué SELECT * sobre un Parquet de 200 columnas es peor que seleccionar 3, si el formato es columnar?

Soluciones en cuadernos/99_soluciones.ipynb.