The planner assumes your columns are independent. When they're actually correlated, its row estimates collapse, it picks a nested loop over a hash join, and a fast query becomes a slow one. CREATE STATISTICS fixes the math.
PostgreSQL Topic Archive
Slow Queries PostgreSQL Articles
Query plans, EXPLAIN ANALYZE, planner regressions, pagination, joins, and statistics.
We partitioned a table by month and the dashboard was still slow. Partitioning only helps when the planner can prune — and ours couldn't, because the queries didn't filter on the partition key.
The PostgreSQL planner is only as good as its estimates. Extended statistics help when correlated columns, multi-column filters, and skewed values make ordinary statistics lie.
Prepared statements reduce parse and planning overhead, but generic plans can become a production regression when tenant skew, partial indexes, and parameter-sensitive predicates arrive.
pg_stat_statements is where PostgreSQL performance work becomes concrete. The trick is ranking by impact, variance, I/O, and calls instead of staring at one dramatic slow query.
EXPLAIN ANALYZE is not just a plan tree. With BUFFERS and real row counts, it becomes a production trace for estimates, I/O, loops, spills, and wasted work.
EXPLAIN ANALYZE is readable once you know the order: find the slow node, compare estimates to actuals, check loops, then confirm buffers and waits.
A nested loop join is fast or slow depending on the row counts. Here is when it is the right plan, when it is not, and how to read the EXPLAIN output to tell.
Hash joins are the workhorse for large equi-joins. They are also the join type that quietly spills to disk when work_mem is too small, turning a fast query into a slow one.
Planner problems usually start with one wrong estimate. Learn how to spot the first lie in the plan, then fix the statistics, predicate, or index that caused it.
CTEs read beautifully and sometimes cost beautifully too. The behavior changed in Postgres 12, and most teams I work with have not updated their mental model.
Window functions are the right tool for ranked-per-group queries. They are also surprisingly fragile in performance. Here is how to write them so they actually scale.
DISTINCT ON is a Postgres-specific shortcut for "give me one row per group, sorted some way." It is fast, it is pleasant, and the version in your head is probably wrong.
OFFSET pagination gets slower the deeper you go. Keyset pagination is constant time. Here is how to switch over and the cases where you cannot.
A practical slow-query incident workflow: find the fingerprint, prove the bottleneck, avoid the tempting wrong fix, and ship the smallest change that actually holds.
Cloud SQL maintenance windows are mostly fine and occasionally not. Here is what happens during them, what gets restarted, and how to make sure your application survives.
N+1 is the most common ORM-induced performance bug. The query count tells the story; the application code makes it impossible to spot at review time.
Prepared statements skip the planning step on repeated execution. Sometimes that is a 5x speedup. Sometimes it is a 50x slowdown. Knowing the difference matters.
Bad plans usually start with bad row estimates. Fix the first wrong estimate and the rest of the plan often stops looking mysterious.
Plan regressions are painful because the SQL did not change. The work is proving the plan changed, finding the estimate or stats shift, and restoring the safe path.
auto_explain is for the slow plan you cannot reproduce later. It captures the execution plan when the bad thing actually happens.