Skip to main content
This page = whole catalog, one page, for model that write query. Nothing link out. Human? Read Human’s manual instead. Page written in caveman format. Few token, same rule. No article, no auxiliary verb, no hedge, no transition. Identifier, type, number, negation stay exact. Only grammar go. Read once. Then build table name yourself. No lookup. Name tell you grain, keys, time column, value columns. Name never lie.

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_dailyproject_id, timestamp, fees. Done. No page needed. Do NOT select column the name not promise. Levels roll up: app_chainsappsprojects. asset_tokensassets. 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.
  1. Bound time column. ALWAYS. No bound = read whole table. You pay, not us. Facts use block_timestamp. Metrics + reports use timestamp.
  2. 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_address for token facts. Read the keys from INFORMATION_SCHEMA.COLUMNS: is_partitioning_column and clustering_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.
  3. Partition column naked on left. date(block_timestamp) >= x = read every partition. Function hide column from pruner. Write block_timestamp >= timestamp('2026-01-01').
  4. Bound BOTH sides of join. Bound not travel across join. Two tables, two bounds.
  5. One token = filter token_address + chain_id. NOT token_id. Same rows, but token_address is the cluster key and chain_id drops whole chains at plan time. token_id is derived, reaches no cluster key, and reads every chain: terabytes against megabytes. Keep token_id for join to dimensions.tokens.
  6. Set maximum_bytes_billed on 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>, or maximumBytesBilled in the job config of any client library. Set it low, raise it when the failure tells you to.
  7. 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. limit changes NOTHING: limit 10 reads the same bytes as no limit. Naming columns DOES cut the bill, because storage is columnar. Never select *.
  8. No recursive CTE. No unbounded self-join. with recursive reads 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.
  9. 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.
  10. Get unit from dimensions.metrics BEFORE format. Unit = usd, count, ratio, pct. Ratio = fraction: apy 0.05 = 5 percent, off_peg -0.02 = 2 percent under peg. NEVER assume dollar.
  11. Sum only how dimensions.metrics allow.
  • 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.
  1. Address case matter. EVM stored lowercase: wrap explorer address in lower(). Solana base58, case-sensitive, match verbatim.
  2. value_raw = STRING. uint256 overflow every BigQuery number type. Cast BIGNUMERIC, divide by pow(10, decimals) from dimensions.tokens.
  3. Financial statements long, NOT wide. reports_projects.financial_statements_* have metric_id + value. One statement = many row. Filter line item, or pivot with max(if(metric_id = ...)).
  4. 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

Access

Catalog = BigQuery share in caller own Google Cloud project. Caller pay compute. No public endpoint. No anonymous access. Missing dataset error = share not set up. Human must arrange. Say so. Do NOT retry.