Monday, July 27, 2026
1 change · 19.0
Enhancements to existing features
The Time Off Ledger report was redesigned to avoid extremely slow database processing on large employee datasets. This reduces upgrade and reporting load dramatically while keeping the report results unchanged.
Original PR description
On a production-sized database the `hr.leave.attendance.report` SQL view took over three hours to run. This caused a significant load on the upgrade platform when processing databases that have a…
On a production-sized database the `hr.leave.attendance.report` SQL view took over three hours to run. This caused a significant load on the upgrade platform when processing databases that have a large number of `hr.employee` records. Specifically, when running `test_mock_crawl` and mocking the `Attendances > Reporting > Time Off Ledger` menu. PostgreSQL mis-estimated the row counts produced by the view: - The public-holiday check combined an `OR` with a function-wrapped `BETWEEN` (`... AT TIME ZONE ... ::date`), which the planner cannot estimate; it predicted ~1 surviving row (actual: 29.4M) and chose nested loops that re-aggregated whole tables once per output row. - The attendance sub-query aggregated the entire `hr_attendance` table with no date bound, and was re-executed per output row. - The working schedule was resolved with a per-(employee, day) `LIMIT 1` lookup into `hr_version` (29.4M index probes). Rewrite the view as a set of CTEs: every heavy table is scanned once, joins use plain equality keys (hash-joinable), public holidays are pre-expanded so their exclusion stays an anti-join, the attendance aggregate is bounded to the report window, and hr_version is resolved by expanding each version over the days it covers. None of the CTEs are explicitly materialized: left to its own heuristic, PostgreSQL inlines a CTE referenced only once as a plain subquery and materializes the ones referenced more than once, which benchmarked faster than forcing materialization everywhere. This rewrites the body of the SQL view only: no schema change, no new field, no index, no migration. The report output is unchanged. Measured with EXPLAIN (ANALYZE, BUFFERS) on the same database: | metric | before | after | factor | |----------------|---------------|----------|--------| | execution time | 11 852 235 ms | 1 834 ms | ~6500x | | buffer hits | 115 311 556 | 71 890 | ~1600x | upg-4288902