Ir al contenido
learn

1. Primeras consultas

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.

Ventana de terminal
winget install DuckDB.cli

Despué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.

Ventana de terminal
duckdb --version
v1.5.5 (Variegata) d8cdaa33fd

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:

Ventana de terminal
git clone https://github.com/spareilleux/learn.git
cd learn/code/duckdb

data/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"}, …

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 failures
FROM 'data/runs.json'
GROUP BY ALL
ORDER BY runs DESC, workflowName
LIMIT 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 ALL agrupa por todas las columnas del SELECT que no son agregaciones. Se acabó copiar la lista de columnas en GROUP BY.
  • FILTER restringe una sola agregación. En SQL Server se escribiría sum(CASE WHEN conclusion = 'failure' THEN 1 ELSE 0 END). FILTER es 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.

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.

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_percentage
FROM (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 columns

Lo que esto dice en unos segundos:

  • 125 filas y ningún NULL en ninguna parte: la instantánea solo contiene ejecuciones terminadas (status tiene un único valor).
  • El historial cubre menos de un día: del 2026-09-13 15:53 al 2026-09-14 14:01.
  • attempt llega hasta 3: al menos una ejecución se volvió a ejecutar dos veces.
  • headBranch tiene dos valores: main, y la rama de un experimento del curso de GitHub Actions.
  • approx_unique es 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.
  • min y max son varchar para todas las columnas, para que quepan en una sola columna del resultado: el max de attempt es 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.

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 runs
FROM runs
GROUP BY ALL
ORDER 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.

El nombre del archivo es el primer argumento de duckdb. El archivo se crea si no existe:

Ventana de terminal
mkdir out
duckdb out/ci.duckdb
CREATE TABLE runs AS FROM 'data/runs.json';
.quit

Vuelve a iniciarla con el mismo archivo, y la tabla sigue ahí:

Ventana de terminal
duckdb out/ci.duckdb
SELECT 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.

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.

Los comandos de punto (dot commands) configuran la propia CLI. No son SQL, y no llevan punto y coma (lista):

.timer on
SELECT count(*) FROM 'out/runs.parquet';
┌──────────────┐
│ count_star() │
│ int64 │
├──────────────┤
│ 125 │
└──────────────┘
Run Time (s): real 0.001 user 0.000000 sys 0.000000

Con 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 line
FROM '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:38
workflowName = Deploy to GitHub Pages

Scripts 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/:

Ventana de terminal
duckdb -csv < sql/01-first-queries.sql
workflowName,runs,failures
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
event,conclusion,runs
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

En 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".

  • 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.
  • DESCRIBE y SUMMARIZE son los dos primeros comandos que conviene ejecutar sobre datos desconocidos; approx_unique es una estimación.
  • Una tabla se queda en memoria salvo que la CLI se haya iniciado con un archivo de base de datos; COPY … TO escribe CSV, JSON o Parquet.
  • Los errores indican su tipo (Binder, Catalog, IO) y sugieren los nombres más cercanos.

Las soluciones están en sql/01-exercises.sql, comprobadas por la CI igual que la lección.

  1. ¿Cuántas ejecuciones manuales (workflow_dispatch) fallaron?
Solución

De sql/01-exercises.sql, líneas 3-5:

SELECT count(*) AS failed_manual_runs
FROM '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.

  1. SUMMARIZE estima 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 commits
FROM '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.

  1. El Binder Error de más arriba sugería workflowName. ¿Funcionaría workflowname, 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 runs
FROM 'data/runs.json'
WHERE workflowname LIKE 'GHA 0%'
GROUP BY ALL
ORDER BY workflowname
LIMIT 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.