ApiaryActiveLive
Try: pause · settings · learn · wipe
← Community / Reading Room
UC
databases · 13 min read

Understanding Cost‑Based Optimizers

Modern data‑driven applications depend on relational database management systems (RDBMS) to turn raw tables into the answers users need—whether that’s a list…

Modern data‑driven applications depend on relational database management systems (RDBMS) to turn raw tables into the answers users need—whether that’s a list of endangered bee habitats, a real‑time inventory of pollination equipment, or a massive analytics report for a conservation NGO. Behind every SELECT you type lies a sophisticated decision‑maker: the cost‑based optimizer (CBO). It evaluates dozens, sometimes hundreds, of alternative execution strategies and picks the one it predicts will consume the fewest resources.

Why does this matter? A sub‑optimal plan can turn a query that finishes in a few milliseconds into a minute‑long operation, inflating cloud bills, throttling API throughput, and—ironically—draining the very computational “honey” that AI agents need to self‑govern. Conversely, a well‑tuned optimizer can squeeze out performance gains that rival hardware upgrades, letting conservation platforms like Apiary serve more researchers, citizen scientists, and automated agents without adding servers.

In this pillar article we’ll peel back the layers of modern CBOs, explore the concrete mechanisms that turn statistics into cost estimates, and give you practical levers to influence the optimizer’s choices. Along the way we’ll sprinkle real‑world numbers, concrete examples from PostgreSQL, MySQL, Oracle, and SQL Server, and even draw honest parallels to bee colonies and self‑governing AI agents—because the principles of efficient resource allocation are universal.


1. The Core Idea: What a Cost‑Based Optimizer Actually Does

At its simplest, a CBO answers the question “Which plan will likely run fastest?” It does not magically know the future; instead it builds a model of the database’s physical layout, the hardware’s performance characteristics, and the statistical distribution of the data. The optimizer then:

  1. Generates candidate plans – different ways to access tables, order joins, and apply filters.
  2. Estimates the cost of each plan – using a formula that combines I/O, CPU, memory, and sometimes network usage.
  3. Ranks the plans – the plan with the lowest estimated cost is chosen for execution.

The term cost is deliberately abstract. In PostgreSQL, for example, the default cost units are “disk page fetches” (a page is typically 8 KB). In Oracle, a cost is a dimensionless number derived from internal cost formulas, often scaled so that a full table scan of a 1 GB table costs roughly 10 000. The optimizer’s goal is not to predict wall‑clock time in seconds, but to maintain a consistent ordering of plans so the cheapest one is usually the fastest.

Concrete Example

Consider a simple query on a pollination_events table (10 million rows, 12 GB) that joins to a bees table (2 million rows, 3 GB):

SELECT b.species, COUNT(*) AS events
FROM pollination_events p
JOIN bees b ON p.bee_id = b.id
WHERE p.event_date BETWEEN '2024-01-01' AND '2024-06-30'
GROUP BY b.species;

Two plausible plans:

PlanAccess PathJoin MethodEstimated Cost
ASeq Scan on pollination_events → Hash Join → Seq Scan on beesHash Join8 200
BIndex Scan on pollination_events(event_date) → Nested Loop → Index Scan on bees(id)Nested Loop5 600

Even though a sequential scan reads the whole 12 GB, the optimizer may still favor it if the index on event_date is low‑selectivity (e.g., returns 70 % of rows). The hash join’s one‑pass build phase can be cheaper than repeatedly probing an index for each row in a nested loop. The optimizer’s cost model captures that trade‑off numerically.


2. Anatomy of a Query Plan

A query plan is a tree of operators. Each node describes a physical operation (e.g., Seq Scan, Index Seek, Hash Join) and the resources it consumes. Let’s dissect a typical PostgreSQL plan output:

Hash Join  (cost=1123.45..2356.78 rows=12345 width=64)
  Hash Cond: (p.bee_id = b.id)
  ->  Index Scan using pollination_events_event_date_idx on pollination_events p  (cost=0.42..1023.11 rows=500000 width=32)
        Index Cond: (event_date >= '2024-01-01'::date AND event_date <= '2024-06-30'::date)
  ->  Hash  (cost=800.00..800.00 rows=20000 width=32)
        ->  Seq Scan on bees b  (cost=0.00..800.00 rows=20000 width=32)
  • Cost range (cost=1123.45..2356.78) – The first number is the startup cost (time before the first row can be returned). The second is the total cost (time to return all rows).
  • Rows and width – Estimated row count (rows=500000) and average row size (width=32 bytes). These are derived from statistics.
  • Operator type – Index Scan, Seq Scan, Hash Join, etc. Each has a known cost formula (e.g., Seq Scan cost ≈ seq_page_cost * pages_read).
  • Conditions – Index Cond, Hash Cond indicate which predicates are applied at that node.

