DuckDB

Des pipelines DuckDB planifiés, sans placer une base de données devant votre base de données.

DuckDB s'exécute dans le processus, sans serveur, ce qui explique précisément l'absence de planificateur, de reprise et d'historique. Dagu ajoute cette couche opérationnelle depuis un binaire unique : la pile reste deux exécutables et un répertoire de données.

Un binaire qui orchestre un binaire, sans base de données à exploiter
max_active_runs garantit la règle d'écrivain unique de DuckDB
Les curseurs survivent aux redémarrages pour les chargements incrémentaux
S'exécute près des données, y compris en réseau fermé
01

Embarqué signifie sans planificateur, par conception

DuckDB s'exécute à l'intérieur de votre processus. Pas de démon, pas de port, et rien qui attende pour lancer une requête à trois heures du matin. C'est le principe même de la conception, pas un oubli, et cela implique que la planification vienne de l'extérieur.

  • Un pipeline DuckDB se résume souvent à un appel CLI, ce qui fait de cron la réponse par défaut, celle où la plupart des équipes s'arrêtent
  • cron ne fournit que l'exécution : aucune reprise quand S3 expire, aucun historique, aucun signal quand la nuit dernière n'a silencieusement rien fait
  • L'extension cron communautaire planifie dans le processus : la planification meurt avec lui et ne laisse aucun historique
02

Ne défaites pas la raison qui vous a fait choisir DuckDB

L'intérêt de DuckDB, c'est l'absence de cluster, de serveur et de facture d'entrepôt. Placer devant lui un orchestrateur lourd rend tout cela. Airflow exige un planificateur, une base de métadonnées et un framework DAG Python. Kestra exige une base JDBC, un stockage objet et quatre composants. Dagu est un binaire dont l'état tient dans des fichiers.

  • Ajouter Postgres pour planifier un moteur analytique sans serveur est un échange étrange
  • Les définitions de workflow restent dans git, à côté du SQL qu'elles exécutent, plutôt que dans les métadonnées d'une autre plateforme
  • Toute la pile tient sur un hôte, en général celui où les données se trouvent déjà
03

L'écrivain unique est un problème de planification

Le conseil standard en production pour DuckDB est de se prémunir contre les écritures concurrentes par un verrou au niveau du système, car la base n'accepte qu'un seul écrivain. Quand cette protection est cron plus un fichier de verrou, elle n'est qu'à un script d'être fausse. La déclarer sur le workflow supprime toute la classe de bogues.

  • max_active_runs à 1 signifie qu'une exécution lente retarde la suivante au lieu de corrompre la base
  • resources.limits.memory plafonne une exécution avant qu'une jointure massive n'emporte l'hôte
  • retry_policy couvre les échecs réellement transitoires, comme un stockage objet qui expire en pleine lecture

Cela encadre les exécutions concurrentes de ce workflow. DuckDB ne devient pas multi-écrivain pour autant : un autre processus écrivant dans le même fichier reste à votre charge.

Agrégation nocturne sur du Parquet en stockage objet
# duckdb-nightly-rollup.yaml
schedule: "0 3 * * *"
max_active_runs: 1

resources:
  limits:
    memory: "8Gi"

steps:
  - id: rollup
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        INSTALL httpfs; LOAD httpfs;
        CREATE OR REPLACE TABLE daily_sales AS
          SELECT order_date, region, sum(amount) AS amount
          FROM read_parquet('s3://warehouse/raw/orders/*.parquet')
          GROUP BY 1, 2;
    retry_policy:
      limit: 2
      interval_sec: 300

  - id: export
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        COPY daily_sales TO '/data/export/daily_sales.parquet' (FORMAT parquet);
    depends: rollup

  - id: count_rows
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      readonly: true
      query: SELECT count(*) AS row_count FROM daily_sales;
    depends: export

  - id: verify
    env:
      - COUNT_JSON: ${steps.count_rows.outputs.result}
    run: test "$(printf '%s\n' "$COUNT_JSON" | jq -r '.[0].row_count')" -gt 0
    depends: count_rows

handler_on:
  failure:
    run: /opt/analytics/notify-failure.sh

mail_on:
  failure: true
04

Les chargements incrémentaux exigent un curseur qui survit au processus

