Repository navigation
[16.0][PERF] ni_observation: filter latest-observation views by id first - #176
Merged
Merged
Conversation
PostgreSQL pushes a WHERE clause into a DISTINCT ON subquery only when it uses the DISTINCT ON columns. The ORM reads these views by id and searches them by survey_response_id, so every call rebuilt the view from the whole ni_observation table (up to ~100 s per read on production). The NOT EXISTS form lets the planner start from the primary key and anti-join through the (key, type_id, occurrence DESC, id) indexes. The anti-join cannot group NULLs the way DISTINCT ON does, so rows without type_id or occurrence are left out, and the encounter view leaves out observations without an encounter. Every caller filters the encounter view by encounter_id, and the report model already requires type_id. Fixes #175
The latest-observation views are searched by survey_response_id about a thousand times per tuning window. The index name matches the one already created on production, so the upgrade leaves it in place. Fixes #175
This was referenced Oct 29, 2026
Closed
This was referenced Oct 29, 2026
Merged
Merged
Merged
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.
Fixes #175 · Related #174
Summary (TH)
เปลี่ยน view
ni.patient.observationและni.encounter.observationจากDISTINCT ONเป็นNOT EXISTSเพื่อให้ PostgreSQL กรองด้วยidหรือsurvey_response_idได้ก่อนเลือกแถวล่าสุด ไม่ต้องสร้าง view ใหม่จากทั้งตารางni_observationทุกครั้งที่อ่าน และเพิ่ม index ให้ni_observation.survey_response_idChanges
ni_observation(patient_id, type_id)/(encounter_id, type_id)with aNOT EXISTSanti-join: highestoccurrence, ties broken by highestid, the same order as the oldORDER BY.SELECT o.*keeps the column set. New tests intest_latest_observation.py. README documents the views and the index dependency.ni_questionnairesurvey_response_idonni.observation.abstractgetsindex=True.ni.observationis the only stored model that inherits it, so the index is namedni_observation_survey_response_id_index, the name already on production.Decisions to review
o.encounter_id IS NOT NULL).DISTINCT ONreturned one row per type for all observations without an encounter. Every caller (encounter_observation_ids,observation_latest_ids,_compute_observation_latest) filters byencounter_id,ni_encounter_ob_line_actionhas no menu or reference, andnirun-dop-16.0does not use the view.type_idoroccurrenceare left out of both views. Both fields are nullable in the schema, and the anti-join cannot group NULLs the wayDISTINCT ONdoes: every NULL row would come back. The report model already declarestype_id required=True. NoNOT NULLconstraint was added.How tested
-i ni_observation,ni_questionnaire --test-enable: 0 failed, 0 errors of 149 tests.-i ni_careplan --test-enable(searches the patient view): 0 failed.ni_careplan_problemis not installed on the local DB, so it was not tested.occurrencewith a higheridloses, a newer row without a value is ignored, another patient/encounter does not hide the latest, search byidreturns only the latest row, and an observation without an encounter is not in the encounter view. The tied-occurrencecase is covered by the existingtest_same_occurrence_picks_the_last_record.DISTINCT ONvs new view on a local DB (~3.8k rows): no new rows in either view. Patient view: 1 row fewer (id 4405, notype_id). Encounter view: 18 rows fewer, all without an encounter.EXPLAIN(withenable_seqscan = off, the table is small): a read byidusesni_observation_pkey, a search bysurvey_response_idusesni_observation_survey_response_id_index, and both anti-join throughni_observation__{patient,encounter}__ob_type__idx. The onlySortorders the result rows; there is no full scan or sort ofni_observation.-u ni_questionnaireon a DB that already hasni_observation_survey_response_id_indexkeeps the same index (same OID).pre-commitclean on the changed files.After merge (production)
ni_observation__patient__ob_type__idxon production still has the old(patient_id, type_id)definition.tools.create_index()skips existing names, so rebuild it as(patient_id, type_id, occurrence DESC, id)withCREATE INDEX CONCURRENTLY, outside the module upgrade, to get index-only anti-joins. The encounter-side index was already rebuilt on 2026-10-23.