The optimizer computes the cost of each node bottom‑up. It first estimates the cost of leaf nodes (scans), then adds the cost of combining them (joins, aggregates). The final plan’s total cost is the sum of all node costs plus any overhead for parallelism or spill‑to‑disk.

Visual Analogy

Think of a bee colony deciding how to collect nectar: each worker (operator) has a known energy cost per flower (I/O per page). The colony evaluates many foraging routes (plans) and picks the one that minimizes total energy expenditure while still gathering enough nectar (rows).


3. Gathering Statistics – The Data the Optimizer Trusts

Statistics are the optimizer’s eyes. Without accurate data about column value distribution, index selectivity, or table size, the cost estimates become guesses. Most RDBMS collect statistics automatically, but the frequency, granularity, and methodology differ.

DBMSStatistic TypesSample SizeRefresh Trigger
PostgreSQLMost‑Common Values (MCV), histograms (up to 10 % of default_statistics_target), pg_class.reltuplesDefault target = 100 rows per column, configurable up to 10 000ANALYZE (auto‑triggered after 10 % of rows change)
MySQL (InnoDB)Index cardinality, column histograms (up to 256 buckets)1000 random rows per partitionANALYZE TABLE or automatic after 10 % change
OracleColumn density, histograms (frequency, hybrid)Up to 100 % sampling for FOR ALL COLUMNS SIZE AUTOAutomatic after DBMS_STATS.GATHER_TABLE_STATS or DBMS auto‑task
SQL ServerHistograms (up to 200 steps), density vector, column statistics100 % for small tables; default sample = 1 % for large tables (capped at 30 000 rows)Auto‑update statistics after 20 % + 500 rows change

How Histograms Work

A histogram partitions a column’s value range into buckets, each storing the number of rows that fall into that bucket. For a numeric column temperature with values from -10 °C to 40 °C, a 10‑bucket histogram might look like:

BucketRange (°C)Row Count
1-10 – -512 000
2-5 – 045 000
30 – 5150 000
………
1035 – 403 000

When the optimizer evaluates a predicate like temperature > 30, it can sum the rows in buckets 9 and 10 and estimate selectivity ≈ (3 000 + 7 000) / total rows. This yields a far more realistic estimate than assuming a uniform distribution.

Keeping Statistics Fresh

  • Thresholds matter – In PostgreSQL, the default auto‑analyze threshold is 0.1 * reltuples + 50. For a 10 million‑row table, that’s 1 000 050 rows. If you load 5 million rows daily, you’ll need to schedule ANALYZE more often.
  • Partial updates – Some systems support incremental statistics (e.g., PostgreSQL 13+ with ALTER TABLE ... ALTER COLUMN ... SET STATISTICS). This reduces overhead on large tables.
  • Skew detection – If a column’s distribution changes dramatically (e.g., a new invasive bee species dominates the species column), the optimizer may still rely on stale MCV lists, leading to wildly inaccurate cost estimates.

4. The Cost Model – From I/O to CPU to Memory

A cost model translates operator characteristics into a numeric cost. Most CBOs decompose cost into three primary resources:

ResourceTypical Cost UnitExample Parameter
Disk I/OPage fetchesseq_page_cost, random_page_cost
CPUTuple processingcpu_tuple_cost, cpu_operator_cost
MemoryBytes spilled or cachedwork_mem, hash_mem_multiplier

4.1 Disk I/O

  • Sequential page cost (seq_page_cost) – The cost of reading a page from a sequential scan. PostgreSQL defaults to 1.0.
  • Random page cost (random_page_cost) – The cost of a random read (e.g., index lookup). Default 4.0 in PostgreSQL, reflecting HDD latency. On SSDs, many DBAs lower this to 1.5 or even 1.0.
  • Formula – For a sequential scan over N pages: cost = N * seq_page_cost. For an index scan: cost = (N_seeks * random_page_cost) + (N_pages * seq_page_cost).

4.2 CPU

CPU cost captures the work per tuple (e.g., evaluating a predicate, computing a projection). PostgreSQL uses:

  • cpu_tuple_cost – default 0.01.
  • cpu_operator_cost – default 0.0025.

A filter WHERE age > 5 on 1 million rows would add 1 000 000 * cpu_operator_cost ≈ 2 500 to the plan cost.

4.3 Memory and Workfiles

When a hash table or sort exceeds work_mem (default 4 MB in PostgreSQL), the operation spills to disk, incurring additional I/O. The optimizer estimates the probability of spill based on row count and width, then adds a penalty factor (often 2–3× the normal I/O cost). In Oracle, the temporary tablespace usage is similarly modeled.

4.4 Putting It All Together

A simplified total cost for a hash join might be:

