Saltar a contenido

04 · CSV y JSON

Cuaderno asociado

cuadernos/03_csv_json.ipynb. Volver al 00 - Índice de la demo.

El 80 % de los datos «sucios» que llegan a un equipo de datos son CSV mal formados y/o JSON anidado. DuckDB ataca ambos sin cargar nada en memoria de Python.

flowchart LR
    subgraph origen["Orígenes"]
        C1["CSV / TSV<br/>delimitadores raros"]
        C2["JSON array"]
        C3["NDJSON<br/>una línea = un objeto"]
    end
    subgraph duck["DuckDB"]
        R1["read_csv / read_csv_auto"]
        R2["read_json_auto"]
        S["sniff_csv"]
        V["::VARIANT"]
    end
    C1 --> R1
    C2 --> R2
    C3 --> R2
    C1 -.->|"Inferir esquema"| S
    R2 -.->|"tipado opcional"| V
    R1 --> T[("Tabla / Parquet")]
    R2 --> T
    V --> T
    linkStyle 3 stroke:red,stroke-width:3px;

CSV: inferencia de metadatos -> control

-- 1. Explorar: ¿qué infiere el motor?
FROM sniff_csv('datos/transacciones.csv');

-- 2. Consultar directamente
SELECT * FROM read_csv_auto('datos/transacciones.csv') LIMIT 5;

-- 3. Fijar el esquema (lo que harás en producción)
SELECT *
FROM read_csv('datos/transacciones.csv',
              header = true,
              delim  = ',',
              columns = {'fecha': 'DATE', 'monto': 'DOUBLE', 'categoria': 'VARCHAR'});

read_csv_auto para explorar, read_csv con esquema explícito para producción. La inferencia falla con columnas vacías, ceros iniciales y fechas en formatos mixtos; el esquema explícito no.

Unir ficheros con columnas distintas

SELECT * FROM read_csv(['datos/2024/*.csv', 'datos/2025/*.csv'],
                       header = true,
                       union_by_name = true);

union_by_name alinea por nombre de columna en vez de por posición: imprescindible cuando el proveedor añade una columna a mitad de año.

Cuando el CSV está roto 🤯🤯

Síntoma Opción
Líneas con más/menos columnas ignore_errors = true (descarta) o null_padding = true (rellena)
Codificación no UTF-8 Volver a codificar antes; encoding cubre latin-1
Decimales con coma delim = ';' y decimal_separator = ','
Comentarios y cabeceras repetidas comment = '#', skip = N

JSON: dos formatos, dos estrategias

-- NDJSON (una línea = un documento) y arrays: el mismo lector los detecta
SELECT * FROM read_json_auto('datos/usuarios.json');

Acceso a campos anidados

SELECT
    id,
    direccion.ciudad        AS ciudad,      -- acceso por punto
    habilidades[1]          AS primera,     -- LIST 1-indexado
    len(habilidades)        AS n_habilidades
FROM read_json_auto('datos/usuarios.json');

De JSON-texto a VARIANT

Si el esquema varía entre filas, no forzar columnas: convertir a tipo VARIANT y consultar de forma flexible.

SELECT payload.action::VARCHAR AS accion,
       variant_typeof(payload) AS tipo
FROM read_json_auto('datos/eventos.jsonl');

→ Más en 08 - VARIANT.

Errores en el cuaderno

  • read_json_auto(..., auto_expand=false) no existe en 2.0.0: ese parámetro fue eliminado. El acceso por punto sobre el STRUCT ya resuelve el caso.
  • read_json(..., format='array') falla si el fichero es NDJSON: «Expected top-level JSON array». Usa format='auto' o read_json_auto.

JSON grande: evitar leer múltiples veces!!

-- Materializa una vez y explota el coste
CREATE TABLE eventos AS SELECT * FROM read_json_auto('datos/eventos.jsonl');

-- A partir de aquí, comprime y vuela
COPY eventos TO 'datos/eventos.parquet' (FORMAT PARQUET, COMPRESSION zstd);

Este es el patrón que conecta con 05 - Parquet y Hive partitioning: JSON para ingerir, Parquet para analizar.

Ejercicios

  1. Generar un CSV con una fila corrupta (una coma de más) y consigue leerlo sin que falle la consulta. ¿Qué filas pierdes?
  2. Leer un NDJSON donde unas filas traen "precio": 10 y otras "precio": "10.5". ¿Qué infiere read_json_auto? Fuerza el tipo.
  3. Extraer la ciudad y el número de habilidades de usuarios.json en una sola consulta.
  4. Convertir eventos.jsonl a Parquet comprimido y compara el tamaño en disco.

Soluciones en cuadernos/99_soluciones.ipynb.