<catalog> and <namespace> with the names Autumn assigned you.
Find your catalog and namespace
Autumn sends you both names when your Lakehouse is provisioned (see Connecting). If you need to rediscover them in ClickHouse Cloud, the catalog mounts as a database — list databases to find it:- ClickHouse
- BigQuery
`<catalog>`.`<namespace>.v2_3_<object>` (the <namespace>.v2_3_<object> part is a single literal table name — see below).Identifier syntax
- ClickHouse
- BigQuery
The Glue catalog mounts as a database. The whole A common mistake is
<namespace>.v2_3_<object> is the table name — the dot is literal, so backtick both parts:`<catalog>.<namespace>`.`v2_3_features` — that won’t resolve, because <namespace>.v2_3_features is a single identifier, not database.table. The single-quoted '<namespace>.v2_3_features' form does not resolve in ClickHouse Cloud either — use backticks on both parts.Timestamps
Allnumber (epoch ms) columns (created_at, started_at, expires_at, current_period_*, *_resets_at, …) are epoch-milliseconds. Convert before use. The only native timestamp is events.timestamp (the unversioned events table — see Schema → Events).
- ClickHouse
- BigQuery
fromUnixTimestamp64Milli(toInt64(created_at)) also works and preserves millisecond precision. events.timestamp is already a DateTime — use it directly.JSON columns
metadata, config, processors, properties, deductions, display, v2_3_rollovers.entities, and the discounts arrays are stored as JSON strings.
- ClickHouse
- BigQuery
Nullable columns: don’t use IS NULL
Examples
Fetch one customer
- ClickHouse
- BigQuery
Active subscriptions joined to their plan
Join on the internal ids, not the external ones (see below).- ClickHouse
- BigQuery
A customer’s balances for a feature
Use the customer-scoped (pooled) rows wherecoalesce(entity_id, '') = ''.
- ClickHouse
- BigQuery
Balance including rollovers
Joinv2_3_rollovers.internal_breakdown_id to v2_3_breakdowns.internal_id and sum the unexpired rows. A rollover with expires_at null never expires; one with expires_at in the past is dead weight the table still keeps, so an unfiltered sum(balance) overstates.
- ClickHouse
- BigQuery
Aggregate the rollovers in a subquery before joining, as above. Joining the rollover rows in directly fans out
v2_3_breakdowns — one entitlement can hold several rollover grants — and then sum(b.remaining) counts the same breakdown row once per rollover. This query also has the same caveat as every other raw balances query: it sums over all entitlement rows, so apply the active + current-cycle filter if you want the figure the API would return.Current-period overage
Overage is derived, not stored, and there are two figures — pick deliberately (see Working with balances → Overage):- Displayed (
max(0, Σusage − Σgranted)— sum first, floor once): what the balance header / dashboard shows. This query computes this one, so it reproduces the dashboard to within sync-lag drift. - Billable (
Σ max(0, usage − granted)— floor per row, then sum): what Autumn invoices. To get it, swap theoverageexpression as noted in the query.
v2_3_breakdowns over the same row set: rows whose customer plan is an active subscription (plus always-live one_off rows), restricted to each entitlement’s current cycle (earliest upcoming reset).
Swap in your
<catalog> / <namespace> and the feature id. Known simplifications: it omits rollover balances (join v2_3_rollovers as in Balance including rollovers if your features roll over — unspent rollover reduces overage) and it is customer-scope-only (coalesce(entity_id,'') = ''), so entity-scoped overage under config.disable_pooled_balance is not counted. For those customers, sum the per-entity rows instead.- ClickHouse
- BigQuery
Invoice totals by status
- ClickHouse
- BigQuery
plan_ids array with ARRAY JOIN:Event volume per day by subtype
- ClickHouse
- BigQuery
Reconstruct a full customer (API shape)
This rebuilds the entireGET /customers/:id response — scalars, subscriptions, purchases, balances (with per-plan breakdowns), flags, and invoices — from the warehouse in a single query, returned as one JSON object. It’s the most useful query if you want the API’s customer view without calling the API.
ClickHouse-specific (uses groupArray, Tuple/Map casts, and the JSON type — ClickHouse 24.8+). Customer-level (pooled) balances and flags use entity_id = ''; per-entity rows are excluded from the customer envelope. FORMAT PrettyJSONEachRow emits one pretty-printed JSON object.
- ClickHouse
Recipes
Short, runnable answers to the questions people ask most often once they start joining the balances lane to everything else. They’re written for BigQuery; to run them on ClickHouse, swap the addressing form to`<catalog>`.`<namespace>.<table>`, replace TIMESTAMP_MILLIS(x) with fromUnixTimestamp64Milli(toInt64(x)), UNIX_MILLIS(CURRENT_TIMESTAMP()) with toUnixTimestamp(now()) * 1000, and every IS NULL / IS NOT NULL with the coalesce form (why).
All of these read raw breakdown rows, which span superseded plan versions and past cycles and exclude rollovers, so apply the active + current-cycle filter before quoting any of these numbers as the customer’s real position.
Join a breakdown to its subscription
internal_customer_product_id is the subscription’s internal_id, so this is one equality — no detour through customer + plan version.
Join a breakdown to its purchase
Identical shape againstv2_3_purchases — one-off purchases have no status, so their liveness is expires_at.
The status of the plan a breakdown belongs to
v2_3_breakdowns has no status column — status is a property of the customer’s plan, so you fetch it through internal_customer_product_id. Because that key resolves to a subscription or a purchase, join both sides and coalesce. This also shows how to recover the external plan_id, which no longer lives on breakdowns: join internal_product_id → v2_3_plans.
The status of the plan behind a flag
v2_3_flags works the same way, and for the same reason: it carries keys, not copies. It has no customer_plan_status, customer_plan_starts_at, or customer_plan_ended_at — those were removed along with plan_id — so a flag’s plan status comes from the same double join on internal_customer_product_id.
Only
v2_3_subscriptions has a status column — a one-off purchase has no lifecycle states, so its liveness is expires_at. That’s why the COALESCE falls back to a literal rather than to pu.status. To keep only flags granted by a live plan, filter s.status = 'active' or an unexpired purchase, rather than reaching for a single status column.Use internal_customer_product_id as the join key
A customer plan is a customer’s own instance of a plan, and it is either a recurring subscription or a one-off purchase — never both, never neither-but-something-else. So internal_customer_product_id on v2_3_breakdowns, v2_3_rollovers, and v2_3_flags points at v2_3_subscriptions.internal_id or v2_3_purchases.internal_id, and exactly one of the two joins matches. Left-join both and let the null tell you which kind it is:
internal_product_id to do this job. It identifies the shared plan version, so matching on it gives you every customer who ever held that plan; you’d have to add the customer id and still couldn’t tell two spells of the same plan apart. internal_customer_product_id is the customer’s own row, and it is unique.
What balances did this customer have on their last plan
“Last plan” is the most recently started customer plan. Take it fromv2_3_subscriptions by started_at, then pull every breakdown that hangs off it.
Two adjustments depending on what you mean. If the customer’s most recent plan might be a one-off, build
last_plan as a UNION ALL over v2_3_subscriptions and v2_3_purchases before the ORDER BY started_at DESC LIMIT 1. And this still sums every reset cycle that plan lived through — add AND b.reset_resets_at = (the current cycle) per the active + current-cycle filter, and fold in rollovers, if you want the position as of now rather than the plan’s whole history.Rows whose next reset is in the past
Useful as a health check. Onv2_3_balances, next_reset_at is the earliest reset across every entitlement in the group, so a past value usually means the group still contains stale entitlement rows from cycles that have already rolled — not that a reset is overdue.
v2_3_breakdowns.reset_resets_at — that column is per-entitlement, so a past value there identifies exactly which rows the current-cycle filter would drop.
Cross-database joins
You can join your Lakehouse tables against your own data living elsewhere in the same engine.- ClickHouse
- BigQuery
Qualify each side fully — the Iceberg catalog table and your own ClickHouse table:
The catalog connection is best for ad-hoc and bounded queries. For very large scans, filter early (by
env, time range, or id) or materialize a subset into a native table first.Important notes
Use internal ids, not external ids
This is the single most important rule for reliable queries.- Each plan version is its own row in
v2_3_plans, with its owninternal_id. The only identifier shared across versions is the externalplan_id. - External ids (
plan_id,customer_id,feature_id,entity_id, …) are mutable — you can rename them in Autumn at any time — and a single external id can map to multiple versioned rows.
- Join and filter on
internal_idand theinternal_*foreign keys (internal_customer_id,internal_feature_id,internal_product_id,internal_entity_id). These are immutable and globally unique. - The balances lane (
v2_3_breakdowns,v2_3_balances,v2_3_rollovers,v2_3_flags) uses the same keys as every other table:internal_product_id(→v2_3_plans),internal_plan_item_id(→v2_3_plan_items), andinternal_customer_product_id(→v2_3_subscriptionsorv2_3_purchases). Reach forinternal_customer_product_idwhenever you need the customer’s own plan row — status, period, cancellation — rather than the shared plan version. - Treat external ids as display-only — great for human-readable output, unreliable as join or lookup keys.
Pooled vs per-entity rows
v2_3_balances, v2_3_breakdowns, and v2_3_flags contain two kinds of row:
- Pooled (customer-scoped) —
entity_idis null. - Per-entity —
entity_idis set.
v2_3_balances and v2_3_breakdowns the two are disjoint: an entitlement produces exactly one row, under whichever scope it holds. So coalesce(entity_id, '') = '' gives you the customer-scoped rows and excludes entity-scoped ones — it isn’t a dedup. If you want a customer’s true total across both, sum them all and don’t filter on scope at all. To analyze one entity, filter on its entity_id (or internal_entity_id).
v2_3_flags is different: an entity-scoped flag is written twice, once at its own entity scope and once into the customer pool, so there coalesce(entity_id, '') = '' genuinely is the dedup.
Customers with config.disable_pooled_balance track per-entity rather than pooled — for them, sum the per-entity rows instead of reading the pooled row. See Working with balances for what the aggregated values mean and the active + current-cycle filter you need before trusting them.