effective_cache_size and Index Cost Estimates #
A large reporting query uses a sequential scan on a 90 GB table on the new server and a fast index scan on the old one. The hardware is better, the data is identical, random_page_cost matches — and effective_cache_size on the new server is still at its default of 4 GB, while the old one was set to 96 GB years ago and forgotten.
effective_cache_size is the setting most often misunderstood in the cost estimation model. It allocates nothing, caches nothing and limits nothing. It is purely an input to the planner’s arithmetic, and it only affects one family of decisions: how expensive an index scan is expected to be.
The Cost-Model Condition #
When the planner costs an index scan, it has to estimate how many of the pages it will touch are already in memory — either PostgreSQL’s shared_buffers or the operating system’s page cache. A page in memory costs little to read; a page that must come from disk costs random_page_cost. The planner cannot see the actual cache contents, so it uses effective_cache_size as its assumption about how much memory is available to hold the table and index pages a single query needs.
The effect is concentrated in two places:
- Index scans that visit many heap pages. The larger the fraction of the relation’s pages the planner believes can be cached, the more it discounts repeated random page fetches, and the cheaper a large index scan looks compared with a sequential scan.
- Index size relative to the setting. Indexes, and especially the upper levels of large B-trees, are assumed to be cached when they are small relative to
effective_cache_size. An index that is several times larger than the setting is priced as though much of it must be read from disk.
What it does not affect: sequential scan cost, hash and sort memory decisions (work_mem governs those), or anything at execution time. A value that is too large does not cause memory pressure; a value that is too small does not save memory. It only moves the crossover point between access paths described in sequential vs index scans.
A reasonable value is the memory the server can realistically devote to caching data: shared_buffers plus the OS page cache that remains after connections, work memory and other processes. On a dedicated database server that is commonly 50–75% of RAM.
Annotated EXPLAIN Evidence #
The symptom is a sequential scan with a cost close to an index scan alternative that is much faster in practice. Compare the two plans under both settings:
SET effective_cache_size = '4GB'; -- default
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events WHERE account_id BETWEEN 2000 AND 2400;
Seq Scan on events (cost=0.00..11820400.00 rows=1840000 width=180)
(actual time=0.05..61204.7 rows=1812300 loops=1)
Filter: ((account_id >= 2000) AND (account_id <= 2400))
Rows Removed by Filter: 408187700
Buffers: shared hit=402110 read=11418290
Execution Time: 61840.2 ms
-- total cost 11.8M: the index scan was estimated slightly higher, so it lost
SET effective_cache_size = '96GB'; -- realistic for a 128 GB server
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events WHERE account_id BETWEEN 2000 AND 2400;
Index Scan using events_account_id_idx on events
(cost=0.57..6104220.10 rows=1840000 width=180)
(actual time=0.04..3810.6 rows=1812300 loops=1)
Index Cond: ((account_id >= 2000) AND (account_id <= 2400))
Buffers: shared hit=1204880 read=140220
Execution Time: 3902.4 ms
-- same estimate of rows; cost almost halved by the cache assumption
What to read:
rows=1840000is identical in both plans. Nothing about the row estimate changed, which rules out statistics as the cause.- The index scan’s total cost fell from just above the sequential scan’s cost to about half of it. Only the page-fetch pricing moved.
Buffers: shared hitdominates in the fast run. The pages really were cached, which is what a realisticeffective_cache_sizetells the planner to expect.
Step-by-Step Resolution #
-
Check the current value and its source.
SELECT name, setting, unit, source, sourcefile FROM pg_settings WHERE name IN ('effective_cache_size', 'shared_buffers', 'random_page_cost');effective_cache_sizeis reported in 8 kB pages; multiplysettingby 8 to get kilobytes. -
Estimate realistic cacheable memory. Take total RAM, subtract what connections and work memory need at peak — see pool size and the total work_mem budget — and other processes on the host. What remains, plus
shared_buffers, is a defensible value. -
Test the effect in a transaction on the queries that misbehave:
BEGIN; SET LOCAL effective_cache_size = '96GB'; EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM events WHERE account_id BETWEEN 2000 AND 2400; ROLLBACK; -
Apply it server-wide once the value is justified; it can be changed with a reload, no restart:
ALTER SYSTEM SET effective_cache_size = '96GB'; SELECT pg_reload_conf(); -
Re-plan cached statements. Prepared statements holding generic plans do not see the new value until they are replanned, as explained in plan invalidation after DDL and ANALYZE.
-
Watch for regressions in the other direction. A value far above real memory makes index scans on cold data look cheaper than they are; if
Buffers: shared readdominates the new index plans, the setting is optimistic for that workload.
Before and After #
-- BEFORE: effective_cache_size = 4GB (default)
Seq Scan on events (cost=0.00..11820400.00) Buffers: shared read=11418290 Execution Time: 61840.2 ms
-- AFTER: effective_cache_size = 96GB
Index Scan using events_account_id_idx (cost=0.57..6104220.10) Buffers: shared read=140220 Execution Time: 3902.4 ms
Why the default is so often wrong #
The 4 GB default was chosen to be safe on small machines, where overestimating the cache would push the planner toward index scans that really do hit disk. On a modern database server it is almost always too low, and because it neither fails nor warns, it can stay at the default for the life of a cluster. Migrations are the typical moment it bites: a new host provisioned from a template configuration carries the default, while the old host had been tuned. Plans that relied on the old value for large index range scans quietly switch to sequential scans, and the regression appears as “the new server is slower” rather than as a configuration difference. Capturing plans with the SETTINGS option makes this visible immediately, because a tuned value shows up in the Settings: line and the default does not.
The setting interacts with random_page_cost. Both push in the same direction for index scans, and tuning one to compensate for the other produces a configuration that is right for today’s cache hit ratio and wrong after the next change in data volume. Set random_page_cost to reflect storage latency — see tuning random_page_cost for SSD storage — and effective_cache_size to reflect memory, and let each describe its own resource.
Common Pitfalls #
Treating it as a memory allocation. Raising it does not grow any cache and lowering it does not free memory. Diagnostic signal: a change proposed “to reduce memory usage”. Fix: tune shared_buffers and work_mem for memory; use this only for planning.
Setting it higher than physical memory. The planner then assumes cache hits that cannot happen. Diagnostic signal: index plans with Buffers: shared read far above hit on repeated runs. Fix: cap it at realistic cacheable memory.
Diagnosing it from a warm test run. A repeated test shows cached pages regardless of the setting. Diagnostic signal: an index plan that is fast in testing and slow after restarts or failovers. Fix: test on a cold cache as well, and reason from memory size rather than one run.
Per-session overrides left in place. Like any planner setting it can be set by role or session. Diagnostic signal: different plans for the same query from different application roles. Fix: audit pg_db_role_setting as described in session parameter drift.
Frequently Asked Questions #
Does effective_cache_size allocate memory? #
No. It is only an assumption the planner uses when costing index scans. Actual caching is done by shared_buffers and the operating system’s page cache, whose sizes are unaffected by this setting.
What should effective_cache_size be set to? #
The memory realistically available for caching table and index pages: shared_buffers plus the operating system page cache left after connections and other processes. On a dedicated server that is typically 50–75% of RAM.
Does changing effective_cache_size require a restart? #
No. It can be changed with ALTER SYSTEM and a configuration reload, or per session with SET. Cached generic plans for prepared statements keep their old costs until they are replanned.
Related #
- Cost Estimation Models — parent guide: how the planner prices every node
- Tuning random_page_cost for SSD Storage — sibling: the storage-side input to index scan cost
- Why Index Scans Sometimes Underperform — when a cheaper-looking index scan is the wrong choice