DuckDB Operators

DuckDB is an in-process analytical database. Queries run inside the Airflow task process, so there is no cluster to provision and no data to move before querying it.

Run a query

Use DuckDBExecuteQueryOperator to run SQL against an in-process DuckDB database.

Unlike the other SQL operators this one does not require an Airflow connection to exist. With no duckdb_default connection configured it runs against an in-memory database, which is the common case for a task that reads its input, transforms it and writes its output back out:

aggregate = DuckDBExecuteQueryOperator(
    task_id="aggregate",
    sql="""
        COPY (
            SELECT category, SUM(price * quantity) AS revenue
            FROM read_parquet('/opt/airflow/data/sales/*.parquet')
            GROUP BY category
        ) TO '/opt/airflow/data/summary/revenue.parquet' (FORMAT PARQUET)
    """,
    hook_params={"memory_limit": "2GB", "threads": 4},
)

Querying cloud object storage

DuckDB can read and write object storage directly, which needs the httpfs extension and credentials for the store:

hook_params = {"extensions": ["httpfs"]}

Note

httpfs has to be present for that to work. Extension downloads are off by default, so either pre-populate an extension directory or set autoinstall_extensions=True to let DuckDB fetch it.

Supplying those credentials is backend-specific and this provider does not do it for you: DuckDB does not use the cloud SDKs, so it needs its own secret rather than the Airflow connection. Use the hook from the provider for your storage backend, which builds that secret from the credentials Airflow already holds, or issue CREATE SECRET yourself.

Configuring the engine

Anything DuckDBHook accepts can be passed through hook_params, or set once on the connection so every task inherits it. Prefer setting memory_limit and threads explicitly: DuckDB otherwise sizes itself from the resources it detects on the host, which over-commits inside a container.

Using the hook directly

For work that is not a single statement — registering a dataframe, chaining several queries against one database, or returning Arrow — use the hook:

@task
def summarize():
    hook = DuckDBHook()
    with hook.get_conn() as conn:
        conn.execute("CREATE TABLE sales AS SELECT * FROM read_parquet('/opt/airflow/data/sales.parquet')")
        return conn.execute("SELECT category, SUM(revenue) FROM sales GROUP BY category").fetchall()

Opening one connection for several statements also avoids paying connection setup and extension loading more than once per task.

Was this entry helpful?