Skip to content

Querying Data

Working with an agent?

You don't have to write these scripts by hand. Claude has this page and the full schema reference in its workspace, so you can describe the question and let it pick the tables, push the filters into the query, and hand back a script you can keep and rerun. See Getting Started With Agents.

Interactive UI

The quickest way to explore your data is the DuckDB web UI. It connects to your storage, registers every table you have access to, and opens a local SQL editor in the browser. No Python beyond the launch command:

Open the interactive SQL editor
uv run duckdb-ui

It opens http://localhost:4213, with all tables available immediately as views. Press Enter in the terminal to stop the server.

Export to CSV

If you'd rather work in Excel or another spreadsheet tool, export every table you have access to as CSV files, one file per table:

Export every table to CSV
uv run export-csv

The files land in an exports/ folder in your workspace, named <table>.csv. Each run overwrites them with a fresh snapshot. To write somewhere else:

Export to a different directory
uv run export-csv --output ~/dda-data

The directory is created if it doesn't exist. Coming from IndieBI DDA v1? See Migrating From v1 for how the table names map.

Querying From Python

The SDK exposes two clients for querying data in code. Put your scripts in the projects/ folder of your workspace; projects/explore.py is there as a starting point.

DuckDBClient

Run SQL against your data and get back a validated Polars DataFrame. The best choice for straightforward queries.

Query with DuckDBClient
from dda_sdk import DuckDBClient, SalesPerSkuSchema

with DuckDBClient() as client:
    df = client.query(
        SalesPerSkuSchema,
        """
        SELECT *
        FROM sales_per_sku
        WHERE date >= '2026-01-01'
          AND date <= '2026-01-31'
        """,
    )

print(df["gross_sales"].sum())

The schema class you pass to query() validates the result. If a column is missing or has the wrong type, you get an error immediately rather than a silent wrong answer downstream.

PolarsClient

Returns a LazyFrame: nothing is fetched until you call .collect(). Better for large date ranges, or when you want to chain several transformations before pulling data.

Build a lazy pipeline with PolarsClient
import polars as pl
from dda_sdk import PolarsClient, SalesPerSkuSchema

with PolarsClient() as client:
    df = (
        client.scan_table(SalesPerSkuSchema)
        .filter(pl.col("date").is_between(pl.date(2026, 1, 1), pl.date(2026, 3, 31)))
        .filter(pl.col("portal_platform_region").str.starts_with("steam"))
        .group_by(pl.col("date").dt.truncate("1mo"))
        .agg(pl.col("gross_sales").sum())
        .sort("date")
        .collect()
    )

PolarsClient takes SQL too:

SQL through PolarsClient
with PolarsClient() as client:
    df = client.query(
        "SELECT * FROM sales_per_sku WHERE date >= '2026-01-01'"
    ).collect()

Passing Credentials Explicitly

Both clients read USER_ID and SAS_TOKEN from your .env by default. You can also pass them directly through Config:

Pass credentials without a .env file
from dda_sdk.config import Config

cfg = Config(user_id="u-xxxxxxx", sas_token="<your-sas-token>")

with DuckDBClient(cfg) as client:
    ...

with PolarsClient(cfg) as client:
    ...

This is useful when you're running against a different storage account, or when credentials come from a secret manager rather than a .env file.