Postgres Keyset (Cursor) Pagination Optimization Prompt
Convert a slow OFFSET/LIMIT pagination query into stable keyset (seek) pagination that stays fast on deep pages — with a matching index and a correct tuple-comparison predicate, not a broken one-column WHERE.
- Target user
- Backend engineers whose list endpoints slow down on high page numbers
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT, Cursor
The prompt
You are a senior PostgreSQL engineer who has fixed dozens of paginated endpoints that fall over on deep pages. You reason from the index and the ORDER BY, and you get the tuple-comparison semantics exactly right, because an off-by-one here silently skips or repeats rows. I will paste: - The current query (with ORDER BY, OFFSET, LIMIT): [CURRENT QUERY] - The plan on a DEEP page from `EXPLAIN (ANALYZE, BUFFERS)`: [EXPLAIN OUTPUT] - The table's existing indexes: [INDEXES] - The sort key(s) and whether the last one is unique (e.g. created_at + id): [SORT KEYS] - Any filters applied alongside pagination (tenant_id, status): [FILTERS] Work through this in order: 1. **Explain the OFFSET cost** from the plan: OFFSET N still reads and discards N rows, so page cost grows linearly with depth. Point at the rows read vs rows returned. 2. **Design the keyset predicate correctly.** For a compound sort like `ORDER BY created_at DESC, id DESC`, the seek predicate must be a row comparison: `WHERE (created_at, id) < ($last_created_at, $last_id)`. Explain why splitting it into `created_at < x OR (created_at = x AND id < y)` is required only when directions differ, and why a naive single-column `created_at < x` skips or duplicates rows at boundaries. Always require a unique tiebreaker (the primary key) as the final sort key. 3. **Match the index to the ORDER BY** exactly, including column order, direction, and any leading equality filter (tenant_id first). Give the `CREATE INDEX CONCURRENTLY` DDL and confirm the composite key supports the seek without a sort node. 4. **Handle the API contract**: what cursor to return to the client (the last row's sort key values, encoded), how the first page differs (no predicate), and how to page backward. 5. **Give a verification step**: the exact seek query with example cursor values, the EXPLAIN that should now show an Index Scan with no OFFSET and constant rows read regardless of page depth, and a correctness check that no row is skipped or repeated across a boundary. Output: (a) the rewritten seek query; (b) the index DDL; (c) the cursor format; (d) the before/after EXPLAIN expectation; (e) one correctness pitfall to test. Guardrails: keyset pagination cannot jump to an arbitrary page number — confirm the UI only needs next/prev before recommending it. Require a unique final sort key or rows on a tie boundary will be lost. Build indexes CONCURRENTLY and validate on a replica.
Run this prompt with AI
Test it, get an AI-improved version, or compare models — live in the Prompt Workspace. No copy-paste.
Why this prompt works
Deep-page pagination is a textbook performance cliff: OFFSET 100000 reads and throws away a hundred thousand rows before returning the page you want, so latency climbs with page number. The fix — keyset (seek) pagination — is well known, but the two ways it breaks are subtle: the tuple-comparison predicate is easy to get wrong, and forgetting a unique tiebreaker corrupts results at page boundaries. This prompt centers both, forcing the correct row-value comparison and a unique final sort key.
It also refuses to hand-wave the index. The seek only stays O(1) per page if a composite index matches the ORDER BY column-for-column, direction-for-direction, with any equality filter leading. By pairing the rewrite with exact DDL, a cursor-format contract, and a boundary correctness check, the prompt delivers a change that is both fast and provably lossless — and the guardrail about arbitrary page jumps stops teams from adopting keyset where the UI genuinely needs numbered pages.
Related prompts
-
Postgres Index Advisor from pg_stat_statements Prompt
Mine pg_stat_statements for your most expensive queries and get a prioritized list of missing indexes to add and redundant indexes to drop — with the write cost of each spelled out.
-
Postgres Slow Query EXPLAIN Triage Prompt
Turn a confusing EXPLAIN (ANALYZE, BUFFERS) plan into a ranked, plain-English diagnosis with concrete index/rewrite fixes and a verification step — so you fix the real bottleneck, not a guess.
-
Postgres Parallel Query Worker Tuning Prompt
Work out why a big analytical query isn't going parallel — or is spawning too many workers and starving everything else — and set the parallel GUCs from evidence in the plan instead of copy-pasted blog values.
-
Postgres Prepared Statement Generic vs Custom Plan Tuning Prompt
Diagnose why a parameterized/prepared query flipped to a bad generic plan after the sixth execution — and decide between plan_cache_mode, query rewrites, or letting Postgres re-plan — so latency stops spiking under skewed parameters.
More Postgres prompts & error guides
Browse every Postgres prompt and troubleshooting guide in one place.
Reading prompts? Get all 500 in one free PDF
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.