EXPLAIN Review Skill
Plan critique checklist for app-team PRs on PostgreSQL 18.4 - structured SKILL.md workflow agents and humans use to accept or reject query changes with EXPLAIN evidence.
Search across all documentation pages
Plan critique checklist for app-team PRs on PostgreSQL 18.4 - structured SKILL.md workflow agents and humans use to accept or reject query changes with EXPLAIN evidence.
## EXPLAIN Review Skill - Quick checklist
- [ ] EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) attached
- [ ] Runs on staging with production-scale stats (ANALYZE recent)
- [ ] No unexpected Seq Scan on large tables
- [ ] Row estimates within 10x of actual (Planning vs Actual rows)
- [ ] No Sort on large rowsets without LIMIT index support
- [ ] Buffer read count justified for query frequencyEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total
FROM orders o
WHERE o.tenant_id = $1 AND o.status = 'open'
ORDER BY o.created_at DESC
LIMIT 50;When to invoke this skill:
Input PR: Django queryset adds OR across status values.
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE tenant_id = $1 AND (status = 'open' OR status = 'pending');FAIL signals in output:
Seq Scan on orders (cost=0..250000 rows=1200000)
Buffers: shared read=180000
Planning Rows: 50000 Actual Rows: 890000Skill output:
## Verdict: FAIL
### Issues
1. Seq Scan - filter on tenant_id + status should use index
2. Row estimate off 18x - run ANALYZE orders
3. OR prevents simple btree composite use - consider IN (...) rewrite
### Suggested fix
```sql
SELECT * FROM orders
WHERE tenant_id = $1 AND status IN ('open', 'pending');CREATE INDEX CONCURRENTLY idx_orders_tenant_status
ON orders (tenant_id, status);
## Deep Dive
### Pass/Fail Rubric
| Signal | Pass | Fail |
| --- | --- | --- |
| Access path | Index Scan / Index Only Scan on filter | Seq Scan > 10k rows |
| Join | Hash/Merge on large sets with equi-join keys | Nested Loop with inner seq scan |
| Sort | Top-N heap scan with index | External sort on millions |
| Buffers | Stable vs call frequency | Read entire table per request |
### Required PR Attachments
```sql
-- Table scale context
SELECT relname, n_live_tup, pg_size_pretty(pg_relation_size(oid))
FROM pg_class c JOIN pg_stat_user_tables s ON s.relid = c.oid
WHERE relname = 'orders';
SELECT relname, last_analyze, last_autoanalyze
FROM pg_stat_user_tables WHERE relname = 'orders';
ANALYZE orders; -- staging before EXPLAIN if stalepg_indexes diff before DDL.| Alternative | Use When | Don't Use When |
|---|---|---|
auto_explain | Sample plans in logs | PR review before merge |
| pganalyze plan UI | Continuous monitoring | Air-gapped review |
| Pairing session | Junior author's first PR | Routine trivial CRUD |
Yes when moderate selectivity and multiple predicates combine. Fail when bitmap covers most of table repeatedly at high QPS.
10x between planned and actual triggers investigate stats or extended statistics. 2x may be acceptable on volatile tables.
It recommends; Migration Safety Skill scores lock risk before merge.
Stack versions: This page was written for PostgreSQL 18.4 and psql client 18.4.
Reviewed by Chris St. John·Last updated Jul 16, 2026