Tuning random_page_cost for SSD Storage #
random_page_cost is the planner’s belief about how much more expensive it is to fetch a page at a random offset than to fetch the next page in a sequential read. The default, 4.0, encodes the physics of a rotating disk: a seek, a rotational delay, then a read. On flash storage there is no seek and no rotation, so a default of 4.0 systematically overprices every index scan and pushes the planner toward sequential scans it does not need.
This page shows how the value enters the cost model, how to choose a defensible number, and how to avoid the failure modes at both extremes. The cost model itself is covered in cost estimation models.
Where the Value Enters the Cost Model #
Every table access path is costed as I/O plus CPU. The I/O half is built from two knobs:
seq_page_cost = 1.0 -- one page fetched sequentially (the reference unit)
random_page_cost = 4.0 -- one page fetched at a random offset
A sequential scan of a 100,000-page table costs about 100000 × seq_page_cost, plus CPU per row. An index scan returning 5,000 rows scattered across the heap costs roughly index pages + 5000 × random_page_cost, discounted by how much of the table the planner believes is cached — that discount is effective_cache_size.
With random_page_cost = 4.0, those 5,000 heap fetches are priced at 20,000 cost units. Drop the setting to 1.1 and the same fetches cost 5,500, which is frequently the difference between the planner choosing the index and ignoring it. Nothing about the query or the data changed; the price list did.
Annotated Evidence #
The symptom is a plan that ignores a perfectly good index:
SHOW random_page_cost; -- 4
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id BETWEEN 40000 AND 40800;
Seq Scan on orders (cost=0.00..214880.00 rows=48210 width=96)
(actual time=0.02..1841.60 rows=48122 loops=1)
Filter: (customer_id >= 40000 AND customer_id <= 40800)
Rows Removed by Filter: 7951878
Buffers: shared read=91240
Execution Time: 1902.4 ms
Force the index path and compare the reality against the estimate:
BEGIN;
SET LOCAL random_page_cost = 1.1;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id BETWEEN 40000 AND 40800;
Index Scan using orders_customer_id_idx on orders (cost=0.44..112402.10 rows=48210 width=96)
(actual time=0.05..142.80 rows=48122 loops=1)
Index Cond: (customer_id >= 40000 AND customer_id <= 40800)
Buffers: shared hit=1204 read=8891
ROLLBACK;
Execution Time: 148.9 ms
Twelve times faster, and the buffer numbers explain why: 10,095 pages touched instead of 91,240. The planner was not wrong about the row count — its estimate of 48,210 against an actual 48,122 is excellent. It was wrong about the price of reaching them.
Step-by-Step Tuning #
-
Collect evidence before changing anything. Find statements where an index exists and is not used, then measure both paths with
SET LOCALinside a transaction. One anecdote is not a workload. -
Fix
effective_cache_sizefirst. It is more often wrong thanrandom_page_costand it changes the same decisions:SHOW effective_cache_size; -- default 4GB, usually far too low ALTER SYSTEM SET effective_cache_size = '24GB'; -- ~50-75% of system RAM SELECT pg_reload_conf(); -
Test candidate values against a set of real queries, not one:
BEGIN; SET LOCAL random_page_cost = 1.5; EXPLAIN (ANALYZE) SELECT … ; -- repeat for each representative statement ROLLBACK; -
Pick a value in the 1.1–2.0 band for SSD or NVMe, higher if a meaningful share of reads still miss the cache and reach network-attached storage.
-
Apply it where it belongs. Globally when all data lives on the same class of storage; per tablespace when it does not:
ALTER TABLESPACE fast_ssd SET (random_page_cost = 1.1); ALTER SYSTEM SET random_page_cost = 1.1; SELECT pg_reload_conf(); -
Re-check a sample of plans afterwards. The point of lowering the value is more index scans; the risk is index scans on queries that touch most of the table anyway. Watch for rising buffer counts on wide-range queries.
Before and After #
-- BEFORE: random_page_cost = 4.0
Seq Scan on orders Buffers: shared read=91240 Execution Time: 1902.4 ms
-- AFTER: random_page_cost = 1.1
Index Scan using orders_customer_id_idx
Buffers: shared hit=1204 read=8891 Execution Time: 148.9 ms
The estimate accuracy was already good in both plans. What changed is which path the cost model preferred, and the buffer count is the honest measure of the improvement.
Common Pitfalls #
Treating the value as a millisecond figure. It is a ratio against seq_page_cost = 1.0. Diagnostic signal: someone proposing a value derived from a disk benchmark in microseconds. Fix: reason in ratios.
Changing it globally after testing one query. A value that rescues one range scan can push a large report onto an index path that re-reads the whole heap. Diagnostic signal: total buffer reads rising after the change. Fix: validate against a representative set.
Leaving effective_cache_size at the default. It undercounts the cache so heavily that index paths look expensive regardless of random_page_cost. Diagnostic signal: SHOW effective_cache_size returning 4 GB on a 64 GB machine. Fix: set it to a realistic fraction of RAM.
Setting it below seq_page_cost. Random access is never genuinely cheaper than sequential. Diagnostic signal: bizarre plans preferring index scans over trivially small tables. Fix: keep it at or above 1.0.
Tuning it to force one plan. The setting is a global statement about storage, not a per-query hint. Lowering it until one stubborn query picks an index changes the cost of every index path in the database. Diagnostic signal: a value chosen by bisection against a single statement, such as 1.03. Fix: pick the value from the storage characteristics, then fix the individual query through statistics or an index.
Ignoring the interaction with parallelism. A cheaper index path can stop a plan clearing the parallel-scan cost threshold, so a query that used four workers quietly becomes single-threaded. Diagnostic signal: a Gather node disappearing from a plan after the change. Fix: compare parallel plans specifically after adjusting page costs, as covered in why parallel workers are not launched.
Assuming cloud block storage behaves like local NVMe. Network-attached volumes have latency profiles closer to a fast spinning disk than to local flash, and their random-read penalty is real. Diagnostic signal: a value of 1.1 that produces index plans slower than the sequential ones they replaced. Fix: measure both paths on the actual volume type before committing, and set the value per tablespace when storage classes differ.
Frequently Asked Questions #
What value should random_page_cost be on SSD?
Most SSD-backed systems settle between 1.0 and 2.0, and 1.1 is a common starting point. Because it is a ratio against seq_page_cost, the right value depends on how much more expensive random access really is on your storage and on how much of the working set is cached.
Should I set it to exactly 1.0? Only if random and sequential access are genuinely indistinguishable, which is closer to true for RAM than for any device. At 1.0 the planner loses its last reason to prefer sequential access and can pick index paths that revisit the same heap pages many times.
Is effective_cache_size more important?
Often, yes, and it is more frequently mis-set. It tells the planner how much of the relation is likely cached, which discounts repeated random access; random_page_cost prices the uncached fetch. Correct effective_cache_size first, then revisit the page cost.
Related #
- Cost Estimation Models — parent guide: how every cost term is assembled
- How PostgreSQL Calculates Node Costs — sibling: the per-node arithmetic
- Execution Plan Fundamentals — section overview: access paths and their costs
- Why Index Scans Sometimes Underperform — when the cheaper-looking index path is genuinely worse