Tout recharger à chaque exécution transforme une requête locale rapide en requête lente et coûteuse. Un chargement incrémental doit se souvenir de l'endroit où la dernière exécution réussie s'est arrêtée, et cette mémoire doit survivre à un redémarrage sans base de données pour la conserver.

  • Dagu conserve un petit curseur JSON entre les exécutions, si bien que chaque exécution ne lit que la fenêtre non encore chargée
  • Le curseur est enregistré après le succès de l'étape de chargement : une exécution en échec le laisse intact et la suivante rejoue la même fenêtre
  • Borner la fenêtre aux deux extrémités évite la course avec les lignes qui arrivent pendant l'exécution
Ajout incrémental avec repère persistant
# duckdb-incremental-load.yaml
schedule: "*/15 * * * *"
max_active_runs: 1

steps:
  - id: load_cursor
    action: state.get
    output: CURSOR
    with:
      key: cursors/events-loaded-through
      default:
        loaded_through: "2026-01-01T00:00:00Z"

  - id: window
    run: |
      printf 'since=%s\n' "$(printf '%s\n' "$CURSOR" | jq -r .value.loaded_through)" >> "$DAGU_OUTPUT_FILE"
      printf 'until=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: since
      - name: until
    depends: load_cursor

  - id: append_new_events
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        INSTALL httpfs; LOAD httpfs;
        INSERT INTO events
          SELECT * FROM read_parquet('s3://warehouse/events/*.parquet')
          WHERE ingested_at >  TIMESTAMP '${steps.window.outputs.since}'
            AND ingested_at <= TIMESTAMP '${steps.window.outputs.until}';
    depends: window
    retry_policy:
      limit: 3
      interval_sec: 60

  - id: save_cursor
    action: state.set
    with:
      key: cursors/events-loaded-through
      value:
        loaded_through: "${steps.window.outputs.until}"
    depends: append_new_events

mail_on:
  failure: true
05

Là où cette combinaison est le choix le plus faible

Dagu planifie DuckDB. Il ne change pas ce qu'est DuckDB, et certaines charges rendent la combinaison inadaptée.

  • DuckDB est mono-nœud et mono-écrivain. Si plusieurs services doivent écrire en même temps, aucun orchestrateur n'y remédie
  • Il n'est pas conçu pour des écritures petites et fréquentes ; cela reste le domaine de PostgreSQL
  • Si vous exploitez déjà Airflow ou un entrepôt doté de son planificateur, ajouter un second orchestrateur pour un seul pipeline se justifie rarement

FAQ

Practical questions before adopting

Existe-t-il un exécuteur ou un plugin DuckDB ?

Non, et c'est délibéré. DuckDB fournit une CLI en binaire unique qui fait déjà le travail, donc Dagu l'exécute comme une étape de commande ordinaire. Un exécuteur dédié n'ajouterait qu'une couche à maintenir au rythme des versions de DuckDB, tout en offrant moins que la CLI.

Comment éviter que deux exécutions corrompent le fichier de base ?

Mettez max_active_runs à 1 sur le workflow. Une exécution en cours bloque la suivante au lieu d'ouvrir un second écrivain : c'est la version déclarative du fichier de verrou que la documentation DuckDB vous demande de construire. Cela ne régit que les exécutions de ce workflow, tenez donc les autres processus à l'écart du même fichier.

Que se passe-t-il si une grosse requête épuise la mémoire ?

Définissez resources.limits.memory sur le workflow pour plafonner l'exécution avant qu'elle n'emporte l'hôte, et donnez à l'étape une politique de reprise pour les échecs transitoires plutôt que structurels. Une requête qui demande plus de mémoire que l'hôte n'en possède est à réécrire, pas à rejouer.

DuckDB a une extension cron communautaire. Pourquoi ne pas l'utiliser ?

Elle planifie à l'intérieur du processus DuckDB : la planification n'existe donc que tant que ce processus vit. Aucun historique, aucune reprise, aucune alerte en cas d'échec et rien à consulter le lendemain matin. Cela convient à une application embarquée de longue durée, pas à du batch sur un serveur.

DuckDB peut-il lire depuis S3 dans une exécution planifiée ?

Oui, via l'extension httpfs, et c'est ainsi que la plupart des travaux DuckDB planifiés lisent du Parquet sans étape d'extraction séparée. Le stockage objet étant une dépendance réseau, c'est précisément l'étape qui mérite une politique de reprise.

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.