cost = (cost_left_scan) + (cost_right_scan)
     + (hash_build_cost = rows_right * (cpu_tuple_cost + random_page_cost))
     + (hash_probe_cost = rows_left * cpu_tuple_cost)
     + (spill_penalty if (rows_right * row_width) > work_mem)

Because each term is deterministic, the optimizer can compare two plans analytically without actually executing them.


5. Plan Enumeration – How the Optimizer Explores the Search Space

Enumerating every possible plan is combinatorial: for n tables, there are n! join orders, and each table can be accessed via multiple indexes, scans, or materialized sub‑queries. Different DBMS use different strategies:

DBMSEnumeration StrategyTypical Limits
PostgreSQLDynamic programming (DP) for up to 12 tables; fallback to heuristic for larger queriesDP limited to 12‑way joins (configurable with geqo for larger)
MySQL (5.7+)DP up to 8 tables; then Greedy algorithmoptimizer_switch='join_cache_level=2'
OracleCOST‑BASED DP with a join order pruning threshold (default 10 000)Uses transformation rules to limit explosion
SQL ServerDP + graph‑based search; uses plan cache to reuse prior resultsLimits to 12‑way joins; beyond that, fallback to heuristic

5.1 Dynamic Programming (DP)

DP builds optimal sub‑plans for every subset of tables and re‑uses them. For 4 tables A‑D, the optimizer computes:

  1. Best single‑table access for each (A, B, C, D).
  2. Best two‑table joins (AB, AC, AD, BC, BD, CD) using the single‑table bests.
  3. Best three‑table joins (ABC, ABD, ACD, BCD) using the best two‑table joins.
  4. Finally the best four‑table join (ABCD).

The complexity is O(3^n) in the worst case, but pruning (discarding plans that exceed a cost threshold) dramatically reduces actual work.

5.2 Heuristic / Greedy

When the table count exceeds DP limits, the optimizer falls back to a greedy algorithm: start with the cheapest join pair, then iteratively add the next cheapest table. This is fast but can miss the global optimum. In practice, for star schemas with many dimension tables, greedy plans are often good enough.

5.3 Adaptive and Learning Optimizers

SQL Server’s Adaptive Query Processing (introduced in 2017) can re‑optimize mid‑execution if the actual row count deviates dramatically from estimates. PostgreSQL 14 added incremental sort, allowing the planner to defer full sort cost until needed. Oracle’s Auto‑Optimizer Statistics Collection continuously gathers column statistics in the background, feeding fresher data into the model.


6. Real‑World Case Studies

6.1 PostgreSQL: The Effect of random_page_cost on Index Usage

A dev team at a bee‑tracking startup noticed a query that filtered on location_id (highly selective) was still doing a sequential scan. Their EXPLAIN (ANALYZE) showed:

Seq Scan on sightings (cost=0.00..1200.00 rows=5000 width=64)

The table had a B‑tree index on location_id. After lowering random_page_cost from the default 4.0 to 1.5 (reflecting their SSD storage), the same query switched to an Index Scan with a total cost of 210.00. Wall‑clock time dropped from 450 ms to 78 ms—a 5.8× improvement.

6.2 MySQL: Histogram‑Driven Selectivity

A conservation portal stored observation_date in a DATE column. After a massive data import for the year 2024, the distribution became heavily skewed toward spring months. By default, MySQL’s histogram had only 10 buckets, causing the optimizer to underestimate the selectivity of WHERE observation_date BETWEEN '2024-03-01' AND '2024-05-31'. The plan used a full table scan (cost ≈ 15 000). After running ANALYZE TABLE observations UPDATE HISTOGRAM ON observation_date WITH 256 BUCKETS, the optimizer recognized that the range covered 70 % of rows, switched to an index range scan, and cut the cost to 4 200—a 3.6× speedup.

6.3 Oracle: Cost‑Based Parallelism

A national pollinator database runs a nightly report aggregating sightings across all states. The query touches 5 tables totaling 150 GB. By default, Oracle ran it serially with a cost of 1 200 000. After enabling parallel execution (ALTER SESSION ENABLE PARALLEL DML;) and setting parallel_max_servers=64, the optimizer generated a parallel plan with a parallelism factor of 12, reducing the estimated cost to 210 000. Actual runtime dropped from 45 minutes to 7 minutes, freeing up the server for other workloads.

6.4 SQL Server: Adaptive Join

A data science team built a query that joined a large fact table (observations) with a small lookup (species). The optimizer initially chose a hash join based on stale statistics, expecting 10 million rows from the fact table. In reality, a predicate on observation_date filtered it down to 150 000 rows. The Adaptive Join operator detected the mismatch after processing 10 % of rows, switched to a nested‑loop join, and saved 12 seconds of CPU time—a classic case where the optimizer’s self‑correction prevented a costly mis‑estimate.


