Everything in this part has been a technique. This lesson is the order
to apply them in, because most unsuccessful performance work is
competent techniques applied to the wrong thing.
Step 1: define what “slow” means
Before touching the database, get a specific claim. “The application is
slow” cannot be investigated; the following can.
Which operation? A page, an endpoint, a job.
How slow, against what? p95 was 200 ms, is now 4 s.
Since when? A deploy, a data volume threshold, gradually.
All the time, or sometimes? Intermittent points at plan
instability, lock waits or cache effects; consistent points at
volume or a plan change.
Is the database implicated at all? Application-side time is often
the answer, and hours are lost assuming otherwise.
Refusing to proceed without these is not pedantry. It is the difference
between measuring and guessing.
Step 2: find where the time goes
pg_stat_statements is the single most valuable extension for this.
Install it before you need it.
-- postgresql.conf: shared_preload_libraries = 'pg_stat_statements'CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT round(total_exec_time)::bigint AS total_ms, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(stddev_exec_time::numeric, 2) AS stddev_ms, shared_blks_hit + shared_blks_read AS total_blocks, round(100.0 * total_exec_time / sum(total_exec_time) OVER (), 1) AS pct_of_total, left(query, 80) AS query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
Step 3: check what sessions are waiting on
Before analysing a plan, establish that the problem is the plan.
SELECT wait_event_type, wait_event, state, count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' GROUP BY 1, 2, 3 ORDER BY count(*) DESC;
Dominant wait_event_type
Points at
Part
Lock
Blocking, DDL, contention
IX
IO
Reads that are not cached
XI
LWLock
Internal contention, often buffer mapping
XI
Client
The application is not reading results
IV
null while active
Genuinely computing
This part
If sessions are waiting on Lock, no amount of query tuning helps.
This step takes ten seconds and routinely redirects an entire
investigation.
Step 4: examine one plan
Take the top query from step 2 and get its plan — from auto_explain
if it is running in production, otherwise EXPLAIN (ANALYZE, BUFFERS)
in the safe form from lesson X-03.
Then apply the procedure from lesson X-02:
Read Buffers at the top. That is the total work.
Find the node with the largest Buffers.
Check its loops.
Compare its estimate to its actual — bottom up, first divergence.
Look at timing last.
Step 5: one hypothesis at a time
Write it down before testing it. A hypothesis has a predicted
observation:
“The orders sequential scan is the cost. An index on
(status, created_at) should reduce buffers from 16,667 to under
500.”
Then test exactly that. Changing three things at once and observing an
improvement teaches you nothing about which of them mattered, and leaves
two changes in production that may be costing you.
Step 6: verify with a comparison
-- beforeEXPLAIN (ANALYZE, BUFFERS) SELECT …;-- make exactly one change-- afterEXPLAIN (ANALYZE, BUFFERS) SELECT …;
Compare buffers, not seconds. Timing varies with cache state, with
concurrent load, and with what the storage happens to be doing. Buffer
counts are deterministic for a given plan and data set, and a change
that reduces buffers has reduced work on any hardware.
Then confirm it in production against the same measurement that
identified the problem:
SELECT pg_stat_statements_reset();-- wait for a representative period-- re-run the step 2 query and compare total_exec_time for that statement
An improvement that shows in a hand-run EXPLAIN and not in
pg_stat_statements has not helped the workload, whatever it did to the
test case.
What to take from this
Get a specific claim before investigating. “Slow” is not one.
Rank by total_exec_time for capacity, by mean_exec_time for
experience, by stddev for instability.
Check wait_event_type before analysing a plan. Ten seconds, and it
redirects investigations.
One hypothesis, one change, one measurement.
Compare buffers, not seconds. Then confirm in
pg_stat_statements.
Install pg_stat_statements and auto_explain now; they need a
restart.
Cross-course references
Linux for Production Sysadmins — Part XXXVIII (Linux performance
fundamentals) and Part LXXIX (Troubleshooting methodology) cover the
same discipline one layer down: measure, form a hypothesis, change one
thing, measure again.
Observability for Production Sysadmins — Part XCVIII
(Troubleshooting methodology) and Part CII (Slow queries) cover
starting from the user-visible latency rather than from the database’s
own opinion of itself.
Git, CI/CD & GitOps — Part CXIV (Deployment markers) covers
putting deploys on the same timeline as the latency, which answers
“what changed” faster than any query does.
Quiz
Knowledge check · 6 questions
Q1. pg_stat_statements shows query A at 4000 ms mean over 3 calls and query B at 40 ms mean over 200,000 calls. Which is consuming the server's capacity?
Q2. An engineer spends two hours tuning a query's plan. It turns out sessions were overwhelmingly waiting on wait_event_type 'Lock'. Which step was skipped?
Q3. After adding an index, a hand-run EXPLAIN ANALYZE is much faster but pg_stat_statements shows no improvement in that statement's total_exec_time. What is the most likely explanation?
Q4. Which are genuine limitations of pg_stat_statements that affect how its output should be read? Select all that apply.
Q5. A before-and-after comparison should be made on buffer counts rather than on elapsed time, because buffers are deterministic for a given plan and data set.
Q6. List the six steps of the methodology in order and say what each one prevents.
Passing score: 75%. Answers are checked in this browser.