HomePostgreSQLArticle
PostgreSQL Mechanics
PostgreSQL: 16 Cores. This Query Uses One
What you will learn
- I have seen this pattern many times in production PostgreSQL
- Eighteen point four seconds, down to four point one.
- shared_buffers was a hundred and twenty-eight megabytes.
- (SLOW DOWN: 45-60s, this is the payoff, let it breathe)
- PostgreSQL's shared_buffers, plus the operating system's page
The production symptom
I have seen this pattern many times in production PostgreSQL
systems. The team was already thinking about more hardware. /
One query took eighteen seconds, on a server with a hundred
Hero Proof
Eighteen point four seconds, down to four point one.
That's more than four times faster, and it cost nothing in
hardware. Let me show you exactly how we got there.
SHOW effective_cache_size;The Diagnosis
shared_buffers was a hundred and twenty-eight megabytes. On a
hundred and twenty-eight gig server, that's about a tenth of
effective_cache_size was four gigs, so the planner thought
ALTER SYSTEM SET effective_cache_size = '96GB'; -- reloadThe Query
Here's the query. Nothing exotic. Top fifty customers by orders in the last ninety days. A join. A date filter. A group by. An order by. Normal reporting SQL. But it takes over eighteen thousand milliseconds, and it reads almost forty-six thousand pages from disk. So before you rewrite the query, stop.
The SQL is not the first suspect. The plan is.
postgres-# SELECT c.id, c.name, count(*) FROM customers cEXPLAIN SELECT * FROM orders WHERE customer_id = 42;Explain Analyze
And the plan tells you everything. This is EXPLAIN ANALYZE,
Look at the sort: "external merge", five hundred and forty-two
megabytes written to disk. That's work_mem being too small.
EXPLAIN (ANALYZE, BUFFERS)The Five Steps
So here's the plan, in order. Five steps. Measure first, with EXPLAIN ANALYZE, always. Then fix the planner's view of memory. Then give PostgreSQL real cache. Then deal with the sorts. And finally, parallel workers. One change at a time. Measure after each one. Never touch two settings at once, or you'll never know which one actually helped.
effective cache size
It does not allocate memory. It's just the planner's estimate
Four gigs to ninety-six. About seventy-five percent of RAM.
Now the planner has a better chance to pick the right scan.
shared buffers
Setting two: shared_buffers. This is PostgreSQL's own cache,
the pages it keeps in memory so it doesn't go to disk.
The default was a hundred and twenty-eight megs. We move it to
SELECT name, setting, unit FROM pg_settings WHERE name INwork mem
Setting three: work_mem. This is the one that bites if you get
with parallel workers and a couple of sorts can use it several
So do not set it to a gigabyte globally. Fifty sessions, two
parallel workers
Setting four: parallel workers. You're raising the ceiling,
you're not forcing anything. The planner still decides whether
On a sixteen-core box, a default of two per gather is low. Four
Before & After
(SLOW DOWN: 45-60s, this is the payoff, let it breathe)
Eighteen thousand two hundred thirty-four milliseconds.
Forty-five thousand eight hundred ninety-two disk reads.
Memory Map
PostgreSQL's shared_buffers, plus the operating system's page
work_mem is separate. It's per-operation scratch space, for
And disk is the fallback. It's the slowest thing in this
TUNING LADDER, recap
Measure first. Tell the planner the truth about your cache.
Give PostgreSQL enough memory to cache with. Fix sort spills
carefully, never globally. Raise the parallel ceiling.
Protect The Gain
(fast, ~20s) You fixed the server. Now protect the gain. Watch three things: your cache hit ratio, your heaviest queries in pg_stat_statements, and EXPLAIN with buffers. Tuning is not a one-time event. Measure again after the workload changes.
Next Steps
(fast, ~20s) The loop, for any slow query: Baseline. One setting. Reload or restart. Re-test. And if it got worse, roll it back. Your servers have capacity. Your defaults don't.