7. Influencing the Optimizer – Practical Levers

Even the smartest CBO can be nudged in the right direction. Below are the most effective, DBMS‑specific techniques.

7.1 Hints and Plan Directives

DBMSSyntaxTypical Use
PostgreSQL/*+ IndexScan(p pollination_events_event_date_idx) */ (via pg_hint_plan extension)Force an index scan when the planner prefers a seq scan.
MySQLUSE INDEX (idx_event_date)Direct the optimizer to a specific index.
Oracle/*+ INDEX(p pollination_events_event_date_idx) */Override the chosen access path.
SQL ServerOPTION (HASH JOIN) or OPTION (LOOP JOIN)Force join method.

Caution: Hints bypass the optimizer’s cost calculations. Use them sparingly and test thoroughly, because data distribution changes can render a hint detrimental.

7.2 Index Design

  • Covering indexes – Include all columns needed by the query (INCLUDE in SQL Server, USING in PostgreSQL) to avoid a heap fetch.
  • Partial indexes – For a column that is rarely used (WHERE is_endangered = true), a partial index can dramatically reduce index size and improve selectivity.
  • Column order – Place the most selective column first in a multi‑column B‑tree index. In a query filtering on species and region, if species has 5 % selectivity and region 30 %, an index on (species, region) is preferable.

7.3 Statistics Management

  • Manual ANALYZE – Run after bulk loads. Example: ANALYZE VERBOSE pollination_events;
  • Increase default_statistics_target – For columns with high skew, raising from 100 to 500 can improve histogram accuracy.
  • Extended Statistics – PostgreSQL 13+ supports multivariate statistics (CREATE STATISTICS s1 (dependencies) ON species, region FROM bees;) which help the optimizer understand column correlation.

7.4 Session‑Level Cost Parameters

Adjusting cost constants can reflect hardware realities:

SET enable_seqscan = off;          -- PostgreSQL: discourage seq scans
SET random_page_cost = 1.2;        -- PostgreSQL: SSDs
SET cpu_tuple_cost = 0.005;        -- PostgreSQL: fast CPUs
SET work_mem = '64MB';             -- PostgreSQL: give more memory to sorts/hashes

In MySQL, the equivalent is SET optimizer_switch='index_merge=off'. In Oracle, ALTER SESSION SET optimizer_index_cost_adj=90; can tilt the optimizer toward index usage.

7.5 Query Refactoring

  • Predicate push‑down – Move filters as close to the data source as possible.
  • Common Table Expressions (CTEs) – In PostgreSQL 12+, CTEs are inlined by default, allowing the optimizer to treat them as sub‑queries; older versions materialized them, sometimes harming performance.
  • Avoid OR on different columns – Rewrite as UNION ALL with separate predicates to give the optimizer more accurate selectivity estimates.

8. Common Pitfalls and How to Diagnose Them

8.1 Stale or Missing Statistics

Symptom: EXPLAIN shows a plan with a sequential scan despite a selective predicate. Diagnosis: Run SELECT relname, reltuples FROM pg_class WHERE relname='mytable'; and compare with actual row count. If the numbers diverge, run ANALYZE.

8.2 Mis‑estimated Row Counts

Symptom: Execution time far exceeds the plan’s estimated cost. Tool: PostgreSQL’s pg_statistic and pg_stats view

Frequently asked
What is Understanding Cost‑Based Optimizers about?
Modern data‑driven applications depend on relational database management systems (RDBMS) to turn raw tables into the answers users need—whether that’s a list…
What should you know about 1. The Core Idea: What a Cost‑Based Optimizer Actually Does?
At its simplest, a CBO answers the question “Which plan will likely run fastest?” It does not magically know the future; instead it builds a model of the database’s physical layout, the hardware’s performance characteristics, and the statistical distribution of the data. The optimizer then:
What should you know about concrete Example?
Consider a simple query on a pollination_events table (10 million rows, 12 GB) that joins to a bees table (2 million rows, 3 GB):
What should you know about 2. Anatomy of a Query Plan?
A query plan is a tree of operators. Each node describes a physical operation (e.g., Seq Scan , Index Seek , Hash Join ) and the resources it consumes. Let’s dissect a typical PostgreSQL plan output:
What should you know about visual Analogy?
Think of a bee colony deciding how to collect nectar: each worker (operator) has a known energy cost per flower (I/O per page). The colony evaluates many foraging routes (plans) and picks the one that minimizes total energy expenditure while still gathering enough nectar (rows).
References & sources
  1. Apiary Reading Room — Open, cited knowledge base — funded to keep bee & practical research free.
From the Apiary Reading Room. Opinion & editorial — not financial advice. We don't overclaim.
More from the Reading Room