Repository navigation
OpenConceptLab/ocl_online#454 | Load list-row lookups once per page for concepts and mappings - #936
Conversation
…or concepts and mappings The concept and mapping list serializers looked up several things for every row: the latest version's uri (version_url), latest_source_version, the from/to concepts' sources and the sources' owners, and created_by/updated_by. A 1,000-row page of mappings ran about 8,500 queries. A list serializer (SourceChildPageSerializer) now loads these for the whole page after pagination: prefetches for the relations, and one query each for the latest version uris and the latest source versions. Serializing a single object is unchanged. The response is the same; the new tests compare it with row-by-row serialization and check that the query count doesn't grow with the page size, for HEAD and repo-version lists, default and verbose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex review, pass 1 (codex-cli 0.162.1, commit 4cb6f4e)Verdict: 1 finding (P1).
findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "full",
"commit": "4cb6f4e1",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "F1.1",
"severity": "P1",
"tag": null,
"file": "core/concepts/models.py",
"line": 986,
"claim": "Concept search pages still perform one source query per revision without a non-HEAD source association.",
"scenario": "Search results can contain concept revisions created since the last source snapshot. The batch loader caches latest_source_version=None, but serializing their property field calls get_parent_source_version(), which falls through to get_source_version_before_creation() and queries sources again. Pages containing 25 or 1,000 such revisions therefore incur 25 or 1,000 additional queries. The new tests miss this because every revision belongs to v1.",
"action": "Make parent-source resolution honor the cached absence and return the prefetched parent without another query. Add query-count coverage for search results containing revisions with no non-HEAD source membership.",
"lines_delta": null,
"confidence": "high"
}
]
} |
…p when a page found no released version Codex review F1.1: a concept revision in no released version (e.g. a search result saved since the last release) still made one query per row, through summary_properties -> get_parent_source_version() -> get_source_version_before_creation(). That lookup only searches the row's released versions, so when load_list_fields found none the answer is None. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Response to Codex pass 1:
|
Codex review, pass 2 (codex-cli 0.162.1, commit 61274f6)Verdict: clean. F1.1 resolved; no new findings. findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "verify",
"commit": "61274f65",
"verdict": "clean",
"prior": [
{
"id": "F1.1",
"status": "resolved"
}
],
"findings": []
} |
…shared list query-count tests From the simplification review of #936: - Concept and Mapping overrode SourceChildMixin.latest_source_version with the same body, leaving the mixin's unused. The shared body now lives on the mixin, next to load_list_fields, and the overrides are gone. - load_list_fields drops guards Django already short-circuits (empty page, no heads) and always sets _latest_version_uri. - The concept and mapping query-count tests share SourceChildListQueryCountMixin (core/integration_tests/test_utils.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simplification review (fresh Claude subagent, commit 61274f6)Verdict: 4 cuts proposed, all applied in 1744f47.
After the cuts, the new tests pass on this branch and fail on master: all 5, with 22 → 39, 22 → 37, 9 → 13, 11 → 17 and 12 → 24 queries for a page of 2 vs 4. simplify.json{
"reviewer": "claude subagent",
"perspective": "simplify",
"mode": "full",
"commit": "61274f65",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "S1",
"severity": null,
"tag": "shrink",
"file": "core/integration_tests/tests_mappings.py",
"line": 1088,
"claim": "MappingListQueryCountTest duplicates ConceptListQueryCountTest almost line for line (get_page, assert_page, set_released_versions, test_head_list, test_version_list differ only in model, M2M name, url attribute and serializers).",
"scenario": null,
"action": "Move the shared methods into one SourceChildListQueryCountMixin parameterized by model, relation, list_serializer, detail_serializer; each test class keeps its setUp and attributes.",
"lines_delta": -40,
"confidence": "medium"
},
{
"id": "S2",
"severity": null,
"tag": "shrink",
"file": "core/concepts/models.py",
"line": 984,
"claim": "Concept.latest_source_version and Mapping.latest_source_version are identical overrides with the same new guard; the mixin's own latest_source_version is dead code.",
"scenario": null,
"action": "Put the shared body (with the _latest_source_version guard) on SourceChildMixin and delete both overrides.",
"lines_delta": -10,
"confidence": "medium"
},
{
"id": "S3",
"severity": null,
"tag": "delete",
"file": "core/common/mixins.py",
"line": 650,
"claim": "The early `if not instances: return` and `if heads else {}` guard cases Django already short-circuits without SQL.",
"scenario": null,
"action": "Nothing replaces it.",
"lines_delta": -2,
"confidence": "medium"
},
{
"id": "S4",
"severity": null,
"tag": "shrink",
"file": "core/common/mixins.py",
"line": 671,
"claim": "The `if instance.id in latest_version_uris` branch is unnecessary; version_url falls back when the value is None.",
"scenario": null,
"action": "instance._latest_version_uri = latest_version_uris.get(instance.id)",
"lines_delta": -1,
"confidence": "high"
}
]
} |
Codex review, check of cuts (codex-cli 0.162.1, commit 1744f47)Verdict: clean. The simplification commit broke nothing. findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "cuts",
"commit": "1744f476",
"verdict": "clean",
"prior": [],
"findings": []
} |
snyaggarwal
left a comment
There was a problem hiding this comment.
Issue complete: no.
Findings:
-
Collection-scoped concept/mapping list responses still omit
latest_source_version, but oclweb3 uses that value in listing rows to render the source repo version. In oclweb3,ResultConstants.jsrenders the concept Repo column viasourceVersionOf(item), andsourceVersionOf()falls back toresource.latest_source_versionwhen references are not present. Because the API omits the field for collection-scoped serializers, oclweb3 has to requestincludeReferences=true&includeResolvedRepoVersions=truefor collection concept/mapping pages just to reconstruct the source version. That is a much heavier path, and in local testing evenlimit=1collection requests with references were slow enough to abandon. Please keep/providelatest_source_versionfor collection-scoped concept and mapping list rows instead of relying on the references fallback. -
load_list_fields()always performs the latest-source-version through-table lookup even when the active serializer removedlatest_source_versionfrom the response. On local LOINC/CIEL data this added ~13 ms / ~5k buffer hits for a 1,000-row collection concept page and ~20 ms / ~4k buffer hits for a collection mapping page, despite the value not being serialized. If the field remains conditional, this work should be gated by the active serializer fields; if we restore the field for collections, this should be measured as intentional response data rather than wasted work.
Additional query-plan notes from local oclapi2-db-1:
- LOINC HEAD source had ~535k concept members and ~537k mapping members; LOINC version
1507had ~267k concept and ~268k mapping members. - Source-version latest-source lookups for 1,000 LOINC concept/mapping IDs were about 10-11 ms hot.
- Global latest-source lookups for 1,000 global concept/mapping rows were about 12-13 ms hot.
- The
version_urllatest-version lookup was ~15-18 ms hot after warming, but cold/mixed-cache runs were materially higher (~150 ms for mappings and one ~485 ms concept run).
Security blockers: none.
Testing gap: the new tests cover source HEAD/version pages, but not global list pages or collection-scoped concept/mapping list responses consumed by oclweb3.
…sion, batched and gated per field From Sunny's review of #936: - Collection-scoped concept and mapping lists return latest_source_version again, as the source version the row's references in that collection resolved to (what TBv3 computes from includeReferences today), not the latest release holding the row (ocl_issues#2770). HEAD resolves to null. It's loaded once per page; a single object computes it for itself. - load_list_fields only runs the lookups the serialized fields read, so a collection mapping list skips the latest-release lookup. - Collection pages no longer query per row for previous_version_url, references, names, descriptions, source_versions or collection_versions. The last three are now ordered (created_at; sorted uris); the order was arbitrary before. - Tests cover collection and global lists too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex review, pass 3 (codex-cli 0.162.1, commit 11caddf)Verdict: 1 finding (P1). F1.1 still resolved.
findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "verify",
"commit": "11caddf1",
"verdict": "findings",
"prior": [
{
"id": "F1.1",
"status": "resolved"
}
],
"findings": [
{
"id": "F3.1",
"severity": "P1",
"tag": null,
"file": "core/common/mixins.py",
"line": 788,
"claim": "Collection source-version resolution still queries per row when references span distinct sources or versions.",
"scenario": "For a collection containing rows from different source/version tuples, every tuple misses the memoization cache. Each call to Source.resolve_reference_expression performs repository and version queries, so database work grows with page size. The new collection test only references self.v1, which hides this remaining N+1 path.",
"action": "Batch resolution of the page's distinct source/version keys, preserving reference-resolution semantics, and add query-count coverage with collection rows spanning multiple sources and versions.",
"lines_delta": null,
"confidence": "high"
}
]
} |
|
Response to Codex pass 3:
|
…references; plainer loaded attributes From the simplification review of 11caddf: load_list_fields loads a page's collection references once for either field that needs them; the reference ids are a plain loaded attribute; FIELD_PREFETCHES is a tuple of relation names; a shorter load_list_fields docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simplification review of the review response (fresh Claude subagent, commit 11caddf)Verdict: 6 cuts proposed: S2–S5 applied in ba8e086, S1 and S6 declined.
After the cuts: simplify.json{
"reviewer": "claude subagent",
"perspective": "simplify",
"mode": "full",
"commit": "11caddf1",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "S1",
"severity": null,
"tag": "delete",
"file": "core/concepts/serializers.py",
"line": 353,
"claim": "ConceptVersionListSerializer repeats the parent's latest_source_version pop.",
"scenario": null,
"action": "delete the subclass's view_kwargs line and pop",
"lines_delta": -3,
"confidence": "high"
},
{
"id": "S2",
"severity": null,
"tag": "shrink",
"file": "core/common/mixins.py",
"line": 778,
"claim": "load_collection_source_versions pre-loads references that load_list_fields could load once.",
"scenario": null,
"action": "load references in load_list_fields for references or latest_source_version; drop the pre-load",
"lines_delta": -3,
"confidence": "medium"
},
{
"id": "S3",
"severity": null,
"tag": "delete",
"file": "core/common/mixins.py",
"line": 764,
"claim": "The (collection id, ids) tuple guards a collection mismatch that can't happen within a request.",
"scenario": null,
"action": "plain _collection_reference_ids with a hasattr check",
"lines_delta": -1,
"confidence": "medium"
},
{
"id": "S4",
"severity": null,
"tag": "yagni",
"file": "core/concepts/models.py",
"line": 224,
"claim": "FIELD_PREFETCHES is a dict with one entry whose field equals its lookup.",
"scenario": null,
"action": "a tuple of relation names; prefetch fields.intersection(FIELD_PREFETCHES)",
"lines_delta": -1,
"confidence": "medium"
},
{
"id": "S5",
"severity": null,
"tag": "shrink",
"file": "core/common/mixins.py",
"line": 659,
"claim": "load_list_fields' docstring restates its loaders.",
"scenario": null,
"action": "keep the first sentence",
"lines_delta": -3,
"confidence": "medium"
},
{
"id": "S6",
"severity": null,
"tag": "delete",
"file": "core/common/serializers.py",
"line": 231,
"claim": "get_collection_in_scope's view.kwargs check repeats what request.instance establishes.",
"scenario": null,
"action": "check request.instance only",
"lines_delta": -2,
"confidence": "low"
}
]
} |
Codex review, check of cuts (codex-cli 0.162.1, commit ba8e086)Verdict: clean. The simplification commit broke nothing. findings.json{
"reviewer": "codex-cli 0.162.1",
"perspective": "defects",
"mode": "cuts",
"commit": "ba8e0860",
"verdict": "clean",
"prior": [],
"findings": []
} |
|
@snyaggarwal thanks, all three are addressed in 11caddf and ba8e086 (master merged in 04576dc, no conflicts). 1.
2. Work for fields the response drops. 3. Testing gap. Also changed while making collection pages flat:
Review loop:
|
Relates to OpenConceptLab/ocl_online#454 (partial: the ticket tracks two more causes, listed at the end).
Summary
Concept and mapping lists made several queries per row while serializing a page. For each row they looked up:
version_url);latest_source_version;created_by/updated_by.A 1,000-row page of mappings ran about 8,500 queries.
This change loads those once per page, after pagination:
SourceChildMixin.load_list_fields(instances)prefetches each model'sLIST_PREFETCHES. It then runs oneDISTINCT ONquery for the HEAD rows' latest version uris and one for each row's latest released source version (plus anin_bulkfor those sources).SourceChildPageSerializer, aListSerializer, calls it for the page. It's set aslist_serializer_classonConcept{List,VersionList,Detail}SerializerandMapping{List,VersionList,Detail}Serializer, so search results get it too.version_urlandlatest_source_versionuse the page-loaded values when they're there. Serializing a single object runs the same code as before.It loads after pagination rather than annotating the page query, because a subquery annotation is evaluated for every row an OFFSET skips. On a deep page of a large source, EXPLAIN showed both subqueries running for the ~300k skipped rows.
Collection-scoped lists (from review)
latest_source_versionis back in collection scope, as the source version the row's references in that collection resolved to (newest first;HEADgivesnull). That's what TBv3 computes fromincludeReferencestoday; the plain latest release was the ocl_issues#2770 bug. It's loaded once per page.previous_version_url,references, names, descriptions,source_versionsorcollection_versions.source_versionsbycreated_at,collection_versionsas a sorted list (was a set), and reference uris by id.Query counts
Local test stack, CIEL-shaped fixture, 1,000-row page of HEAD mappings:
The local DB has no network round trip, so production saves more per query than this shows. Timings from before the change are recorded on the ticket.
Tests
MappingListQueryCountTestandConceptListQueryCountTest(new, sharingSourceChildListQueryCountMixinincore/integration_tests/test_utils.py) cover HEAD, repo-version, collection and global lists, both default andverbose=true. Each checks three things:latest_source_versionandversion_urlhave the expected values.ConceptListQueryCountTest.test_revisions_in_no_released_versioncovers search-result rows saved since the last release.pylint -j0 core/is clean.Review
get_source_version_before_creation().latest_source_versionon the mixin, two redundant guards dropped, shared test mixin.Details are in the comments below.
Not in this PR (tracked on OpenConceptLab/ocl_online#454)
🤖 Generated with Claude Code