4. Archivos
Secciones
Ajustes de lectura
Voz natural grabada para este curso, la misma en todos los navegadores.
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Hasta ahora, DuckDB leía un archivo JSON cada vez. Esta lección escribe y lee los demás formatos, y varios archivos a la vez. Las consultas están en sql/04-files.sql. Escriben en out/, una carpeta que check.sh crea y que Git ignora; créala a mano antes de ejecutarlas tú mismo.
De sql/04-files.sql, líneas 3-4:
CREATE TABLE runs AS FROM 'data/runs.json';CREATE TABLE jobs AS FROM 'data/jobs.json';CSV: lo que adivina el sniffer
Sección titulada «CSV: lo que adivina el sniffer»CSV no tiene tipos, ni delimitador estándar, ni formato de fecha estándar. Para tener un caso difícil, el script escribe las ejecuciones como las guarda una hoja de cálculo con configuración regional francesa: punto y coma, y el día antes del mes.
De sql/04-files.sql, líneas 7-8:
COPY (SELECT databaseId, workflowName, conclusion, createdAt FROM runs ORDER BY createdAt)TO 'out/runs-fr.csv' (DELIMITER ';', TIMESTAMPFORMAT '%d/%m/%Y %H:%M');databaseId;workflowName;conclusion;createdAt34766934801;Deploy to GitHub Pages;success;13/09/2026 15:5334767508304;Deploy to GitHub Pages;success;13/09/2026 16:05Cuando DuckDB lee un archivo CSV, el sniffer de CSV lee una muestra y adivina el dialecto y los tipos. sniff_csv muestra lo que adivina sin leer el archivo (líneas 11-12):
SELECT Delimiter, HasHeader, c.name, c.typeFROM sniff_csv('out/runs-fr.csv'), unnest(Columns) AS t(c);┌───────────┬───────────┬──────────────┬─────────┐│ Delimiter │ HasHeader │ name │ type ││ varchar │ boolean │ varchar │ varchar │├───────────┼───────────┼──────────────┼─────────┤│ ; │ true │ databaseId │ BIGINT ││ ; │ true │ workflowName │ VARCHAR ││ ; │ true │ conclusion │ VARCHAR ││ ; │ true │ createdAt │ VARCHAR │└───────────┴───────────┴──────────────┴─────────┘El delimitador, la cabecera y la columna de enteros son correctos. createdAt es VARCHAR: 13/09/2026 no es un formato de fecha que el sniffer pruebe. Esto no es un error. El archivo se carga, y toda comparación de fechas sobre esa columna pasa a ser una comparación de cadenas: min(createdAt) devuelve '13/09/2026 15:53', lo que ordenaría 01/10 antes que 30/09 en cuanto los datos abarquen dos meses.
sniff_csv también devuelve, en su columna Prompt, la llamada a read_csv que reproduce lo que adivinó, un punto de partida para corregir a mano:
FROM read_csv('out/runs-fr.csv', auto_detect=false, delim=';', quote='', escape='', new_line='\n', skip=0, comment='', header=true, columns={'databaseId': 'BIGINT', 'workflowName': 'VARCHAR', 'conclusion': 'VARCHAR', 'createdAt': 'VARCHAR'});Aquí la corrección es darle el formato a read_csv (líneas 15-16):
SELECT typeof(min(createdAt)) AS type, min(createdAt) AS first_run, max(createdAt) AS last_runFROM read_csv('out/runs-fr.csv', timestampformat = '%d/%m/%Y %H:%M');┌───────────┬─────────────────────┬─────────────────────┐│ type │ first_run │ last_run ││ varchar │ timestamp │ timestamp │├───────────┼─────────────────────┼─────────────────────┤│ TIMESTAMP │ 2026-09-13 15:53:00 │ 2026-09-14 14:01:00 │└───────────┴─────────────────────┴─────────────────────┘Un formato incorrecto falla de la misma forma silenciosa: con timestampformat = '%m/%d/%Y %H:%M' (el mes primero), la columna vuelve a ser VARCHAR, sin ningún mensaje. Para convertir una suposición equivocada en un error, indica también el tipo, con types = {'createdAt': 'TIMESTAMP'}:
Conversion Error: CSV Error on Line: 2Original Line: 34766934801;Deploy to GitHub Pages;success;13/09/2026 15:53Error when converting column "createdAt". Could not convert string "13/09/2026 15:53" to 'TIMESTAMP'Para un archivo que llega todos los días, se aplica la costumbre de SQL Server del archivo de formato: escribe una sola vez la llamada a read_csv con columns o types, y no dejes que el sniffer vuelva a decidir cada vez.
Parquet: tipos y columnas
Sección titulada «Parquet: tipos y columnas»Parquet almacena una tabla por columnas, comprimida y con su esquema. El archivo JSON de los jobs ocupa 526.328 bytes; en Parquet ocupa 37.270, 14 veces menos (línea 19):
COPY jobs TO 'out/jobs.parquet';parquet_metadata muestra lo que hay dentro, una fila por bloque de columna (column chunk) (líneas 20-22):
SELECT path_in_schema, type, num_values, compressionFROM parquet_metadata('out/jobs.parquet')ORDER BY column_id;┌────────────────────────────────────┬────────────┬────────────┬─────────────┐│ path_in_schema │ type │ num_values │ compression ││ varchar │ varchar │ int64 │ varchar │├────────────────────────────────────┼────────────┼────────────┼─────────────┤│ id │ INT64 │ 315 │ SNAPPY ││ run_id │ INT64 │ 315 │ SNAPPY ││ name │ BYTE_ARRAY │ 315 │ SNAPPY ││ status │ BYTE_ARRAY │ 315 │ SNAPPY ││ conclusion │ BYTE_ARRAY │ 315 │ SNAPPY ││ created_at │ INT64 │ 315 │ SNAPPY ││ started_at │ INT64 │ 315 │ SNAPPY ││ completed_at │ INT64 │ 315 │ SNAPPY ││ runner_name │ BYTE_ARRAY │ 315 │ SNAPPY ││ labels, list, element │ BYTE_ARRAY │ 315 │ SNAPPY ││ steps, list, element, number │ INT64 │ 2208 │ SNAPPY ││ steps, list, element, name │ BYTE_ARRAY │ 2208 │ SNAPPY ││ steps, list, element, conclusion │ BYTE_ARRAY │ 2208 │ SNAPPY ││ steps, list, element, started_at │ INT64 │ 2208 │ SNAPPY ││ steps, list, element, completed_at │ INT64 │ 2208 │ SNAPPY │└────────────────────────────────────┴────────────┴────────────┴─────────────┘- Cada campo del struct de un step es una columna propia. Una consulta sobre los nombres de los steps lee el bloque
steps, list, element, namey se salta las marcas de tiempo: eso es lo que aporta el almacenamiento «columnar», y la lección 7 lo mide. - Las marcas de tiempo son
INT64: un número de microsegundos, con el tipo lógico en el esquema. Las cadenas sonBYTE_ARRAY. - 2.208 valores para 2.204 steps: los 4 jobs de la lección 3 con una lista
stepsvacía siguen ocupando una entrada cada uno, para registrar que la lista está vacía. - La compresión es Snappy por defecto;
COPY … (FORMAT parquet, COMPRESSION zstd)elige otra, y reduce este archivo a 24.243 bytes.
Al principio el script también comparaba total_compressed_size. Pasaba en Linux y Windows, y fallaba en el runner de macOS, una máquina arm64, donde 8 de los 15 bloques de columna salían con unos pocos bytes de diferencia: id 1.652 bytes en lugar de 1.651, name 1.240 en lugar de 1.235, las horas de inicio de los steps 8.862 en lugar de 8.864. Mismos datos, misma versión de DuckDB, bytes distintos. La salida comprobada ahora solo conserva lo que no depende del compresor.
Varios archivos: particiones y globs
Sección titulada «Varios archivos: particiones y globs»PARTITION_BY escribe una carpeta por valor, llamada column=value, la organización conocida como particionado Hive (línea 25):
COPY (SELECT *, labels[1] AS os FROM jobs) TO 'out/jobs-by-os' (FORMAT parquet, PARTITION_BY (os), OVERWRITE);Tamaños en Windows:
out/jobs-by-os/├── os=macos-latest/data_0.parquet 9,651 bytes├── os=ubuntu-latest/data_0.parquet 27,631 bytes└── os=windows-latest/data_0.parquet 10,409 bytesSin OVERWRITE, una segunda ejecución del script falla, a diferencia de COPY a un único archivo, que lo reemplaza sin avisar (physical_copy_to_file.cpp):
IO Error: Directory "out/jobs-by-os" is not empty! Enable OVERWRITE option to overwrite filesUn glob lee todos los archivos como una sola tabla. La columna os no está almacenada en los archivos: DuckDB ve los nombres de carpeta key=value y vuelve a añadir la columna. filename = true añade la ruta del archivo de cada fila (líneas 28-31):
SELECT replace(filename, '\', '/') AS file, os, count(*) AS jobsFROM read_parquet('out/jobs-by-os/*/*.parquet', filename = true)GROUP BY ALLORDER BY file;┌─────────────────────────────────────────────────┬────────────────┬───────┐│ file │ os │ jobs ││ varchar │ varchar │ int64 │├─────────────────────────────────────────────────┼────────────────┼───────┤│ out/jobs-by-os/os=macos-latest/data_0.parquet │ macos-latest │ 34 ││ out/jobs-by-os/os=ubuntu-latest/data_0.parquet │ ubuntu-latest │ 246 ││ out/jobs-by-os/os=windows-latest/data_0.parquet │ windows-latest │ 35 │└─────────────────────────────────────────────────┴────────────────┴───────┘El replace está ahí por la CI: las rutas de archivo usan el separador del sistema operativo.
out\jobs-by-os\os=macos-latest\data_0.parquetout/jobs-by-os/os=macos-latest/data_0.parquetout/jobs-by-os/os=macos-latest/data_0.parquetEl propio patrón del glob, 'out/jobs-by-os/*/*.parquet', funciona con barras normales en todas partes, Windows incluido.
Archivos por HTTPS
Sección titulada «Archivos por HTTPS»Una URL es un nombre de archivo como cualquier otro. La copia de runs.json en GitHub (líneas 34-35):
SELECT count(*) AS runs, max(createdAt) AS last_runFROM 'https://raw.githubusercontent.com/spareilleux/learn/main/code/duckdb/data/runs.json';┌───────┬─────────────────────┐│ runs │ last_run ││ int64 │ timestamp │├───────┼─────────────────────┤│ 125 │ 2026-09-14 14:01:09 │└───────┴─────────────────────┘El soporte de HTTP es una extensión, httpfs, que no viene integrada en la CLI. Con los valores por defecto de autoinstall_known_extensions y autoload_known_extensions, la primera consulta que la necesita la descarga, y las siguientes la cargan. Se guarda en el perfil del usuario:
SELECT extension_name, install_path FROM duckdb_extensions() WHERE extension_name = 'httpfs';C:\Users\<you>\.duckdb\extensions\v1.5.5\windows_amd64\httpfs.duckdb_extension~/.duckdb/extensions/v1.5.5/linux_amd64/httpfs.duckdb_extensionPor verificar: la ruta en Linux, deducida de la de Windows.
~/.duckdb/extensions/v1.5.5/osx_arm64/httpfs.duckdb_extensionPor verificar: la ruta en macOS, deducida de la de Windows.
La CI de este curso la descarga en cada ejecución, en cada sistema operativo, sin ninguna configuración. Una máquina sin acceso a Internet no puede descargarla: ejecuta INSTALL httpfs de antemano donde haya red. Para Parquet por HTTP, DuckDB solo lee las partes del archivo que necesita, con peticiones de rango (range requests): la lección 7 cuenta los bytes.
Puntos clave
Sección titulada «Puntos clave»- El sniffer de CSV recurre a
VARCHARsin ningún mensaje; compruébalo consniff_csvoDESCRIBE, e indica los tipos de los archivos que lees con regularidad. - Parquet conserva los tipos y almacena cada columna por separado, campos anidados incluidos; es el formato en el que guardar los datos.
PARTITION_BYescribe carpetaskey=value, y un glob las vuelve a leer con la clave como columna.- Los nombres de archivo en los resultados usan el separador del sistema operativo; los patrones de glob aceptan
/en todas partes. - Las rutas
https://necesitanhttpfs, que la CLI instala la primera vez que se usa.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en sql/04-exercises.sql, que escribe los mismos archivos que la lección para poder ejecutarse por sí solo.
- El formato de hoja de cálculo de
out/runs-fr.csvperdió información. Vuelve a unir el archivo conrunsy cuenta las ejecuciones cuyocreatedAtcambió.
Solución
De sql/04-exercises.sql, líneas 10-12:
SELECT count(*) AS runs, count(*) FILTER (c.createdAt <> r.createdAt) AS different_timestampsFROM read_csv('out/runs-fr.csv', timestampformat = '%d/%m/%Y %H:%M') cJOIN runs r USING (databaseId);┌───────┬──────────────────────┐│ runs │ different_timestamps ││ int64 │ int64 │├───────┼──────────────────────┤│ 125 │ 125 │└───────┴──────────────────────┘Todas: %H:%M no tiene segundos, y ninguna de las 125 ejecuciones empezó en el segundo :00. La primera ejecución es 15:53:00 en el archivo y 15:53:16 en el JSON. Un viaje de ida y vuelta por un formato de hoja de cálculo pierde todo lo que el formato no puede escribir, otra vez en silencio. JOIN … USING (databaseId) es la forma corta de ON c.databaseId = r.databaseId cuando ambos lados usan el mismo nombre.
- Cuenta los jobs de Windows y sus fallos, a partir de la carpeta particionada.
Solución
De sql/04-exercises.sql, líneas 15-18:
SELECT os, count(*) AS jobs, count(*) FILTER (conclusion = 'failure') AS failedFROM read_parquet('out/jobs-by-os/*/*.parquet')WHERE os = 'windows-latest'GROUP BY ALL;┌────────────────┬───────┬────────┐│ os │ jobs │ failed ││ varchar │ int64 │ int64 │├────────────────┼───────┼────────┤│ windows-latest │ 35 │ 3 │└────────────────┴───────┴────────┘La columna os existe sin hive_partitioning = true: DuckDB detectó la organización de las carpetas. Con hive_partitioning = false, desaparece:
Binder Error: Referenced column "os" not found in FROM clause!Candidate bindings: "steps", "status", "conclusion", "started_at", "completed_at"Según la documentación, un filtro sobre una columna de partición se salta por completo los archivos de las demás carpetas: solo se abre os=windows-latest. La lección 7 lo comprueba en el plan de la consulta.
FROM 'data/*.json'lee los dos archivos JSON del curso, que no tienen los mismos campos. ¿Qué devuelve?
Solución
De sql/04-exercises.sql, líneas 21-25:
SELECT replace(filename, '\', '/') AS file, count(*) AS rows, count(databaseId) AS with_databaseId, count(run_id) AS with_run_id, count(conclusion) AS with_conclusionFROM read_json('data/*.json', filename = true)GROUP BY ALLORDER BY file;┌────────────────┬───────┬─────────────────┬─────────────┬─────────────────┐│ file │ rows │ with_databaseId │ with_run_id │ with_conclusion ││ varchar │ int64 │ int64 │ int64 │ int64 │├────────────────┼───────┼─────────────────┼─────────────┼─────────────────┤│ data/jobs.json │ 315 │ 0 │ 315 │ 315 ││ data/runs.json │ 125 │ 125 │ 0 │ 125 │└────────────────┴───────┴─────────────────┴─────────────┴─────────────────┘Ningún error: 440 filas y 20 columnas, la unión de ambos esquemas. Los campos de las ejecuciones son NULL en las filas de los jobs, y viceversa, y los campos que tienen ambos archivos, status y conclusion, se fusionan en una sola columna. Útil para archivos que ganaron un campo con el tiempo; una trampa cuando un glob atrapa un archivo que no debía. count(column) cuenta los valores que no son NULL, y así es como la consulta distingue los dos esquemas.