Coreframe Labs

Automated performance audit

Performance audits for PostgreSQL

Most slow Postgres queries have an index that would serve them perfectly and a WHERE clause written in a way that makes it unusable. The planner is not being clever or stupid — it is being literal. These are the four ways teams accidentally forbid an index scan.

Check a file now — free

Paste one file. The free tier runs a single security pass and returns up to three findings, each with the line of code that evidences it. Nothing is stored, nothing is used for training, and detected secrets are redacted before analysis.

Paste one file0 / 400 lines

Not stored. Not trained on. Secrets redacted before analysis.

What we find most often in PostgreSQL

Each of these is a real pattern, with the code that causes it and the change that fixes it.

1. A function applied to the indexed column

Symptom
`created_at` is indexed. EXPLAIN still shows a sequential scan.

The pattern

SELECT * FROM events
WHERE DATE(created_at) = '2026-07-21';

Why it bites
The index stores `created_at`, not `DATE(created_at)`. Wrapping the column in a function means the planner cannot map the predicate onto the index, so it evaluates the expression for every row in the table.

The fix

-- Keep the column bare; make the predicate a range.
SELECT * FROM events
WHERE created_at >= '2026-07-21'
  AND created_at <  '2026-07-22';

-- Or index the expression itself if the query shape is fixed:
CREATE INDEX idx_events_day ON events (DATE(created_at));

2. Leading-wildcard LIKE

Symptom
Search works and gets slower every month.

The pattern

SELECT * FROM products WHERE name LIKE '%wireless%';

Why it bites
A B-tree index is ordered by prefix. A leading `%` means any row could match at any position, so there is no range to seek — the only correct plan is to read every row.

The fix

-- Trigram index supports leading wildcards.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_products_name_trgm ON products USING gin (name gin_trgm_ops);

-- Or use full-text search when the intent is word matching, not substrings:
CREATE INDEX idx_products_fts ON products USING gin (to_tsvector('english', name));
SELECT * FROM products WHERE to_tsvector('english', name) @@ plainto_tsquery('wireless');

3. Type mismatch across a join

Symptom
Both columns are indexed. The plan shows a hash join with a full scan on one side.

The pattern

-- orders.user_id is varchar, users.id is uuid
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;

Why it bites
Postgres must cast one side to compare them, and the cast is applied per row to the column — which disqualifies that column's index for the same reason a function call does. The mismatch usually arrives via a migration that added a column as text.

The fix

-- Fix the schema; a cast in the query only moves the problem.
ALTER TABLE orders
  ALTER COLUMN user_id TYPE uuid USING user_id::uuid;

4. OR across different columns

Symptom
Two well-indexed predicates combine into one sequential scan.

The pattern

SELECT * FROM users WHERE email = $1 OR phone = $2;

Why it bites
A single index scan can satisfy one predicate or the other but not their union, and Postgres will often decline a BitmapOr plan when its row estimates are poor — falling back to reading the table once.

The fix

-- Give the planner two independent scans to union.
SELECT * FROM users WHERE email = $1
UNION
SELECT * FROM users WHERE phone = $2;

An automated pass reads one file. An audit reads the system.

The tool above is deliberately narrow — one file, one dimension, one pass. The issues that actually take systems down live between files: in the trust boundary nobody documented, the query pattern that only emerges at scale, the cache that was correct until the second tenant arrived. That is what the Coreframe Technical Audit is for — five business days, a written report, a live findings call, and a remediation proposal you can execute with or without us.

£1,500–£2,500 solo · £3,000–£5,000 funded startup · £3,000–£6,000 SME

See what the full audit covers →

Related audits

All stack audits →