What we find

Three databases, before and after

Every story below is a composite of real analyses with names, numbers and schema details changed. The pattern is always the same: the ranking points at one statement, the plan and the table statistics explain it, and the fix is small.

Online marketplace · PostgreSQL 15 on Amazon RDS, 3.1 GB orders table, 40 req/s at peak

One missing composite index was 61% of the primary's CPU

The problem

Checkout latency doubled every Friday evening. The team had added read replicas twice; the primary still sat at 90% CPU.

What the report showed

The top statement by total time was an order lookup filtered by customer and status. It ran 48,000 times an hour and every run read the whole orders table: a sequential scan removing 4.2 million rows by filter, then a sort. An index on created_at existed but was never used, because the filter columns were not in it.

The fix

CREATE INDEX CONCURRENTLY orders_customer_status_created_idx ON orders (customer_id, status, created_at DESC); the unused legacy index was dropped a week later.

Before → after

BeforeAfter
Mean time of the statement412 ms1.8 ms
Primary CPU at peak90%34%
Checkout p952.9 s640 ms
“We had been staring at Grafana for a month. The report ranked the problem first and showed the plan next to the table statistics; the fix took ten minutes.”

B2B analytics SaaS · Self-managed PostgreSQL 16, 52 GB events table, Kubernetes

A count over eight million rows was hiding behind a small API call

The problem

The dashboard's usage widget timed out for the largest accounts. Engineers suspected the ORM; they were about to add a caching layer.

What the report showed

pg_stat_statements showed a count(*) executed 120,000 times a day with a mean of 220 ms and a plan that used a bitmap scan on account_id alone, then filtered six million rows by date, with 48,000 lossy heap blocks because work_mem was 4 MB.

The fix

A composite index on (account_id, occurred_at) turned the filter into an index condition; the widget now reads an hourly rollup for accounts above a threshold.

Before → after

BeforeAfter
Mean time of the statement220 ms3.4 ms
Statement's share of total DB time31%under 1%
Widget timeouts per day1,4000
“The plan said lossy heap blocks; none of us had heard the term. The report's AI prompt explained it and proposed the index.”

Payments company · PostgreSQL 14 on Supabase, 2.3 GB carts table, autovacuum defaults

Bloat, not traffic, was slowing down a one-row UPDATE

The problem

A trivial UPDATE by session id had a mean of 95 ms and ran a quarter of a million times an hour. Nobody had looked at it because each call was 'fast enough'.

What the report showed

There was no index on session_id, so every update scanned 2.3 million live rows plus a million dead ones: the table was 31% dead tuples and autovacuum had not caught up in two days. The statement was third by total time despite the small mean.

The fix

An index on (session_id), a per-table autovacuum_vacuum_scale_factor of 0.01, and one manual VACUUM (ANALYZE) during a quiet hour.

Before → after

BeforeAfter
Mean time of the statement95 ms0.4 ms
Dead tuples31%2%
Total DB time per hour6.6 h1.9 h
“Total time, not average time, is what the ranking sorts by. That one choice surfaced a problem our APM tooling had filtered out.”

See it on your own database

Free for three analyses a month. Read-only; the password is used once and never stored.