Data structures and storage engines
How a SQL query executes
How a database plans and runs a query, which access paths and join algorithms it chooses between, and how to read EXPLAIN ANALYZE when a query is slow. Start here for query performance.
You write SELECT … JOIN … WHERE … and rows come back. In between, the database parses the text, estimates how many rows each step will produce, picks among many equivalent plans, and runs them against the stored pages. A query that took 5 ms last month can take 5 seconds today. Usually the plan changed. The estimates drifted, an index stopped being useful, or an ORM started sending many small queries.
This topic maps that pipeline, so you can see why a plan changed and what to do about it.
The big idea
The planner picks a plan from estimates, not from the real data. Three things decide the plan's speed.
- Access paths: how each table's rows are found, by scanning or by an index.
- Join algorithms: how two inputs are matched up.
- Statistics: the estimates of how many rows each step will produce.
You control the plan through indexes, statistics and the shape of the query. The answer to a slow query is almost never "make the database faster". It is usually "give the planner a better choice".
Concepts in this section
Read them in this order.
| Step | Concept | What it answers |
|---|---|---|
| 1 | Query plans and execution | How does SQL become a tree of steps, and what does EXPLAIN show? |
| 2 | Access paths and selectivity | When does an index help, and when does a scan win? |
| 3 | Join algorithms | Which of nested loop, hash join or merge join is cheapest here? |
| 4 | Statistics and cardinality | Why are the row estimates wrong, and how do I refresh them? |
| 5 | Reading EXPLAIN ANALYZE | How do I find the slow step in a real plan and fix it? |
Row stores and column stores
Where the data is laid out affects which queries are cheap.
| Store | Layout | Good at | Example |
|---|---|---|---|
| Row store | Each page holds whole rows together | Fetching one user or order, with all columns in one read | Postgres, MySQL |
| Column store | Each column is stored and compressed separately | Scanning a few columns of many rows, such as sums and counts | ClickHouse, BigQuery, Snowflake, Redshift |
A query like SELECT sum(amount) FROM events WHERE day = … reads only two columns in a column store. A row store has to read the whole row for every event.
Use a row store for high-volume lookups and writes. Use a column store, or a copy of the data in a warehouse, for big scans and reports. Running heavy GROUP BY queries on the primary database takes buffer memory and write bandwidth away from user traffic. See transactions and isolation for what the primary must guarantee.
Common mistakes
- Adding indexes before checking the plan. Compare estimate with actual first.
- Letting the ORM choose the queries. Count the queries per request, and check for N+1 patterns.
- Running reports on the primary. Move them to a replica or a warehouse.
- Wrapping indexed columns in functions. Use a range or an expression index.
Key takeaways
- SQL becomes a plan, a tree of steps chosen by estimated cost. The executor runs it one row at a time.
- Access paths decide how rows are found. An index helps only when the condition is selective.
- Join algorithms depend on sizes, sort order and indexes. The planner chooses from row estimates.
- Bad estimates cause bad plans. Refresh statistics, and create extended statistics for correlated columns.
- Read the plan bottom up, and fix the step with the most actual time or reads.
- Row stores serve point lookups and writes. Column stores serve big scans.
Further learning
A short walkthrough of reading a real Postgres plan tree, with its costs and buffer hits:
Use The Index, Luke covers index design with a worked example for each pattern.
CMU 15-445 Database SystemsThis is Carnegie Mellon UniversityCMU 15-445/645
Related
Storage engines: pages, B-trees and the WAL
How a relational database stores rows in fixed-size pages, finds them in a B+tree, caches them in a buffer pool, and survives crashes with a write-ahead log.
Transactions, MVCC and isolation levels
What ACID guarantees on one database node, which concurrency bugs each isolation level still allows, how MVCC gives snapshots without blocking, and the code patterns that keep concurrent writes correct. Start here for transactions.
Databases and data modeling
Choose between relational, document, wide-column, key-value, graph and analytical stores by listing your access patterns first, then model the data to fit them.
Product analytics
Designing event tracking plans, reading funnels and cohorts, writing the SQL that answers product questions, and avoiding mix-shift illusions. Start here for the map.