Build table name
Name =dataset.table. Dataset say what kind, what about. Table say rest.
Metrics table = ONE measure. Columns = level ids +
timestamp + one value column. Value column name = table name minus period.
metrics_projects.fees_daily → project_id, timestamp, fees. Done. No page needed. Do NOT select column the name not promise.
Levels roll up: app_chains → apps → projects. asset_tokens → assets. Use biggest level that answer question. Rollup already exist. Cheaper. Already correct.
Rules. Break rule = wrong answer or big bill.
All these seen for real.-
Bound time column. ALWAYS. No bound = read whole table. You pay, not us. Facts use
block_timestamp. Metrics + reports usetimestamp. -
Time bound alone is NOT enough. Not every table is partitioned, and a bound on an unpartitioned column prunes nothing. Every table is clustered, so also filter its cluster key: the entity id for a metrics table,
token_addressfor token facts. Read the keys fromINFORMATION_SCHEMA.COLUMNS:is_partitioning_columnandclustering_ordinal_position. Filter the leading cluster column first, order matters. Empty result = the object is a VIEW, which reports no keys; take its contract from its page. -
Partition column naked on left.
date(block_timestamp) >= x= read every partition. Function hide column from pruner. Writeblock_timestamp >= timestamp('2026-01-01'). - Bound BOTH sides of join. Bound not travel across join. Two tables, two bounds.
-
One token = filter
token_address+chain_id. NOTtoken_id. Same rows, buttoken_addressis the cluster key andchain_iddrops whole chains at plan time.token_idis derived, reaches no cluster key, and reads every chain: terabytes against megabytes. Keeptoken_idfor join todimensions.tokens. -
Set
maximum_bytes_billedon EVERY job. Over the cap, the query fails before it runs and bills nothing; the error names the bytes it needed. Wrong query costs you an error, not money.bq query --maximum_bytes_billed=<bytes>, ormaximumBytesBilledin the job config of any client library. Set it low, raise it when the failure tells you to. -
Bytes scanned is the bill, NOT rows returned. BigQuery on-demand charges per byte the query reads, at a rate per TiB, with a 10 MiB floor per table per query.
limitchanges NOTHING:limit 10reads the same bytes as no limit. Naming columns DOES cut the bill, because storage is columnar. Neverselect *. -
No recursive CTE. No unbounded self-join.
with recursivereads again every step and the bytes compound; a fan-out join multiplies rows before you see them. Size the join with a count first. A question that seems to need recursion wants a smaller question. - Empty ≠ zero. Empty = no figure exist for that measure, that thing, that day. Do NOT coalesce 0 then average or sum across. Do NOT call it decline.
-
Get unit from
dimensions.metricsBEFORE format. Unit =usd,count,ratio,pct. Ratio = fraction:apy0.05 = 5 percent,off_peg-0.02 = 2 percent under peg. NEVER assume dollar. -
Sum only how
dimensions.metricsallow.
- Additive (fees, volume): sum across day, sum across thing. Both fine.
- Semi-additive (TVL, supply, open interest): sum across thing. NEVER across day. Week = last day of week.
- Non-additive (APY, active users, share, funding rate): sum NEITHER way. Week active users = count distinct address over week from fact table. Add 7 daily counts = count same person 7 time. Wrong.
-
Address case matter. EVM stored lowercase: wrap explorer address in
lower(). Solana base58, case-sensitive, match verbatim. -
value_raw= STRING.uint256overflow every BigQuery number type. CastBIGNUMERIC, divide bypow(10, decimals)fromdimensions.tokens. -
Financial statements long, NOT wide.
reports_projects.financial_statements_*havemetric_id+value. One statement = many row. Filter line item, or pivot withmax(if(metric_id = ...)). - NEVER query table from Not served yet list. Those are spec, not table. Tell user data not available. Do NOT write query that cannot run.
Join keys
Id = lowercase name. Combined id = parts joined with dash. Every table carry at least one. That is why catalog join.
Asset = one identity, all chains. Token = one contract, one chain. USDC = one row
dimensions.assets, one row per chain dimensions.asset_tokens.
Question → table
Shortest correct path for common question. Prefer metrics table over sum facts table yourself: smaller, and additivity already settled.All tables
Every table in catalog. Generated from pages, so complete at build time.Dimensions
dimensions.accounts, dimensions.apps, dimensions.asset_tokens, dimensions.assets, dimensions.chains, dimensions.contracts, dimensions.dex_pools, dimensions.lending_market_reserves, dimensions.lending_markets, dimensions.metric_instances, dimensions.metrics, dimensions.order_book_markets, dimensions.perp_markets, dimensions.projects, dimensions.reference_assets, dimensions.tokens, dimensions.transfer_purposes
Facts
Metrics
Reports
Functions
functions.calculate_historical_eod_token_balances, functions.calculate_latest_token_balances
Not served yet
Name documented before table land. Page = spec. Table NOT exist. Query these = fail.dimensions.transfer_purposes, facts_chains.transactions, facts_dex_pools.trades, facts_lending_markets.borrows, facts_lending_markets.deposits, facts_lending_markets.flash_loans, facts_lending_markets.liquidations, facts_lending_markets.repayments, facts_lending_markets.withdrawals