Performance of event vs enrollment type PI with V{tei_count}

Continuing the discussion from The difference between V{enrollment_count}, V{tei_count}, V{event_count}:

Hi @malekpour,

Glad you found the thread useful :up_arrow:

Your intuition regarding performance is validated by some initial tests (see below), but only when there is no filter for an event value and no event date boundaries. That said, I think the best strategy still relies on the use case and what outputs are expected.

When the expression is V{tei_count} and the aggregation type is Count, which Analytics Type is more performant, Event or Enrollment? Let’s assume the filter logic is set up appropriately in each case and we’re only interested in the latest event of a program stage.

In this scenario, you are counting unique individuals, but filtering by the latest event for a PS.

  • What is the orgunit dimension for the enrolment? Where the event took place, or current TEI ownership, or the enrolment orgunit? If it is the first two, then there may be some joins in an enrollment type PI. If the orgunit dimension is the enrolment orgunit, then facilities will have 0s for patients they served, but did not enrol themselves – this may be confusing.
  • If you are interested in any analytics with a DE value, then enrolment type PI would also be joining with an event data value in any case, thus you lose the performance gains from tei count with enrolment type PI.
  • If you are interested in a data value for latest event for a PS, then you have to use enrolment type PI anyway. I’m not aware of any way to filter for latest event in event type PI. But if you are querying to see, for example, “how many patients presented with uncontrolled BP in May 2026”–not concerned for latest value–then event type with tei_count expression would work.
  • A lot also depends if you are using event date analytic period boundaries. I expect you would want to display period of patient encounters, not when they entered the system, so if you use enrolment type, you would again be joining with event tables.

TLDR; the enrolment type seems to be best match for the use case, but with event type boundaries and data element value in filter, it would not have substantial performance gains over event type.

I ran some local tests and interpreted log outputs with Claude below.

PI performance (v40.9.1)

Block 1 — _EV_enrbounds Block 2 — _EV_evbounds Block 3 — _ENR_enrbounds Block 4 — _ENR_EVbounds
PI ID Tw02Ffi7nIz ZMGabl5xFg5 tXzET5znaPv xszjiwSGLum
Config
analyticsType EVENT EVENT ENROLLMENT ENROLLMENT
orgUnitField ENROLLMENT (explicit) not set → event OU not set → enrollment OU not set → enrollment OU
boundaryTarget ENROLLMENT_DATE EVENT_DATE ENROLLMENT_DATE EVENT_DATE :warning: mismatch
expression / filter V{tei_count} + d2:hasValue(...) V{tei_count} + d2:hasValue(...) V{tei_count} + d2:hasValue(...) V{tei_count} + d2:hasValue(...)
Resulting SQL
FROM analytics_event_* analytics_event_* analytics_enrollment_* analytics_enrollment_*
Join to _orgunitstructure yes, on enrollmentou no no no
Top-level period filter enrollmentdate >= … and < … monthly in (…) and yearly in (…) enrollmentdate >= … and < … none at top level (pushed into subquery)
OU filter ous.uidlevel1 in (…) (via join) ax.uidlevel1 in (…) (direct) ax.uidlevel1 = … (direct) ax.uidlevel1 = … (direct)
d2:hasValue translation inline CASE predicate inline CASE predicate correlated subquery on analytics_event_* correlated subquery on analytics_event_* (date filter lives inside it)
Partition pruning available No Yes (via yearly) No No (subquery scans event table per enrollment)
GROUP BY none (literal label) ax.monthly none (literal label) none (literal label)
Semantic correctness May 2026 enrollments with the DE valued May 2026 events with the DE valued May 2026 enrollments where their latest stage event ever has the DE valued :warning: semantics shifted All enrollments under OU where their latest May 2026 stage event has the DE valued
Performance (May 2026 / level 1 / IWp9dQGM0bS)
Execute duration 0.066 ms 0.029 ms 0.045 ms 0.095 ms
Relative to fastest ~2.3× slower baseline ~1.6× slower ~3.3× slower
Why join + filter, no partition prune no join + equality + partition prune + inline filter correlated subquery, but small dataset hides it correlated subquery + date filter pushed inside it

Hi @brian,

Many thanks for the detailed explanation and sharing the benchmark results. My benchmarks match with what you observed too. In my case, the performance advantage of enrollment-type programs tends to disappear as soon as the query needs to check for a DE in events or when a boundary condition requires an event date (or another date-type DE in events). At that point, the additional JOINs between the enrollment and event tables cancel out the performance gain.

Before I started looking at the SQL generated by DHIS2 (and SQL tables themselves), I had assumed that information about the latest event for each program stage and enrollment was already materialized in the enrollment analytics tables, which would have avoided the need for these joins. Since that is not the case, it is certain that in the above-mentioned scenarios, enrollment-type has little, if any, performance benefit.