# How BigQuery works > BigQuery keeps storage and compute separate, stores each column on its own, and runs a query as parallel stages on slots. What that means for what you pay, why there are no indexes to tune, and what a slot is. Source: https://docs.querylab.io/bigquery/how-bigquery-works/ Updated: 2026-10-14 BigQuery keeps your data in one managed storage system and runs queries on a separate pool of compute. The two scale independently and are billed separately. Tables are stored by column, so a query reads only the columns it names. A query is split into stages, and each stage runs as many parallel pieces of work on units of compute called slots. ## Storage and compute are separate When you load data, BigQuery allocates the storage for you. There is no disk to provision and no cluster to size. Google describes the storage as designed for 99.999999999% annual durability, and it replicates data across multiple availability zones. All data is encrypted before it is written to disk. When you run a query, a query engine hands the work to many workers. They scan the tables in storage, process their share, and pass results along. Google says the whole execution happens in memory, over a petabit network that moves data to the workers. Storage and compute are priced on separate meters: [storage pricing](https://docs.querylab.io/bigquery/storage-pricing/) charges for bytes kept, and [on-demand or capacity pricing](https://docs.querylab.io/bigquery/on-demand-vs-capacity-pricing/) charges for the query. ## Data is stored by column A row-oriented database keeps each row together, which is fast for reading one record. BigQuery stores each column separately. A query that sums one column of a million-row table reads that column and leaves the rest of the table alone. Data in one column also tends to repeat (the same country code a million times), so it compresses well. Google's own name for the storage format is Capacitor. You can see this in the byte estimate. The table `bigquery-public-data.samples.shakespeare` has 164,656 rows. Reading its integer column `word_count`: **one INT64 column** ```sql SELECT word_count FROM `bigquery-public-data.samples.shakespeare` ``` BigQuery dry run: this query processes 1.3 MB. The estimate is 1,317,248 bytes, which is 164,656 rows times 8 bytes. Reading the string column `word` instead: **one STRING column** ```sql SELECT word FROM `bigquery-public-data.samples.shakespeare` ``` BigQuery dry run: this query processes 1.3 MB. costs 1,332,943 bytes, because a string is counted as 2 bytes plus its length in UTF-8. Reading both: **both columns** ```sql SELECT word, word_count FROM `bigquery-public-data.samples.shakespeare` ``` BigQuery dry run: this query processes 2.5 MB. costs 2,650,191 bytes, exactly the sum of the two. The estimate follows the columns you name, nothing else. Filtering rows does not make a column cheaper, because the filter still reads its column: **selecting one column, filtering on another** ```sql SELECT word FROM `bigquery-public-data.samples.shakespeare` WHERE corpus = 'hamlet' ``` BigQuery dry run: this query processes 3.6 MB. This reads `word` and `corpus`, 3,797,568 bytes, more than `word` alone. The rules for what counts, including `LIMIT`, are in [how queries are billed](https://docs.querylab.io/bigquery/how-queries-are-billed/). ## How a query runs BigQuery turns the SQL into an execution plan made of stages. A stage is a set of steps that can run in parallel, such as reading a table, filtering, or aggregating. Stages pass data to the next stage through shuffle, a fast distributed memory layer that holds the intermediate results. If a query moves a lot of data between stages, shuffle can spill to disk, which the plan reports as `shuffleOutputBytesSpilled`. The plan is not fixed. While a query runs, BigQuery can change it based on what earlier stages produced. When the query finishes, the result is written to persistent storage before it is returned, which is what lets BigQuery serve [cached results](https://docs.querylab.io/bigquery/query-cache/) to the same query later. The stages, steps and shuffle statistics of a finished query are in the execution graph and in the job's metadata. See [how a query runs](https://docs.querylab.io/bigquery/query-execution/). Querylab.io's [execution plan](https://docs.querylab.io/writing-sql/execution-plan/) draws a finished query's stages as a data-flow chart, with the rows each stage read and wrote, its shuffle bytes and slot time, and a BOTTLENECK tag on the slowest stage. ## What a slot is A slot is a virtual compute unit that BigQuery uses to run a query. Google's definition abstracts compute, memory and I/O. Slots run the individual units of work of each stage in parallel. BigQuery decides how many slots a query uses, based on the amount of data, the complexity of the query and the slots available. You can't set it per query. What you do control is how many slots are available and how you pay for them. On-demand pricing bills by bytes scanned, and each project is subject to a maximum concurrent slots limit. Capacity pricing (editions) lets you buy slots. See [slots](https://docs.querylab.io/bigquery/slots/) and [reservations](https://docs.querylab.io/bigquery/reservations/). Each finished job records how many slot milliseconds it used. This is real metadata from your own project, so the query is shown here and not run: ```sql -- Not run: reads your project's job history SELECT job_id, total_bytes_processed, total_slot_ms, TIMESTAMP_DIFF(end_time, start_time, MILLISECOND) AS elapsed_ms FROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT WHERE job_type = 'QUERY' AND creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY) ORDER BY creation_time DESC LIMIT 10; ``` `total_slot_ms` is the slot time the job used, summed over all its workers, so it can be larger than `elapsed_ms`. Change `region-us` to your data's location; see [locations](https://docs.querylab.io/bigquery/locations/). ## Why there are no indexes to tune You don't create indexes to make ordinary queries fast. BigQuery doesn't index primary keys, and Google's join advice follows from that: the wider the join column, the longer the comparison takes, so join on `INT64` where you can. Query speed comes from reading fewer columns, reading fewer partitions with [partitioning](https://docs.querylab.io/bigquery/partitioning/) and [clustering](https://docs.querylab.io/bigquery/clustering/), and having enough slots. BigQuery does have two optional index types: [search indexes](https://docs.querylab.io/bigquery/search/) for finding a value in text, and [vector indexes](https://docs.querylab.io/bigquery/vector-search/) for nearest-neighbor search. ## Consequences for writing queries - Name the columns you need. `SELECT *` reads everything. - Narrow the data before you join and aggregate, since both move data through shuffle. In Querylab.io the editor shows BigQuery's dry-run estimate for the query you are writing, the same number as the dry runs above. See [cost limits](https://docs.querylab.io/writing-sql/cost-limits/). Open the playground at https://app.querylab.io?playground=true&utm_source=docs&utm_medium=concepts to run it. It is the real app on public BigQuery datasets and needs no sign-in. ## Sources Based on the BigQuery documentation by Google, CC BY 4.0, rewritten with examples checked against BigQuery: - [Overview of BigQuery storage](https://cloud.google.com/bigquery/docs/storage_overview) - [Understand slots](https://cloud.google.com/bigquery/docs/slots) - [Query plan explanation](https://cloud.google.com/bigquery/docs/query-plan-explanation) - [Introduction to optimizing query performance](https://cloud.google.com/bigquery/docs/best-practices-performance-overview)