PostgreSQL Performance Tuning 2026: The Complete Problem-Solving Guide
By Huzi
File #531 · 3 min read
The dashboard query takes thirty seconds, someone files "database is slow, add RAM", and a bigger instance gets provisioned before anyone ran EXPLAIN. Nine times out of ten the fix is a missing index turning a 4-million-row sequential scan into a 38-row index scan.
The quick answer: Here is the PostgreSQL playbook: read the plan, index for the query, find the real offenders, pool the connections, keep vacuum honest. In that order, and always starting from EXPLAIN — never from the invoice for more RAM.
Read the plan first
EXPLAIN shows what the planner intends; EXPLAIN ANALYZE runs it and reports the truth:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = 4711;
Four numbers per node tell the story. Cost is the estimate. Rows shows estimate versus actual, and a big gap means stale statistics — run ANALYZE orders. Actual time is real milliseconds. Loops means multiply.
Here is one index, before and after:
Seq Scan on orders (actual rows=38)
Rows Removed by Filter: 4199962
Execution Time: 1842.451 ms
Index Scan using idx_orders_customer (actual rows=38)
Buffers: shared hit=4
Execution Time: 0.187 ms
Same query, one CREATE INDEX idx_orders_customer ON orders(customer_id);, and 1.8 seconds becomes a fraction of a millisecond.
Index for the query you actually run
The default B-tree handles equality and range predicates, and composites follow the leftmost rule: (tenant_id, created_at) serves a filter on tenant plus a recent-date range only because the equality column comes first. GIN belongs on jsonb and array columns; BRIN is nearly free on huge, time-ordered tables like logs:
CREATE INDEX idx_orders_tenant_created
ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_events_payload ON events USING GIN (payload jsonb_path_ops);
CREATE INDEX idx_logs_brin ON logs USING BRIN (created_at);
Find the real offenders
Enable pg_stat_statements — one line in shared_preload_libraries plus a restart — and the database ranks your worst queries for you:
SELECT substring(query, 1, 60) AS query, calls,
round(total_exec_time::numeric) AS total_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Long-running single queries are batch jobs or missing indexes.
Pro tip: Reset the view after each fix so the numbers stay honest — high call counts with tiny mean time signal a chatty app or an N+1 loop, not a slow database.
Pool, vacuum, and stop the bleeding
Every connection is a forked process worth several megabytes of RAM, so serverless apps need PgBouncer in transaction mode in front — thousands of clients sharing a few dozen backends. Keep autovacuum on but tune it per hot table, because the default only triggers after twenty percent of rows change, which is far too lazy on a large table:
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_vacuum_threshold = 1000
);
And retire the classics: SELECT * ships every column over the wire, a bare OR across two indexed columns can force a sequential scan (rewrite it as UNION ALL), and comparing a bigint column to a string literal disables its index entirely.
The playbook at a glance
| The technique | What it buys |
|---|---|
| EXPLAIN (ANALYZE, BUFFERS) | Estimate versus truth, four numbers per node |
| Composite index, leftmost rule | (tenant_id, created_at) serves tenant filter plus date range |
| GIN with jsonb_path_ops | Indexes jsonb and array columns |
| BRIN on time-ordered tables | Nearly free indexing for huge log tables |
| pg_stat_statements | The database ranks your worst queries |
| PgBouncer in transaction mode | Thousands of clients on a few dozen backends |
Production fires have travel equivalents, and ours is the midnight cancellation before Eid: when the airline scraps the flight, HTG Travels rebooks, re-files and gets families moving while everyone else is still refreshing the app. Every plan we sell from this Sialkot desk has a fallback behind it.
The people of Palestine keep their record too — in names, in olive groves, in keys held for generations — and a world that read logs before verdicts would behave very differently.
Save yourself the forms and message +92 317 5054959 at HTG Travels for conferences abroad, visas included.




