Skip to content

perf: extract AOC ACL check into MATERIALIZED CTE for data value export [DHIS2-21497]#23896

Merged
jason-p-pickering merged 13 commits into
masterfrom
perf/DHIS2-21497-aoc-materialized-cte
May 14, 2026
Merged

perf: extract AOC ACL check into MATERIALIZED CTE for data value export [DHIS2-21497]#23896
jason-p-pickering merged 13 commits into
masterfrom
perf/DHIS2-21497-aoc-materialized-cte

Conversation

@jason-p-pickering

@jason-p-pickering jason-p-pickering commented May 13, 2026

Copy link
Copy Markdown
Contributor

GET /api/dataValueSets/ with children=true was slow for non-superusers because PostgreSQL inlined the AOC sharing check (NOT EXISTS over JSONB functions) into the main query. Query analysis showed that the row estimate was 14 (actual: 2,981), so it placed the 225 AOC rows as the outer driver of a nested loop. The resulted in scanning the entire data value table 225 times. 737k buffer hits, ~1,150ms on the Sierra Leone demo DB.

The fix extracts the check into a MATERIALIZED CTE (aoc_access). This forces it to execute once before the main join is planned, giving the planner real cardinality (225 rows) and a hash join instead. 4k buffer hits, ~130ms.

Performance comparison below.

Requests master req/s feature req/s Diff (ms) Change
GET dataValueSets - root OU with descendants 1,109 1.55 137 2.67 -972 ⬇️ -87.6%

95th Percentile Response Time (p95) (ms)

Requests master req/s feature req/s Diff (ms) Change
GET dataValueSets - root OU with descendants 1,191 1.55 194 2.67 -998 ⬇️ -83.7%

⬇️ = faster, ⬆️ = slower

Percentiles use the inclusive definition (e.g. p95 = X means 95% of requests responded in X ms or less) and are computed over OK + KO responses.
🤖 AI Assisted

jason-p-pickering and others added 11 commits May 13, 2026 09:43
…21497]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…[DHIS2-21497]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…on for AOC CTE [DHIS2-21497]

Instead of maintaining a dual-JOIN template and erasing the wrong line at
runtime, determine the AOC path (CTE vs direct join) upfront and build the
SQL string with String.replace() before handing it to QueryBuilder.

Removes the eraseLineContaining / erasedFragments machinery from QueryBuilder
that was added specifically to work around the alias-extraction limitation
(both JOIN lines extract alias "aoc", making eraseJoinLine unable to
distinguish them). The simpler approach: if useAocCte, inject the MATERIALIZED
CTE block and JOIN aoc_ids; otherwise inject the direct categoryoptioncombo
join. No erasure of AOC-related lines needed at all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the multi-line AOC sharing CTE string into a `private static
final String AOC_CTE_BLOCK` text block, and uses
`CollectionUtils.isEmpty` for the orders null-or-empty check to reduce
cognitive complexity from 16 to 15 (Sonar S1764 / S5663).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…arams

Moves the SQL template and AOC CTE block construction into a private
static `getSql(String aocAclSql)` helper, keeping `createExportQuery`
focused on parameter binding and query builder configuration.
The WHERE NOT EXISTS subquery is now spread across readable lines
inside the text block (test expectation updated to match).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per reviewer feedback: the QueryBuilder DSL is designed for one SQL template
and one parameter/condition block — splitting the SQL into a helper method
works against this design. Inline everything back into createExportQuery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jason-p-pickering
jason-p-pickering requested review from a team and stian-sandvold May 13, 2026 15:35
@sonarqubecloud

Copy link
Copy Markdown

@jason-p-pickering
jason-p-pickering merged commit 9d70349 into master May 14, 2026
18 checks passed
@jason-p-pickering
jason-p-pickering deleted the perf/DHIS2-21497-aoc-materialized-cte branch May 14, 2026 06:40
jason-p-pickering added a commit that referenced this pull request May 14, 2026
* perf: Adds Gatling performance test for aggregate data export with descendants [DHIS2-21490] (#23895)

* perf: add Gatling performance test for aggregate data export with descendants [DHIS2-21490]

Adds AggregateDataExportPerformanceTest covering GET /api/dataValueSets/ with
children=true against the Sierra Leone demo database — the exact scenario that
exposed the O(n²) org unit descendants cross-join fixed in DHIS2-21490.

Uses Sierra Leone root org unit and ART monthly summary dataset with a rolling
two-year date window, 5 concurrent users over 10 seconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add aggregate data export with descendats performance test

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* perf: extract AOC ACL check into MATERIALIZED CTE for data value export [DHIS2-21497] (#23896)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants