1. Primeras consultas
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.
Instalar la CLI
Sección titulada «Instalar la CLI»DuckDB es una biblioteca: se ejecuta dentro del proceso que la usa, como SQLite. La CLI de DuckDB es uno de esos procesos, un único ejecutable sin dependencias. Es la forma más rápida de empezar, y la que usa este curso hasta la lección 5.
winget install DuckDB.cliDespués, abre una terminal nueva para que duckdb esté en el PATH. La CI de este curso descarga en su lugar duckdb_cli-windows-amd64.zip de las releases de GitHub, para fijar la versión: basta con descomprimir el ejecutable.
curl https://install.duckdb.org | shEl script instala el binario en ~/.duckdb/cli/<version>/, hace que ~/.duckdb/cli/latest apunte a él, e imprime la línea que debes añadir al perfil de tu shell:
export PATH="$HOME/.duckdb/cli/latest":$PATHSi ~/.local/bin existe, también crea allí un enlace duckdb. En un runner de GitHub: Successfully installed DuckDB 1.5.5 to /home/runner/.duckdb/cli/1.5.5/duckdb. Define DUCKDB_VERSION=1.5.5 antes de sh para fijar la versión.
brew install duckdbO el mismo script que en Linux, que es el que usa la CI de este curso en macos-latest:
curl https://install.duckdb.org | shduckdb --versionv1.5.5 (Variegata) d8cdaa33fdLos datos
Sección titulada «Los datos»Clona el repositorio de este sitio y entra en la carpeta del curso, donde funcionan las rutas relativas de todas las consultas de este curso:
git clone https://github.com/spareilleux/learn.gitcd learn/code/duckdbdata/runs.json es un único array JSON de 125 objetos, uno por cada ejecución de GitHub Actions de este repositorio:
[{"attempt":1,"conclusion":"success","createdAt":"2026-09-14T14:01:09Z","databaseId":34852867099,"event":"push","headBranch":"main","headSha":"95a3830ebbcfa3f0addbc1eba8335a20cc2c41f2","startedAt":"2026-09-14T14:01:09Z","status":"completed","updatedAt":"2026-09-14T14:03:38Z","workflowName":"Deploy to GitHub Pages"}, …Una consulta sobre un archivo
Sección titulada «Una consulta sobre un archivo»Inicia la CLI con duckdb: responde con su versión y Enter ".help" for usage hints., y abre una base de datos en memoria. Luego, sin crear nada:
DESCRIBE FROM 'data/runs.json';┌────────────────────────┐│ data/runs.json ││ ││ attempt bigint ││ conclusion varchar ││ createdAt timestamp ││ databaseId bigint ││ event varchar ││ headBranch varchar ││ headSha varchar ││ startedAt timestamp ││ status varchar ││ updatedAt timestamp ││ workflowName varchar │└────────────────────────┘Un nombre de archivo entre comillas simples es una tabla. DuckDB vio la extensión .json, leyó el archivo e infirió un tipo para cada campo: bigint para los números, timestamp para las cadenas ISO 8601, varchar para el resto. En SQL Server, lo más parecido es OPENROWSET(BULK …) con OPENJSON y una cláusula WITH explícita que enumera las columnas y sus tipos.
DESCRIBE FROM 'data/runs.json' es también un primer contacto con el Friendly SQL de DuckDB: FROM puede ir primero, y una consulta con solo FROM significa SELECT *.
La primera consulta de sql/01-first-queries.sql: qué workflows se ejecutan más, y con qué frecuencia fallan.
De sql/01-first-queries.sql, líneas 2-6:
SELECT workflowName, count(*) AS runs, count(*) FILTER (conclusion = 'failure') AS failuresFROM 'data/runs.json'GROUP BY ALLORDER BY runs DESC, workflowNameLIMIT 5;┌────────────────────────┬───────┬──────────┐│ workflowName │ runs │ failures ││ varchar │ int64 │ int64 │├────────────────────────┼───────┼──────────┤│ Deploy to GitHub Pages │ 65 │ 2 ││ Rust course examples │ 18 │ 3 ││ Java course examples │ 7 │ 0 ││ GHA 03: triggers │ 5 │ 2 ││ GHA 07: security │ 4 │ 0 │└────────────────────────┴───────┴──────────┘Dos extensiones al SQL que ya conoces:
GROUP BY ALLagrupa por todas las columnas delSELECTque no son agregaciones. Se acabó copiar la lista de columnas enGROUP BY.FILTERrestringe una sola agregación. En SQL Server se escribiríasum(CASE WHEN conclusion = 'failure' THEN 1 ELSE 0 END).FILTERes SQL estándar, y PostgreSQL también lo tiene.
LIMIT es TOP al final de la consulta. El formato de salida es duckbox, el predeterminado de la CLI: los tipos de las columnas bajo los nombres, y los números alineados a la derecha.
Leer los errores
Sección titulada «Leer los errores»Una costumbre de PostgreSQL, los nombres de columna en snake_case:
SELECT workflow_name FROM 'data/runs.json';Binder Error: Referenced column "workflow_name" not found in FROM clause!Candidate bindings: "workflowName", "conclusion"
LINE 1: SELECT workflow_name FROM 'data/runs.json'; ^Un Binder Error significa que el SQL se analizó bien, pero que un nombre no se resuelve. DuckDB sugiere los nombres más cercanos y señala la posición. Una tabla que todavía no existe da un Catalog Error:
FROM runs;Catalog Error: Table with name runs does not exist!Did you mean "duckdb_constraints"?
LINE 1: FROM runs; ^La sugerencia es el nombre más cercano entre las vistas del sistema: una suposición, no un indicio de que algo va mal con duckdb_constraints.
Primer vistazo a los datos: SUMMARIZE
Sección titulada «Primer vistazo a los datos: SUMMARIZE»SUMMARIZE calcula estadísticas para cada columna. Devuelve 12 columnas, demasiado ancho para esta página; como su resultado es una tabla como cualquier otra, una consulta puede elegir algunas:
SELECT column_name, column_type, min, max, approx_unique, null_percentageFROM (SUMMARIZE FROM 'data/runs.json');┌──────────────┬─────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┬───────────────┬─────────────────┐│ column_name │ column_type │ min │ max │ approx_unique │ null_percentage ││ varchar │ varchar │ varchar │ varchar │ int64 │ decimal(9,2) │├──────────────┼─────────────┼──────────────────────────────────────────┼──────────────────────────────────────────┼───────────────┼─────────────────┤│ attempt │ BIGINT │ 1 │ 3 │ 2 │ 0.00 ││ conclusion │ VARCHAR │ cancelled │ success │ 4 │ 0.00 ││ createdAt │ TIMESTAMP │ 2026-09-13 15:53:16 │ 2026-09-14 14:01:09 │ 93 │ 0.00 ││ databaseId │ BIGINT │ 34766934801 │ 34852867099 │ 131 │ 0.00 ││ event │ VARCHAR │ push │ workflow_dispatch │ 2 │ 0.00 ││ headBranch │ VARCHAR │ gha-06-invalid │ main │ 2 │ 0.00 ││ headSha │ VARCHAR │ 04c8770830c5666ca1e9bb8a4711065edfb351b4 │ fd52d46d1d2940e5dee33790e4a2873056f074f3 │ 59 │ 0.00 ││ startedAt │ TIMESTAMP │ 2026-09-13 15:53:16 │ 2026-09-14 14:01:09 │ 94 │ 0.00 ││ status │ VARCHAR │ completed │ completed │ 1 │ 0.00 ││ updatedAt │ TIMESTAMP │ 2026-09-13 15:53:55 │ 2026-09-14 14:03:38 │ 122 │ 0.00 ││ workflowName │ VARCHAR │ Deploy to GitHub Pages │ WSL containers examples │ 17 │ 0.00 │└──────────────┴─────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴───────────────┴─────────────────┘ 11 rows 6 columnsLo que esto dice en unos segundos:
- 125 filas y ningún
NULLen ninguna parte: la instantánea solo contiene ejecuciones terminadas (statustiene un único valor). - El historial cubre menos de un día: del 2026-09-13 15:53 al 2026-09-14 14:01.
attemptllega hasta 3: al menos una ejecución se volvió a ejecutar dos veces.headBranchtiene dos valores:main, y la rama de un experimento del curso de GitHub Actions.approx_uniquees una estimación.databaseId, el identificador de la ejecución, es único, y sin embargo 131 es más que las 125 filas. Usa HyperLogLog, que sacrifica exactitud a cambio de memoria constante. El ejercicio 2 encuentra los recuentos exactos.minymaxsonvarcharpara todas las columnas, para que quepan en una sola columna del resultado: elmaxdeattemptes la cadena'3'.
Las marcas de tiempo del archivo terminan en Z (UTC). DuckDB las leyó como TIMESTAMP, sin zona horaria: los valores son las horas UTC, y nada en el tipo lo indica. La lección 2 vuelve sobre esto.
Guardar las filas: una tabla
Sección titulada «Guardar las filas: una tabla»Cada consulta anterior volvió a leer y analizar el archivo JSON. Para leerlo una sola vez, guarda las filas en una tabla (CREATE TABLE … AS, el SELECT INTO de SQL Server) (sql/01-first-queries.sql, líneas 8-13):
CREATE TABLE runs AS SELECT * FROM 'data/runs.json';
SELECT event, conclusion, count(*) AS runsFROM runsGROUP BY ALLORDER BY event, conclusion;┌───────────────────┬─────────────────┬───────┐│ event │ conclusion │ runs ││ varchar │ varchar │ int64 │├───────────────────┼─────────────────┼───────┤│ push │ cancelled │ 2 ││ push │ failure │ 7 ││ push │ startup_failure │ 1 ││ push │ success │ 96 ││ workflow_dispatch │ cancelled │ 2 ││ workflow_dispatch │ failure │ 9 ││ workflow_dispatch │ success │ 8 │└───────────────────┴─────────────────┴───────┘Las ejecuciones manuales (workflow_dispatch) fallaron 9 veces de 19; los push, 7 de 106. Encaja con cómo se escribió el curso de GitHub Actions: los workflows de ejercicio, lanzados a mano, estaban pensados para fallar.
La tabla vive en la base de datos en memoria, y desaparece al salir de la CLI.
Guardar la base de datos: un archivo
Sección titulada «Guardar la base de datos: un archivo»El nombre del archivo es el primer argumento de duckdb. El archivo se crea si no existe:
mkdir outduckdb out/ci.duckdbCREATE TABLE runs AS FROM 'data/runs.json';.quitVuelve a iniciarla con el mismo archivo, y la tabla sigue ahí:
duckdb out/ci.duckdbSELECT count(*) FROM runs;SELECT database_name, path FROM duckdb_databases() WHERE NOT internal;┌──────────────┐│ count_star() ││ int64 │├──────────────┤│ 125 │└──────────────┘┌───────────────┬─────────────────────────────────────────────────────────────┐│ database_name │ path ││ varchar │ varchar │├───────────────┼─────────────────────────────────────────────────────────────┤│ ci │ C:\Users\spare\source\repos\learn\code\duckdb\out\ci.duckdb │└───────────────┴─────────────────────────────────────────────────────────────┘La base de datos toma el nombre del archivo. La extensión .duckdb es una convención; DuckDB no la exige.
Escribir archivos: COPY
Sección titulada «Escribir archivos: COPY»COPY … TO escribe una tabla o el resultado de una consulta en un archivo, y elige el formato según la extensión:
COPY runs TO 'out/runs.csv';COPY runs TO 'out/runs.parquet';Los tamaños, para las mismas 125 filas:
| Archivo | Bytes |
|---|---|
data/runs.json |
39.705 |
out/runs.csv |
21.058 |
out/runs.parquet |
8.553 |
out/ci.duckdb |
536.576 |
Parquet es un formato de archivo comprimido, por columnas y con tipos, y el que la lección 4 recomienda para los datos que conservas. El archivo .duckdb es el más grande. Un archivo de base de datos vacío ocupa 12.288 bytes de cabeceras; los datos se guardan en bloques de 262.144 bytes (block_size en PRAGMA database_size), y este tiene dos: 12.288 + 2 × 262.144 = 536.576. El formato está hecho para millones de filas, no para 125.
COPY no crea carpetas. Sin el mkdir out de más arriba:
IO Error: Cannot open file "out/runs.parquet": The system cannot find the path specified.La segunda mitad de ese mensaje es el texto del error del sistema de Windows.
Dos ajustes de la CLI
Sección titulada «Dos ajustes de la CLI»Los comandos de punto (dot commands) configuran la propia CLI. No son SQL, y no llevan punto y coma (lista):
.timer onSELECT count(*) FROM 'out/runs.parquet';┌──────────────┐│ count_star() ││ int64 │├──────────────┤│ 125 │└──────────────┘Run Time (s): real 0.001 user 0.000000 sys 0.000000Con 125 filas, cada consulta tarda alrededor de un milisegundo: el temporizador solo se vuelve interesante en la lección 7. .mode cambia el formato de salida; .mode line muestra un campo por línea, práctico para filas anchas:
.mode lineFROM 'data/runs.json' LIMIT 1; attempt = 1 conclusion = success createdAt = 2026-09-14 14:01:09 databaseId = 34852867099 event = push headBranch = main headSha = 95a3830ebbcfa3f0addbc1eba8335a20cc2c41f2 startedAt = 2026-09-14 14:01:09 status = completed updatedAt = 2026-09-14 14:03:38workflowName = Deploy to GitHub PagesScripts y salida CSV: cómo se prueba este curso
Sección titulada «Scripts y salida CSV: cómo se prueba este curso»La CLI también lee SQL de la entrada estándar, y -csv cambia la salida a CSV. check.sh ejecuta así cada script de sql/ y compara el resultado con el archivo del mismo nombre en sql/expected/:
duckdb -csv < sql/01-first-queries.sqlworkflowName,runs,failuresDeploy to GitHub Pages,65,2Rust course examples,18,3Java course examples,7,0GHA 03: triggers,5,2GHA 07: security,4,0event,conclusion,runspush,cancelled,2push,failure,7push,startup_failure,1push,success,96workflow_dispatch,cancelled,2workflow_dispatch,failure,9workflow_dispatch,success,8En un runner de GitHub, para cada uno de los tres sistemas operativos: ok 01-first-queries. -c ejecuta un único comando y termina, para comandos de una línea en una terminal: duckdb -c "FROM 'data/runs.json' LIMIT 3".
Puntos clave
Sección titulada «Puntos clave»- DuckDB es una biblioteca que se ejecuta dentro de tu proceso; la CLI es un único ejecutable, y no necesita ningún servidor.
- Un nombre de archivo entre comillas simples es una tabla: DuckDB infiere el formato a partir de la extensión y los tipos a partir del contenido.
DESCRIBEySUMMARIZEson los dos primeros comandos que conviene ejecutar sobre datos desconocidos;approx_uniquees una estimación.- Una tabla se queda en memoria salvo que la CLI se haya iniciado con un archivo de base de datos;
COPY … TOescribe CSV, JSON o Parquet. - Los errores indican su tipo (
Binder,Catalog,IO) y sugieren los nombres más cercanos.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en sql/01-exercises.sql, comprobadas por la CI igual que la lección.
- ¿Cuántas ejecuciones manuales (
workflow_dispatch) fallaron?
Solución
De sql/01-exercises.sql, líneas 3-5:
SELECT count(*) AS failed_manual_runsFROM 'data/runs.json'WHERE event = 'workflow_dispatch' AND conclusion = 'failure';┌────────────────────┐│ failed_manual_runs ││ int64 │├────────────────────┤│ 9 │└────────────────────┘El mismo número que la fila workflow_dispatch / failure de la tabla anterior.
SUMMARIZEestima 17 nombres de workflow distintos y 59 commits distintos. ¿Cuáles son los números exactos?
Solución
De sql/01-exercises.sql, líneas 8-9:
SELECT count(DISTINCT workflowName) AS workflows, count(DISTINCT headSha) AS commitsFROM 'data/runs.json';┌───────────┬─────────┐│ workflows │ commits ││ int64 │ int64 │├───────────┼─────────┤│ 21 │ 65 │└───────────┴─────────┘21 y 65: la estimación se quedó corta en torno a un 10 % en ambos casos, y se pasó en un 5 % con los identificadores de ejecución. count(DISTINCT …) es exacto pero tiene que recordar cada valor que ha visto; approx_count_distinct(…) da el mismo tipo de estimación que SUMMARIZE, con memoria constante. Con 125 filas, usa el exacto.
- El Binder Error de más arriba sugería
workflowName. ¿Funcionaríaworkflowname, todo en minúsculas? ¿Y cómo se llamaría la columna en el resultado?
Solución
De sql/01-exercises.sql, líneas 12-17:
SELECT workflowname, count(*) AS runsFROM 'data/runs.json'WHERE workflowname LIKE 'GHA 0%'GROUP BY ALLORDER BY workflownameLIMIT 3;┌─────────────────────────┬───────┐│ workflowName │ runs ││ varchar │ int64 │├─────────────────────────┼───────┤│ GHA 01: hello │ 1 ││ GHA 02: build and test │ 3 ││ GHA 02: exercise checks │ 1 │└─────────────────────────┴───────┘Sí. En DuckDB, los identificadores no distinguen mayúsculas de minúsculas, «de forma similar a PostgreSQL», y tampoco entre comillas dobles, a diferencia de PostgreSQL, donde "workflowName" y workflowname son dos nombres distintos. La comparación solo tiene en cuenta ASCII: para ella, é y É son letras distintas. DuckDB conserva las mayúsculas y minúsculas del nombre tal como se definió, así que la columna del resultado es workflowName, no lo que se escribió en la consulta. workflow_name falló porque el guion bajo lo convierte en otro nombre, no por las mayúsculas.