You ran what felt like a small query and BigQuery reports it processed 4 TB. Since on-demand pricing bills by bytes scanned, that is a real charge for what you thought was a quick look. The scan size is almost always the result of a few avoidable habits.
SELECT * Reads Every Column
BigQuery is columnar: it only reads the columns you name. SELECT * reads all of them, including the giant JSON blob you didn't need:
-- Scans every column in every row SELECT * FROM events WHERE action = 'purchase'; -- Scans only two columns SELECT user_id, amount FROM events WHERE action = 'purchase';
Naming the columns you actually use is the single biggest lever on scan size.
Filtering the Wrong Column Skips Pruning
If the table is partitioned by date but you filter on something else, BigQuery can't prune and scans the whole table. Always include the partition column in your WHERE clause:
-- No partition filter โ full scan SELECT COUNT(*) FROM events WHERE action = 'purchase'; -- Partition filter โ scans only matching days SELECT COUNT(*) FROM events WHERE DATE(event_time) BETWEEN '2026-05-01' AND '2026-05-07' AND action = 'purchase';
Previewing Data the Expensive Way
Running SELECT * ... LIMIT 10 still scans the underlying data before limiting โ LIMIT does not reduce bytes scanned. To eyeball a table for free, use the table preview in the console or bq head, which don't incur query cost.
Estimate Before You Run
Every query can be dry-run to see its cost first:
bq query --dry_run --use_legacy_sql=false 'SELECT ...' # Shows bytes processed with no charge
Make this a reflex on anything touching a large table.
Summary
Runaway scan size comes from reading columns you don't need, missing the partition filter, and assuming LIMIT saves money. Name your columns, always filter on the partition key, preview tables with free tools, and dry-run before running. Scan less and the bill takes care of itself.