1. Premières requêtes
Sections
Réglages de lecture
Voix naturelle enregistrée pour ce cours, la même dans tous les navigateurs.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Installer la CLI
Section intitulée « Installer la CLI »DuckDB est une bibliothèque : elle s’exécute dans le processus qui l’utilise, comme SQLite. La CLI de DuckDB est l’un de ces processus, un exécutable unique sans dépendances. C’est le moyen le plus rapide de commencer, et celui qu’utilise ce cours jusqu’à la leçon 5.
winget install DuckDB.cliOuvre ensuite un nouveau terminal pour que duckdb soit dans le PATH. La CI de ce cours télécharge plutôt duckdb_cli-windows-amd64.zip depuis les releases GitHub, pour épingler la version : il suffit de décompresser l’exécutable.
curl https://install.duckdb.org | shLe script installe le binaire dans ~/.duckdb/cli/<version>/, fait pointer ~/.duckdb/cli/latest vers lui, et affiche la ligne à ajouter au profil de ton shell :
export PATH="$HOME/.duckdb/cli/latest":$PATHSi ~/.local/bin existe, il y crée aussi un lien duckdb. Sur un runner GitHub : Successfully installed DuckDB 1.5.5 to /home/runner/.duckdb/cli/1.5.5/duckdb. Définis DUCKDB_VERSION=1.5.5 avant sh pour épingler la version.
brew install duckdbOu le même script que sous Linux, celui qu’utilise la CI de ce cours sur macos-latest :
curl https://install.duckdb.org | shduckdb --versionv1.5.5 (Variegata) d8cdaa33fdLes données
Section intitulée « Les données »Clone le dépôt de ce site et va dans le dossier du cours, là où fonctionnent les chemins relatifs de toutes les requêtes de ce cours :
git clone https://github.com/spareilleux/learn.gitcd learn/code/duckdbdata/runs.json est un unique tableau JSON de 125 objets, un par exécution GitHub Actions de ce dépôt :
[{"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"}, …Une requête sur un fichier
Section intitulée « Une requête sur un fichier »Lance la CLI avec duckdb : elle répond avec sa version et Enter ".help" for usage hints., et ouvre une base de données en mémoire. Ensuite, sans rien créer :
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 nom de fichier entre apostrophes est une table. DuckDB a vu l’extension .json, a lu le fichier, et a déduit un type pour chaque champ : bigint pour les nombres, timestamp pour les chaînes ISO 8601, varchar pour le reste. Dans SQL Server, ce qui s’en rapproche le plus est OPENROWSET(BULK …) avec OPENJSON et une clause WITH explicite qui liste les colonnes et leurs types.
DESCRIBE FROM 'data/runs.json' est aussi un premier aperçu du Friendly SQL de DuckDB : FROM peut venir en premier, et une requête avec seulement FROM signifie SELECT *.
La première requête de sql/01-first-queries.sql : quels workflows s’exécutent le plus, et à quelle fréquence ils échouent.
Extrait de sql/01-first-queries.sql, lignes 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 │└────────────────────────┴───────┴──────────┘Deux extensions au SQL que tu connais :
GROUP BY ALLregroupe par toutes les colonnes duSELECTqui ne sont pas des agrégats. Fini de recopier la liste des colonnes dansGROUP BY.FILTERrestreint un seul agrégat. En SQL Server, on écriraitsum(CASE WHEN conclusion = 'failure' THEN 1 ELSE 0 END).FILTERfait partie du SQL standard, et PostgreSQL l’a aussi.
LIMIT, c’est TOP placé à la fin de la requête. Le format de sortie est duckbox, celui de la CLI par défaut : les types des colonnes sous leurs noms, et les nombres alignés à droite.
Lire les erreurs
Section intitulée « Lire les erreurs »Une habitude de PostgreSQL, les noms de colonnes 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'; ^Une Binder Error signifie que le SQL a été analysé, mais qu’un nom ne se résout pas. DuckDB propose les noms les plus proches et indique la position. Une table qui n’existe pas encore donne une Catalog Error :
FROM runs;Catalog Error: Table with name runs does not exist!Did you mean "duckdb_constraints"?
LINE 1: FROM runs; ^La suggestion est le nom le plus proche parmi les vues système : une supposition, pas un indice que quelque chose ne va pas avec duckdb_constraints.
Premier regard sur les données : SUMMARIZE
Section intitulée « Premier regard sur les données : SUMMARIZE »SUMMARIZE calcule des statistiques pour chaque colonne. Il renvoie 12 colonnes, trop pour la largeur de cette page ; comme son résultat est une table comme une autre, une requête peut en choisir quelques-unes :
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 columnsCe que cela dit en quelques secondes :
- 125 lignes et aucun
NULLnulle part : l’instantané ne contient que des exécutions terminées (statusn’a qu’une valeur). - L’historique couvre moins d’une journée : du 2026-09-13 15:53 au 2026-09-14 14:01.
attemptmonte jusqu’à 3 : au moins une exécution a été relancée deux fois.headBrancha deux valeurs :main, et la branche d’une expérience du cours GitHub Actions.approx_uniqueest une estimation.databaseId, l’identifiant de l’exécution, est unique, et pourtant 131 dépasse les 125 lignes. Il repose sur HyperLogLog, qui sacrifie l’exactitude pour une mémoire constante. L’exercice 2 trouve les nombres exacts.minetmaxsont envarcharpour toutes les colonnes, afin de tenir dans une seule colonne de résultat : lemaxdeattemptest la chaîne'3'.
Les horodatages du fichier se terminent par Z (UTC). DuckDB les a lus comme TIMESTAMP, sans fuseau horaire : les valeurs sont les heures UTC, et rien dans le type ne le dit. La leçon 2 y revient.
Garder les lignes : une table
Section intitulée « Garder les lignes : une table »Chaque requête ci-dessus a relu et réanalysé le fichier JSON. Pour ne le lire qu’une fois, stocke les lignes dans une table (CREATE TABLE … AS, le SELECT INTO de SQL Server) (sql/01-first-queries.sql, lignes 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 │└───────────────────┴─────────────────┴───────┘Les exécutions manuelles (workflow_dispatch) ont échoué 9 fois sur 19 ; les push, 7 fois sur 106. Cela correspond à la façon dont le cours GitHub Actions a été écrit : les workflows d’exercice, lancés à la main, étaient conçus pour échouer.
La table vit dans la base de données en mémoire, et disparaît quand on quitte la CLI.
Garder la base de données : un fichier
Section intitulée « Garder la base de données : un fichier »Le nom du fichier est le premier argument de duckdb. Le fichier est créé s’il n’existe pas :
mkdir outduckdb out/ci.duckdbCREATE TABLE runs AS FROM 'data/runs.json';.quitRelance-la avec le même fichier, et la table est toujours là :
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 données porte le nom du fichier. L’extension .duckdb est une convention ; DuckDB ne l’exige pas.
Écrire des fichiers : COPY
Section intitulée « Écrire des fichiers : COPY »COPY … TO écrit une table ou le résultat d’une requête dans un fichier, et choisit le format d’après l’extension :
COPY runs TO 'out/runs.csv';COPY runs TO 'out/runs.parquet';Les tailles, pour les mêmes 125 lignes :
| Fichier | Octets |
|---|---|
data/runs.json |
39 705 |
out/runs.csv |
21 058 |
out/runs.parquet |
8 553 |
out/ci.duckdb |
536 576 |
Parquet est un format de fichier compressé, en colonnes et typé, et celui que la leçon 4 recommande pour les données que tu conserves. Le fichier .duckdb est le plus gros. Un fichier de base de données vide fait 12 288 octets d’en-têtes ; les données sont stockées dans des blocs de 262 144 octets (block_size dans PRAGMA database_size), et celui-ci en a deux : 12 288 + 2 × 262 144 = 536 576. Le format est fait pour des millions de lignes, pas pour 125.
COPY ne crée pas les dossiers. Sans le mkdir out ci-dessus :
IO Error: Cannot open file "out/runs.parquet": The system cannot find the path specified.La seconde moitié de ce message est le texte de l’erreur système de Windows.
Deux réglages de la CLI
Section intitulée « Deux réglages de la CLI »Les commandes à point (dot commands) configurent la CLI elle-même. Ce n’est pas du SQL, et elles ne prennent pas de point-virgule (liste) :
.timer onSELECT count(*) FROM 'out/runs.parquet';┌──────────────┐│ count_star() ││ int64 │├──────────────┤│ 125 │└──────────────┘Run Time (s): real 0.001 user 0.000000 sys 0.000000Sur 125 lignes, chaque requête prend environ une milliseconde : le chronomètre ne devient intéressant qu’à la leçon 7. .mode change le format de sortie ; .mode line affiche un champ par ligne, pratique pour les lignes larges :
.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 et sortie CSV : comment ce cours est testé
Section intitulée « Scripts et sortie CSV : comment ce cours est testé »La CLI lit aussi du SQL sur l’entrée standard, et -csv passe la sortie en CSV. check.sh exécute ainsi chaque script de sql/ et compare le résultat avec le fichier du même nom dans 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,8Sur un runner GitHub, pour chacun des trois systèmes d’exploitation : ok 01-first-queries. -c exécute une seule commande puis quitte, pour les commandes d’une ligne dans un terminal : duckdb -c "FROM 'data/runs.json' LIMIT 3".
À retenir
Section intitulée « À retenir »- DuckDB est une bibliothèque qui s’exécute dans ton processus ; la CLI est un exécutable unique, et n’a besoin d’aucun serveur.
- Un nom de fichier entre apostrophes est une table : DuckDB déduit le format de l’extension et les types du contenu.
DESCRIBEetSUMMARIZEsont les deux premières commandes à lancer sur des données inconnues ;approx_uniqueest une estimation.- Une table reste en mémoire, sauf si la CLI a été lancée avec un fichier de base de données ;
COPY … TOécrit du CSV, du JSON ou du Parquet. - Les erreurs annoncent leur type (
Binder,Catalog,IO) et proposent les noms les plus proches.
Exercices
Section intitulée « Exercices »Les solutions sont dans sql/01-exercises.sql, vérifiées par la CI comme la leçon.
- Combien d’exécutions manuelles (
workflow_dispatch) ont échoué ?
Solution
Extrait de sql/01-exercises.sql, lignes 3-5 :
SELECT count(*) AS failed_manual_runsFROM 'data/runs.json'WHERE event = 'workflow_dispatch' AND conclusion = 'failure';┌────────────────────┐│ failed_manual_runs ││ int64 │├────────────────────┤│ 9 │└────────────────────┘Le même nombre que la ligne workflow_dispatch / failure du tableau ci-dessus.
SUMMARIZEestime 17 noms de workflow distincts et 59 commits distincts. Quels sont les nombres exacts ?
Solution
Extrait de sql/01-exercises.sql, lignes 8-9 :
SELECT count(DISTINCT workflowName) AS workflows, count(DISTINCT headSha) AS commitsFROM 'data/runs.json';┌───────────┬─────────┐│ workflows │ commits ││ int64 │ int64 │├───────────┼─────────┤│ 21 │ 65 │└───────────┴─────────┘21 et 65 : l’estimation était trop basse d’environ 10 % dans les deux cas, et trop haute de 5 % sur les identifiants d’exécution. count(DISTINCT …) est exact mais doit retenir chaque valeur rencontrée ; approx_count_distinct(…) donne le même genre d’estimation que SUMMARIZE, en mémoire constante. Sur 125 lignes, prends la version exacte.
- La Binder Error ci-dessus proposait
workflowName. Est-ce queworkflowname, tout en minuscules, fonctionnerait ? Et comment s’appellerait la colonne dans le résultat ?
Solution
Extrait de sql/01-exercises.sql, lignes 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 │└─────────────────────────┴───────┘Oui. Les identifiants sont insensibles à la casse dans DuckDB, « de manière similaire à PostgreSQL », et ils le restent entre guillemets doubles, contrairement à PostgreSQL, où "workflowName" et workflowname sont deux noms différents. La comparaison ne porte que sur l’ASCII : pour elle, é et É sont des lettres différentes. DuckDB conserve la casse du nom tel qu’il a été défini, donc la colonne du résultat s’appelle workflowName, et non comme dans la requête. workflow_name a échoué parce que le tiret bas en fait un autre nom, pas à cause de la casse.