Repository navigation
OpenConceptLab/ocl_online#476 | Covering list indexes so deep pages walk their OFFSET in the index - #938
Merged
Conversation
…alk their OFFSET in the index concepts_repo_list_idx and mappings_repo_list_idx become concepts/mappings_repo_list_ids_idx: the same key and predicate with INCLUDE (id), so a HEAD list's ids subquery (oclapi2#937) can walk a deep page's OFFSET as an index-only scan instead of reading every skipped row. Non-atomic migrations, built with CONCURRENTLY. On a table of more than 3M rows they don't build: build the index by hand first (every API task runs migrate before serving, and a build past the health check's grace leaves an INVALID index). Given a valid index, they drop the old one; given an invalid one, they stop and keep the old one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
Codex review, pass 1 (codex-cli 0.162.1, commit f44d882)Verdict: 2 findings (P1, P2).
findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "full",
"commit": "f44d8828",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "F1.1",
"severity": "P1",
"tag": null,
"file": "core/concepts/migrations/0090_concepts_repo_list_ids_idx.py",
"line": 44,
"claim": "Both migrations drop the old indexes after checking validity alone, bypassing the required confirmation that replacements are used.",
"scenario": "On a QA/staging table below the threshold, startup creates the covering index and immediately drops the old index before gunicorn serves any requests. There is no intervening opportunity to validate the actual HEAD query's plan or cold-cache performance. Deploying this diff without prerequisite oclapi2#937 also removes the old indexes while HEAD pagination still fetches full rows.",
"action": "Defer both old-index drops to a separately gated cleanup after oclapi2#937 and the required staging/production EXPLAIN and timing checks.",
"lines_delta": null,
"confidence": "high"
},
{
"id": "F1.2",
"severity": "P2",
"tag": null,
"file": "core/mappings/migrations/0061_mappings_repo_list_ids_idx.py",
"line": 49,
"claim": "Retrying an interrupted rollback can drop the valid covering index while leaving an invalid old index.",
"scenario": "A rollback's concurrent creation of the old index is interrupted, leaving it INVALID. On retry, CREATE INDEX IF NOT EXISTS skips that existing index, then restore_index drops the valid covering index and reports success. The intended original list index remains unusable. PostgreSQL documents both behaviors in its [CREATE INDEX reference](https://www.postgresql.org/docs/14/sql-createindex.html). Both migrations have this defect.",
"action": "Check the restored old index's indisvalid before dropping the covering index; preserve the covering index and fail with recovery instructions when invalid. Add an interrupted-rollback retry test.",
"lines_delta": null,
"confidence": "high"
}
]
} |
…he old ones later Codex review of #938: - F1.1: the migrations dropped the old list indexes as soon as the new ones were valid, before anyone could confirm the new ones are used, which the ticket requires. They now only add the covering indexes; a later migration drops the old ones once EXPLAIN on staging and production shows them in use. - F1.2: an interrupted rollback could leave an invalid old index and then drop the valid covering one. Rollback now only drops the covering index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
|
Response to Codex pass 1, both fixed in 6ff431e:
Verified on Postgres 14:
|
Member
Author
Codex review, pass 2 (codex-cli 0.162.1, commit 6ff431e)Verdict: clean. F1.1 and F1.2 resolved; no new findings. findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "verify",
"commit": "6ff431e0",
"verdict": "clean",
"prior": [
{
"id": "F1.1",
"status": "resolved"
},
{
"id": "F1.2",
"status": "resolved"
}
],
"findings": []
} |
… trim index tests From the simplification review of #938: the predicate is inlined into the one statement that uses it; the tests drop a mocked small-table build (creating the test database runs the real one) and a validity join (a migrated database can't hold an invalid index). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
Simplification review (fresh Claude subagent, commit 6ff431e)Verdict: 3 cuts proposed, all applied in b3b2b6b.
After the cuts: 5 index tests pass on a freshly created test database, and simplify.json{
"reviewer": "claude subagent",
"perspective": "simplify",
"mode": "full",
"commit": "6ff431e0",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "S1",
"severity": null,
"tag": "dup-test",
"file": "core/integration_tests/tests_list_indexes.py",
"line": 51,
"claim": "test_builds_on_a_small_table repeats ListCoveringIndexTest: creating the test database runs the real build on small tables, and ListCoveringIndexTest checks the result.",
"scenario": null,
"action": "nothing replaces it",
"lines_delta": -6,
"confidence": "medium"
},
{
"id": "S2",
"severity": null,
"tag": "shrink",
"file": "core/integration_tests/tests_list_indexes.py",
"line": 17,
"claim": "The pg_index join and assertTrue(valid) can go: add_index raises on an INVALID index, so a migrated test database can't hold one.",
"scenario": null,
"action": "select indexdef from pg_indexes only; keep the definition and old-index assertions",
"lines_delta": -3,
"confidence": "medium"
},
{
"id": "S3",
"severity": null,
"tag": "shrink",
"file": "core/concepts/migrations/0090_concepts_repo_list_ids_idx.py",
"line": 15,
"claim": "PREDICATE is used once, inside CREATE_NEW_INDEX (same in mappings 0061).",
"scenario": null,
"action": "inline the literal predicate",
"lines_delta": -2,
"confidence": "medium"
}
]
} |
Member
Author
Codex review, check of cuts (codex-cli 0.162.1, commit b3b2b6b)Verdict: clean. The simplification commit broke nothing. findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "cuts",
"commit": "b3b2b6b4",
"verdict": "clean",
"prior": [
{
"id": "F1.1",
"status": "resolved"
},
{
"id": "F1.2",
"status": "resolved"
}
],
"findings": []
} |
snyaggarwal
approved these changes
Oct 14, 2026
snyaggarwal
pushed a commit
that referenced
this pull request
Oct 14, 2026
…by hand before deploying (#941) Every API container runs migrate before it serves, so a long index build can outlast the health check and leave an INVALID index. The note says how to add an index to concepts or mappings safely, with #938 as the example. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relates to OpenConceptLab/ocl_online#476. The speed-up depends on #937.
Summary
Adds covering versions of the HEAD list indexes: the same key and predicate, plus
INCLUDE (id).concepts_repo_list_idxconcepts_repo_list_ids_idxmappings_repo_list_idxmappings_repo_list_ids_idxThe existing indexes stay for now. A later migration drops them once EXPLAIN on staging and production shows the new ones in use, as the ticket requires.
With #937, a deep page of a HEAD list selects its ids first:
SELECT id … ORDER BY updated_at DESC LIMIT n OFFSET m. Withidin the index, Postgres walks that OFFSET as an index-only scan instead of reading every skipped row from the table. On a cold cache, that table walk is most of a deep page's cost (timings on the ticket). Without #937 nothing changes: page queries read whole rows, and the new index serves them as the old one did.The old indexes were created with
RunSQLand aren't inMeta.indexes, so these migrations don't touch model state either.Migrations
concepts.0090andmappings.0061are non-atomic and useCONCURRENTLY.migratemigratedoesThe migrations never drop the existing indexes. Reversing drops only the new ones.
Rollout: build by hand first (recommended)
Every API container runs
migrate(pre_startup.sh) before it starts serving. On production-sized tables, aCONCURRENTLYbuild takes minutes, because it scans the table twice. If it outlasts the load balancer's health-check grace, the container is replaced mid-build. That leaves an INVALID index, and every restart hits it.So in a quiet window, before deploying:
CREATE INDEX CONCURRENTLYabove for each table, on staging first.pg_index.indisvalid.The deploy's
migratethen only checks the new indexes. If the manual build is skipped, staging'smigratestops with the SQL to run. Staging deploys before the production approval, so that happens before production.Tests
core/integration_tests/tests_list_indexes.py(new):id, and the existing ones are still there;OFFSET 90000runs as anIndex Only ScanwithHeap Fetches: 0.Review
pylint -j0 core/is clean.🤖 Generated with Claude Code