Data structures and storage engines
Query plans and execution
How a database turns SQL text into a tree of operators, picks the cheapest one, and runs it row by row. Introduces the query plan and the difference between EXPLAIN and EXPLAIN ANALYZE.
When you write SELECT … JOIN … WHERE …, the database does not run your text directly. It turns the text into a plan: a tree of steps that fetch, join, sort and count rows. Many plans can give the same answer, and they can differ in speed by a factor of thousands. Knowing how the plan is built is the first step to fixing a slow query.
Concept
Query plan: a tree of operators such as scan, join, sort and aggregate. Each operator takes rows from its children and passes rows up to its parent. The planner picks the tree with the lowest estimated cost. The executor runs it.
The big idea: four stages
A query passes through four stages, in order.
- Parse. The SQL string becomes a parse tree. Syntax errors stop here.
- Rewrite. Views are expanded into their underlying queries, and
*is expanded into column names. Types and functions are resolved. The output is a query tree. - Plan. The planner lists ways to find each table's rows and orders for the joins. It gives each option an estimated cost, in disk pages and CPU, and keeps the cheapest.
- Execute. The plan runs. Most engines use the iterator (Volcano) model. Each operator has a
next()call that returns one row, and a parent pulls rows from its children one at a time.
Pulling one row at a time keeps memory low. A few operators, such as a sort or the build side of a hash join, must collect all their input before they can return anything.
Walking through a small plan
Here is a simple query on two tables, and the plan that a database might choose.
SELECT c.name, o.total_cents
FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'paid';Hash Join ← build a hash table on customers, probe with orders
-> Seq Scan on orders ← read every order row, keep status = 'paid'
-> Seq Scan on customers ← read every customer row into the hash tableRead it bottom up for data flow: the scans produce rows, and the join consumes them. Read it top down for control: the top node asks for rows, and the requests flow down.
Two ways to see a plan
EXPLAINshows the plan the planner would run. It does not run the query, so it is safe and fast.EXPLAIN ANALYZEruns the query and adds real time and real row counts to each node.
Warning
EXPLAIN ANALYZE on an UPDATE or DELETE really changes the data. Wrap it in a transaction you can roll back, or run it on a staging copy.
Common mistakes
- Reading the plan from the top only. Data flows from the leaves up, so the slow step is often a leaf.
- Assuming
EXPLAINshows real timings. It shows estimates. UseANALYZEto see what happened. - Running
EXPLAIN ANALYZEon production writes. It executes the statement.
Check yourself
- Why does the executor pull rows one at a time instead of building every intermediate result? Pulling keeps memory low, because each operator passes rows along as soon as they are ready.
- What is the difference between
EXPLAINandEXPLAIN ANALYZE?EXPLAINshows the planned tree with estimates.EXPLAIN ANALYZEruns the query and adds real time and row counts.
Key takeaways
- SQL runs through parse, rewrite, plan and execute. Performance is decided mainly in the plan and in I/O.
- A plan is a tree of operators. Data flows from the leaves to the root.
- The planner chooses by estimated cost, so the quality of its estimates matters.
EXPLAINshows the plan.EXPLAIN ANALYZEshows what actually happened.
Further learning
Next, read access paths and selectivity to see how each table's rows are found, and why an index is not always the winner.
PostgreSQL docs: Using EXPLAIN14.1. Using EXPLAIN # 14.1.1. EXPLAIN Basics 14.1.2. EXPLAIN ANALYZE 14.1.3. Caveats PostgreSQL devises a query plan for each query it …PostgreSQL Documentation
Related
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.
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.