--- name: postgres-index-selection description: Which index type to reach for — btree/gin/gist/brin/hash — and when partial or expression indexes beat a plain one. when_to_use: You're adding an index or diagnosing a slow query in Postgres. tags: [backend, postgres, performance] --- # Postgres index selection (17) Default to **btree** unless the data shape says otherwise. ## btree - Equality + range on scalar columns. - Multi-column: leading column must be in the WHERE clause for the index to help. `(a, b, c)` helps `WHERE a=? AND b=?`, not `WHERE b=?`. - Descending: rare — btree can scan in either direction, but `ORDER BY x DESC` with a mixed-direction multi-col query benefits from `(a ASC, b DESC)`. ## Partial indexes — huge wins Any time a WHERE clause repeats across queries, a partial index is a smaller, faster index: ```sql CREATE INDEX missions_running_idx ON missions (updated_at DESC) WHERE status IN ('running', 'pending'); ``` The clawmates codebase leans on this heavily — see `topology_runs_status_idx WHERE status = 'queued'`. ## Expression indexes For computed lookups: ```sql CREATE UNIQUE INDEX users_lower_email_idx ON users (lower(email)); -- Query MUST use the exact expression: SELECT * FROM users WHERE lower(email) = lower($1); ``` ## GIN - **JSONB**: `CREATE INDEX foo_meta_gin ON foo USING gin (metadata jsonb_path_ops);` — `jsonb_path_ops` is smaller + faster than default for `@>` queries. Use default when you also need `?` / `?|` / `?&`. - **Arrays**: `CREATE INDEX skills_tags_gin ON skills USING gin (tags);` — `WHERE tags && ARRAY['rust']` becomes index-served. - **Full-text search**: `to_tsvector('english', body)` with a GIN index. ## GiST - Geo (PostGIS): `USING gist` on `geometry` / `geography` columns. - Range types: `USING gist` on `tstzrange` for "does this range overlap" queries. ## BRIN - Very large tables where the indexed column correlates with physical row order (append-only timestamps, sequential ids). 1/1000th the size of btree, works only for range scans. ## Hash (rare) - Equality-only, very high-cardinality, no range. Postgres 10+ hash indexes are WAL-logged + replicated so they're finally safe — but btree wins in most benchmarks. Reach for it only when profiling justifies. ## Diagnosis flow ```sql EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT ...; ``` Read from bottom up. Watch for: - **Seq Scan on a hot table** with a WHERE that should be indexed → missing index. - **Index Scan** with `Rows Removed by Filter` > returned rows → wrong index; add a compound or partial. - **Bitmap Heap Scan** with `Recheck` on lossy → index is being used but not tightly; check if partial would help. - **Sort** with `Method: external merge Disk` → sort spills to disk; add index that provides the order. ## Anti-patterns - **Indexing every column**. Each index has a write cost (INSERT/UPDATE amplification). Prefer 1–2 well-chosen composites over 6 single-column indexes. - **Indexing a boolean without WHERE**. `CREATE INDEX foo_active_idx ON foo(active)` is worthless; `... WHERE active = true` is what you want. - **Trusting `pg_stat_user_indexes.idx_scan = 0`** without waiting a full billing cycle. Some indexes exist for the monthly report.