Performance: observation_event - #974
Merged
Merged
Conversation
mpeels
marked this pull request as ready for review
July 29, 2026 23:28
eliSkylight
previously approved these changes
Aug 10, 2026
eliSkylight
left a comment
Contributor
There was a problem hiding this comment.
the CTE approach is neatly organized. thanks!
ericbuckley
reviewed
Aug 10, 2026
ericnagel
approved these changes
Aug 11, 2026
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.
Description
Updates the
observation_eventstored proc and adds a unit test for coverage. The unit test was tested against the pre-optimized stored procedure and post-optimized stored proc with both passing.Related Issue
APP-899
Detailed Summary
This PR restructures
sp_observation_eventfrom a set of deeply nested, per-row correlated subqueries into a CTE-driven query. The output shape and columns are unchanged — this is a performance and maintainability refactor, not a functional rewrite.Key Optimizations
The original computed followup_observations using four separate UNION-joined subqueries, each one hard-coding one more hop of act_relationship joins (act1, act1→act2, act1→act2→act3, act1→act2→act3→act4). This subquery was re-evaluated once per output row inside the OUTER APPLY.
The updated version replaces this with a single recursive CTE (related_acts) bounded by OPTION (MAXRECURSION 4), computed once for the whole batch and then joined via resolved_followups. This avoids redundant re-scans of act_relationship for every observation row and scales much better as the input id list grows.
resolved_phc_uids, resolved_followups, and resolved_person_participations are now computed once as CTEs ahead of the main SELECT, rather than being recomputed as correlated subqueries inside OUTER APPLY for each observation. This lets the optimizer build a single set-based plan instead of executing the same logic N times (once per row).
The original joined the full person → person_name → entity_id → role → code_value_general chain for every person in the table, relying on the outer join to filter down to relevant rows afterward — meaning the fan-out (entity_id/role joins) was computed broadly before being filtered.
The updated version introduces a relevant_persons CTE that first identifies the distinct person_uids actually referenced by ACTIVE participations on the requested observations, then restricts the enrichment join to just those persons (WHERE person.person_uid IN (SELECT person_uid FROM relevant_persons)). This is a pure filter push-down — same results, far less work when the person table is large.
The original called STRING_SPLIT(@obs_id_list, ',') inline in the final WHERE clause with an implicit type comparison. The updated version parses the input once into a base_ids CTE with an explicit CAST(... AS BIGINT), and every downstream CTE/join reuses that single canonical set — removing repeated parsing and making the join type explicit.
The original wrapped the driving Observation table plus its OUTER APPLY in a derived table (... AS results) which was then joined to act and observation_interp. The updated version joins act/observation_interp directly to Observation, with the id-list filter applied straight to the driving table (WHERE o.observation_uid IN (SELECT observation_uid FROM base_ids)). This removes a layer of indirection and makes the query easier for the optimizer (and for humans) to reason about.
Commented-out blocks for ldf_observation and associated_investigations (already unused/commented in the original) were removed rather than carried forward, reducing noise.
Checklist