LIMIT 10 doesn't make it cheaper: BigQuery bills the bytes it reads, not the rows it returns
A SELECT * with LIMIT 10 returns ten rows and can still bill the whole table. How BigQuery stores and reads data, and how to make every query read less.
- A SELECT * with LIMIT 10 on a 2 TiB table. The console returns 10 rows.
- But BigQuery stores data by column and reads every column you select, top to bottom: all six.
- On demand you're billed for bytes scanned, at $6.25 per TiB. The 10 rows don't count: $12.50 every run.
- The cause: LIMIT only trims the result. On a non-clustered table the bytes read stay the same.
- The fix: ask for three columns and filter seven days of the partition. The scan drops to 6 GiB.
- Check it before you run: the editor shows how many bytes the query will process.
You open the GA4 export or a sales table in BigQuery, write SELECT * FROM shop.events LIMIT 10 to see what’s in it, and get ten rows back in a couple of seconds. It looks like a tiny query. On the bill it isn’t: on the on-demand model, BigQuery charges for the bytes it reads, and ten rows of output can mean reading every column of the table from top to bottom.
What an expensive “small” query looks like
The query returns ten rows, but the Job information tab says something else:
- Bytes processed and Bytes billed show the size of the selected columns, not the size of the result. On a 2 TiB table with
SELECT *, that’s 2 TiB. - The editor already warned you before running: This query will process 2 TiB when run. Adding
LIMIT 10doesn’t change that figure. - At the end of the month, the BigQuery line on the bill grows with queries that “only looked at a few rows”, often from scheduled reports or dashboards that run the same thing every hour.
Why BigQuery reads the whole table
The bill follows from how BigQuery is built.
- Storage is columnar. BigQuery doesn’t store rows one after another. Each column is stored separately, compressed, in a format called Capacitor. A query reads only the columns it names, and reads them whole:
SELECT *names all of them. - Storage and compute are separate. Tables live on Colossus, Google’s distributed file system. When you hit Run, slots (BigQuery’s virtual units of compute) read the data in parallel over Google’s network.
- Your SQL becomes a plan of stages. Each slot scans and filters its own chunk, a shuffle regroups the partial results and a final stage merges them into one answer. You can see it in the Execution graph tab.
LIMITis applied at the end, to the result: by then the columns have already been read. - On-demand pricing counts bytes scanned. $6.25 per TiB at the list price (some regions differ), with the first 1 TiB per month free. The bytes are logical bytes: the size of each value by data type, not the compressed size on disk. Rows returned don’t affect the price.
Google’s cost guide says it plainly: on a non-clustered table, applying LIMIT doesn’t reduce the bytes read. On a clustered table it can, because BigQuery can stop scanning blocks early. Cached results are free, and with capacity pricing (BigQuery editions) you pay for slot-hours instead of bytes, so the same query costs differently there.
How to see what a query really reads
- Before running, look at the estimate in the top right corner of the editor: This query will process X when run. Remove
LIMITand it doesn’t change; remove columns and it does. - After running, open Job information and compare Bytes processed with Bytes billed (billing rounds up to the MB, with a 10 MB minimum per table referenced and per query).
- To find the queries that cost the most, look up
total_bytes_billedinINFORMATION_SCHEMA.JOBSfor your project or region, sorted from highest to lowest. Scheduled queries and dashboards repeat their cost on every run.
How to make every query read less
- Name only the columns you need.
SELECT event_date, event_name, revenuereads three columns instead of all of them. If you only want to see what a table contains, use the table’s Preview tab, which doesn’t bill bytes. - Filter on the partition. On a table partitioned by date, a filter on the partitioning column with fixed values (
WHERE event_date BETWEEN '2026-10-22' AND '2026-10-28') only reads those partitions. You can make that filter mandatory with therequire_partition_filteroption. The GA4 export is sharded by day instead of partitioned: there the equivalent is filtering_TABLE_SUFFIX, which is the next post in this series. - Cluster big tables on the columns you filter by most (for example
event_name), so BigQuery can skip blocks that don’t match. - Check the estimate before you hit Run. For queries that run on their own, use a dry run and set Maximum bytes billed: if a query would read more, it fails without being billed.
Google Cloud documentation: BigQuery pricing, Estimate and control costs, Query plan and timeline and BigQuery under the hood.
How to check the query reads less
- The editor’s estimate falls when you remove columns and add the partition filter: in the example, from 2 TiB to 6 GiB.
- Bytes billed in Job information matches the estimate once the query has run.
- In
INFORMATION_SCHEMA.JOBS,total_bytes_billedfor the scheduled queries and dashboards you changed goes down from the next run.
The table size, the byte figures and the cost per run in the example are illustrative.