{"id":"code-dev-sql-create-index-from-explain","audience":"developers","category":"coding","track":"coding","technique":"context","title":"Write one production-safe index from an EXPLAIN plan","free":false,"task":"The account activity page is slow. Your migration rules: production indexes are always CREATE INDEX CONCURRENTLY, and this one must be named idx_events_account_id_created_at. Write exactly one statement in 1 code block (at most 3 lines) that fits the query's filter and sort, as a partial index on rows that aren't deleted. Then exactly one line starting 'Why: ' under 20 words, and nothing else.","materialLabel":"The slow query, its plan and the table (attached to your prompt automatically)","material":"-- PostgreSQL 16, app: Lanternly (activity feed)\n-- events: 38 million rows, ~2% soft-deleted\n\nCREATE TABLE events (\n  id          BIGSERIAL PRIMARY KEY,\n  account_id  BIGINT NOT NULL,\n  kind        TEXT NOT NULL,\n  payload     JSONB NOT NULL,\n  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),\n  deleted_at  TIMESTAMPTZ\n);\n-- existing indexes: events_pkey (id), idx_events_kind (kind)\n\n-- the page runs:\nSELECT id, kind, payload, created_at\nFROM events\nWHERE account_id = $1\n  AND deleted_at IS NULL\nORDER BY created_at DESC\nLIMIT 50;\n\n-- EXPLAIN (ANALYZE) for account_id = 81723:\nLimit  (cost=912033.10..912033.23 rows=50) (actual time=4120.551..4120.560 rows=50)\n  ->  Sort  (cost=912033.10..912071.42 rows=15329)\n        Sort Key: created_at DESC\n        Sort Method: top-N heapsort  Memory: 52kB\n        ->  Seq Scan on events  (cost=0.00..911524.00 rows=15329)\n              Filter: ((deleted_at IS NULL) AND (account_id = 81723))\n              Rows Removed by Filter: 37984671\nPlanning Time: 0.210 ms\nExecution Time: 4120.602 ms\n\n-- Pablo suggested \"just add an index on kind too\" in standup.","rules":[{"type":"codeBlocks","count":1},{"type":"maxCodeLines","max":3},{"type":"maxProseWords","max":30}],"mistakes":[{"title":"A locking index","why":"A plain CREATE INDEX locks writes on a 38-million-row table while it builds.","fix":"Add: \"Production rule: CREATE INDEX CONCURRENTLY.\"","checks":["r0"]},{"title":"Letting the AI name it","why":"The AI invents a name, and your migration checker rejects it.","fix":"Add: \"Name it idx_events_account_id_created_at.\"","checks":["r0"]},{"title":"Wrong columns or order","why":"Indexing kind, or created_at first, doesn't help this filter and sort.","fix":"Add: \"Fit the WHERE and ORDER BY: (account_id, created_at DESC).\"","checks":["r1"]},{"title":"Several options and a lecture","why":"The AI offers three indexes and explains query plans.","fix":"Add: \"Exactly one statement, then one 'Why:' line under 20 words.\"","checks":["r2","codeBlocks","maxCodeLines","maxProseWords"]}],"promptHints":[{"label":"Says it's for production","patterns":["concurrently","production","no lock"]},{"label":"Gives the index name","patterns":["idx_events_account_id_created_at"]},{"label":"Asks for a partial index","patterns":["partial","deleted_at"]},{"label":"Asks for one statement","patterns":["one (statement|index)","single index","exactly one"]},{"label":"Shapes the explanation","patterns":["why:","one line","\\d+\\s*words"]}],"hint":"'Make it faster' invites a lecture and a list of options. Tell the AI your production rules, the name, and the one statement plus one line you want back.","rubricLabels":["Uses CREATE INDEX CONCURRENTLY with the exact name idx_events_account_id_created_at on events","Indexes (account_id, created_at DESC) in that order, equality column first and the sort column second","Is a partial index with WHERE deleted_at IS NULL, and after the code block there is exactly one line starting 'Why: '"]}