Skip to content

Combine involved user and group identity link checks into a single EXISTS - #4310

Merged
filiphr merged 2 commits into
flowable:mainfrom
filiphr:combined-involved-user-query
Oct 16, 2026
Merged

filiphr merged 2 commits into
flowable:mainfrom
filiphr:combined-involved-user-query

Conversation

@filiphr

@filiphr filiphr commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

When a query restricts by a user or any of their groups, the identity link checks were rendered as EXISTS(user link) OR EXISTS(group link). Most databases can only turn an EXISTS into a semi-join when it is not inside an OR, so they ran both subqueries for every candidate row. On Postgres the resulting cost estimate also lands around the JIT thresholds, so every call paid for JIT compilation.

The user and group conditions now go into one EXISTS with the OR inside it. The rows returned are the same, and when nothing else is in the OR the database can execute it as a semi-join. This applies to:

  • the or() blocks of the process, historic process, case and historic case instance queries, for involvedUser, involvedGroups and the typed involvedUser(userId, type) / involvedGroup(groupId, type)
  • startableByUser / startableByUserOrGroups for process and case definitions
  • forUserOrGroups for external worker job queries and acquisition
  • the or() blocks of the task and historic task queries (there the assignee and owner checks stay next to the EXISTS, so this only saves one subquery per task)

The second commit adds an index on GROUP_ID_ of ACT_HI_IDENTITYLINK (ACT_RU_IDENTITYLINK already has one), so historic queries by involved groups no longer have to scan the whole historic identity link table. This bumps the schema version to 8.1.0.3.

Measurements

Count query of or().involvedUser(user).involvedGroups(groups).endOr(), on a dataset with about 15k running process instances, 6k running case instances, 50k historic process instances, 90k runtime and 520k historic identity links. Median time in ms for a user with 500 links (and one with 10k links), before and after:

Query Postgres 18 (JIT on, default) MySQL 8.4 Oracle 23
Process instances 150 → 3.6 (152 → 10.5) 205 → 1.4 (185 → 25) 0.6 → 0.4 (10.8 → 9.6)
Case instances 12.4 → 1.0 (16.6 → 3.2) 72 → 1.2 (65 → 19) 0.6 → 0.4 (3.0 → 3.5)
Historic process instances 211 → 28 (222 → 36) 584 → 107 (525 → 112) 24 → 30 (36 → 32)
  • Postgres: the OR of EXISTS cannot be pulled up into a semi-join. The cost estimate drops from about 530k to about 5k, below the JIT thresholds. With JIT off both shapes take about the same time.
  • MySQL: the OR of EXISTS runs as two dependent subqueries per row. The single EXISTS uses an index merge over the user and group indexes and a semi-join.
  • Oracle: already unnests the OR of EXISTS into a UNION ALL view, so it is neutral (same buffer gets).
  • The historic numbers were measured without the new GROUP_ID_ index.

Check List:

  • Unit tests: YES
  • Documentation: NA

…ISTS

When a query restricts by a user or any of their groups, the identity link
checks were rendered as EXISTS(user link) OR EXISTS(group link). Most
databases (Postgres, MySQL, CockroachDB, SQL Server, DB2) can only turn an
EXISTS into a semi-join when it is not inside an OR, so they ran both
subqueries for every candidate row. On Postgres the resulting cost estimate
also triggered JIT compilation on every call.

The user and group conditions now go into one EXISTS with the OR inside it.
The result is the same, and when nothing else is in the OR the EXISTS can be
executed as a semi-join. This applies to:

* the or() blocks of the process, historic process, case and historic case
  instance queries, for involvedUser, involvedGroups and the typed
  involvedUser(userId, type) / involvedGroup(groupId, type)
* startableByUser / startableByUserOrGroups for process and case definitions
* forUserOrGroups for external worker job queries and acquisition
* the or() blocks of the task and historic task queries
ACT_RU_IDENTITYLINK has an index on GROUP_ID_, but ACT_HI_IDENTITYLINK only
has one on USER_ID_. Historic process, case and task queries that restrict
by involved groups therefore had to scan the whole historic identity link
table instead of looking up the matching links.
@filiphr
filiphr merged commit f6017e3 into flowable:main Oct 16, 2026
3 checks passed
@filiphr
filiphr deleted the combined-involved-user-query branch October 16, 2026 12:11
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.

1 participant