How it works
The explanation, with examples you can run.
The four stages
- Parse. Check the syntax and build a tree; look up the tables and columns it names.
- Rewrite. Apply rules. A query on a view becomes a query on the view's underlying tables here.
- Plan. Generate candidate plans (scan each table this way or that, join in this order with this method) and estimate the cost of each. Keep the cheapest.
- Execute. Walk the plan tree. Each node pulls rows from its children and hands rows to its parent, so a
LIMIT 10at the top can stop the work early.
Each client connection has its own backend process doing all of this. That is one reason connections are not free, which comes back in the pooling topic.
Cost, statistics and ANALYZE
The planner cannot run every candidate to see which is fastest. It estimates. For each table it knows roughly how many rows and pages there are; for each column, the most common values, a histogram of the rest, and the fraction of nulls. These live in pg_statistic (readable through the pg_stats view) and are refreshed by ANALYZE, which autovacuum also runs in the background.
ANALYZE orders; SELECT attname, n_distinct, most_common_valsFROM pg_statsWHERE tablename = 'orders' AND attname = 'status';From these numbers it estimates how many rows each step will produce, and from that the cost in abstract units (by default, reading one page sequentially costs 1.0 and a random page 4.0). If the estimate says a condition matches 25% of the table, reading the whole table in order is usually cheaper than millions of random index lookups. If it matches 25 rows, the index wins. When estimates are wrong, for example right after a bulk load, or when two columns are correlated in ways single-column statistics cannot see, the planner can pick a plan that is cheap on paper and slow in practice.
Planning once, running many times
Planning costs time too, so drivers often use prepared statements: parse once, then execute with different parameters.
PREPARE orders_for(bigint) AS SELECT id, total_cents FROM orders WHERE customer_id = $1; EXECUTE orders_for(42);By default the first five executions are planned for the actual parameter values (custom plans). After that PostgreSQL also builds a generic plan and uses it if its estimated cost is not worse than the average of the custom ones. That is usually a good trade, but if one parameter value matches half the table and another matches three rows, a single generic plan suits only one of them. plan_cache_mode lets you force either behaviour when you see this.
The next topic is how to see the plan the planner picked, and how close its estimates were.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.