Dashboard chart by Approver with "Time in step" measure fails with a query timeout
Description
Description
A dashboard chart grouped by Approver (or Current step) with any measure other than Approvals fails to load for tenants with many approvals or large approver groups, for example GET /forge/jira/rest/gadget/chart?dimension=APPROVER&measure=TIME_IN_STEP&aggregation=MEDIAN&statuses=ONGOING. The log shows:
org.jooq.exception.DataAccessException: SQL [with "measured_config" as materialized (select ...ERROR: canceling statement due to user request
"Canceling statement due to user request" is the chart's 10-second query timeout (ApprovalAggregationDao.AGGREGATION_TIMEOUT_SECONDS), not a database failure. In the gadget editor the preview then fails with can't access property "filter", e is undefined, because the error comes back as HTTP 200 with the error page (not changed here).
Two causes add up:
-
The measure is computed for too many rows. One approval falls into several buckets (one per pending approver). Originally the measure was computed once per approver row, so for Time in step the two correlated subqueries on
approval_stepran 500 times for an approval waiting on a 500-member group. Computing it once per approval in a materialized CTE fixed that, but the CTE measured every approval in scope, including the ones nobody is pending on (with no status filter, every approval of the host). Joining that CTE to the approvers also cannot run in parallel. -
Postgres estimates the approvals in scope at about one row. Deleted, archived and outdated approvals were excluded with
approval_path_deleted.deleted_date is null(and the same forarchived_date/changed_date) on left-joined tables. Postgres estimates these filters from the columns of those tables, where the date is never null, so it expects almost no approvals to remain. It then picks nested loops that run once per approval or once per approver row. For example, on a large tenant a chart by Approver counting Ongoing approvals scansapproval_path_archivedonce per pending approver and does not finish within a minute.
Affected: charts by Approver or Current step with a measure (single and grouped), count charts with a status filter on large tenants, and, to a lesser degree, approval lists and personal approvals that use the same filters.
Fix
-
The approvers of each approval are collected into an array with the same join the count chart uses, grouped by approval. The materialized CTE
measured_configcomputes the measure once per approval from that per-approval array (joined by primary key), and the outer query unnests the array into buckets. A chart by Approver only measures approvals that have a pending approver. In a grouped chart, approvals without one still land in the empty bucket. -
ApprovalPathConditionsexcludes deleted, archived and outdated approvals withnot exists (...)and matches the Archived and Outdated statuses withexists (...), instead of testing the left-joined date columns for null. The semantics are the same, and Postgres now estimates the approvals in scope correctly. -
ChartDimensionSqljoins the Approver and Current step tables onapproval_config.config_idonly again. -
Results are unchanged; only the query shape changes.
AS MATERIALIZEDrequires PostgreSQL 12 or newer.
Testing
-
Charts by Approver and by Current step, grouped charts with Approver or Current step on either axis, personal views and count charts return the same rows as before, for every measure and with and without a status filter.
-
Approval lists (any, default, Ongoing, Archived, Outdated, Expired), personal approvals, approvals of a reference, reminders, find and reference scopes return the same rows as before.
-
Measured on a local copy of the schema with generated data (two data sets: host with 300,000 approvals and 2.3 million pending approver rows; host with 600,000 approvals and small groups), best of two runs:
|
Query |
Before |
After |
|---|---|---|
|
By Approver, Time in step, Average, any status (large groups) |
4.0 s |
2.5 s |
|
By Approver, Time in step, Median, Ongoing (large groups) |
4.4 s |
3.5 s |
|
By Approver, Time in step, Average, any status (many approvals) |
1.6 s |
1.1 s |
|
By Approver, Approvals, Ongoing (both data sets) |
over 30 s |
0.2-1.1 s |
|
By Current step, Approvals (many approvals) |
1.1 s |
0.3 s |
|
Personal approvals list (many approvals) |
5.2 s |
1.1 s |
|
Approval list count, default filter (many approvals) |
1.2 s |
0.4 s |