Continuing the discussion from The difference between V{enrollment_count}, V{tei_count}, V{event_count}:
Hi @malekpour,
Glad you found the thread useful ![]()
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_countexpression 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 |
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 |
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 |