From 879cb108c9e7ffe5a62419b33405c059032f12bb Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Fri, 5 Jun 2026 22:27:14 +0300 Subject: [PATCH 1/2] Seek changes-filter predecessor via LATERAL instead of a version scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The :only_changes activity filter identified each journal's predecessor with `version = (SELECT MAX(version) WHERE version < current)`. That predicate cannot use the (journable_type, journable_id, version) index, so Postgres scanned every journal of the journable and filtered by version — turning a per-page filter into an O(history) sweep run twice (pagy's count plus the page query). A LATERAL `ORDER BY version DESC LIMIT 1` seeks the predecessor through that index in a single row, preserving gap-tolerant matching on `< version`. --- .../paginator/journal_changes_filter.rb | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb b/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb index 0c5075b04a2..538dcc99427 100644 --- a/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb +++ b/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb @@ -60,13 +60,10 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter def attribute_data_changes_condition_sql <<~SQL.squish SELECT 1 - FROM journals predecessor + FROM #{predecessor_lateral_sql} INNER JOIN work_package_journals pred_data ON predecessor.data_id = pred_data.id INNER JOIN work_package_journals curr_data ON journals.data_id = curr_data.id - WHERE predecessor.journable_id = journals.journable_id - AND predecessor.journable_type = journals.journable_type - AND predecessor.version = (#{max_predecessor_version_sql}) - AND (#{data_changes_condition_sql}) + WHERE (#{data_changes_condition_sql}) SQL end @@ -98,15 +95,22 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter ) end - # Identify the immediate predecessor journal for comparison. - # NB: Journal versions are incremental but not guaranteed to be sequential. - def max_predecessor_version_sql + # The immediate predecessor journal, exposed as a `predecessor` relation for + # comparison. Seeking the highest version below the current one through the + # (journable_type, journable_id, version) index keeps this a single-row lookup + # per journal; versions are incremental but may have gaps, so the seek matches + # on `< version` rather than `version - 1`. + def predecessor_lateral_sql <<~SQL.squish - SELECT MAX(version) - FROM journals p2 - WHERE p2.journable_id = journals.journable_id - AND p2.journable_type = journals.journable_type - AND p2.version < journals.version + LATERAL ( + SELECT p.id, p.data_id + FROM journals p + WHERE p.journable_id = journals.journable_id + AND p.journable_type = journals.journable_type + AND p.version < journals.version + ORDER BY p.version DESC + LIMIT 1 + ) predecessor SQL end @@ -154,10 +158,7 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter <<~SQL.squish SELECT 1 FROM #{table} curr - LEFT JOIN journals predecessor - ON predecessor.journable_id = journals.journable_id - AND predecessor.journable_type = journals.journable_type - AND predecessor.version = (#{max_predecessor_version_sql}) + LEFT JOIN #{predecessor_lateral_sql} ON TRUE LEFT JOIN #{table} pred ON pred.journal_id = predecessor.id AND #{join_conditions} @@ -175,16 +176,13 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter <<~SQL.squish SELECT 1 - FROM journals predecessor + FROM #{predecessor_lateral_sql} INNER JOIN #{table} pred ON pred.journal_id = predecessor.id LEFT JOIN #{table} curr ON curr.journal_id = journals.id AND #{join_conditions} - WHERE predecessor.journable_id = journals.journable_id - AND predecessor.journable_type = journals.journable_type - AND predecessor.version = (#{max_predecessor_version_sql}) - AND curr.id IS NULL + WHERE curr.id IS NULL SQL end end From 32a8bc43cc17d1bd0109d6a5e5c8002c6b10007f Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Tue, 9 Jun 2026 08:36:32 +0300 Subject: [PATCH 2/2] Surface the predecessor lateral alias at each call site The LATERAL subquery is aliased `predecessor` where it is joined rather than inside the helper, so the relation each EXISTS clause references is visible without reading the helper. --- .../paginator/journal_changes_filter.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb b/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb index 538dcc99427..9eaa3b2f598 100644 --- a/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb +++ b/app/services/work_packages/activities_tab/paginator/journal_changes_filter.rb @@ -60,7 +60,7 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter def attribute_data_changes_condition_sql <<~SQL.squish SELECT 1 - FROM #{predecessor_lateral_sql} + FROM #{predecessor_lateral_sql} predecessor INNER JOIN work_package_journals pred_data ON predecessor.data_id = pred_data.id INNER JOIN work_package_journals curr_data ON journals.data_id = curr_data.id WHERE (#{data_changes_condition_sql}) @@ -95,11 +95,11 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter ) end - # The immediate predecessor journal, exposed as a `predecessor` relation for - # comparison. Seeking the highest version below the current one through the - # (journable_type, journable_id, version) index keeps this a single-row lookup - # per journal; versions are incremental but may have gaps, so the seek matches - # on `< version` rather than `version - 1`. + # The immediate predecessor journal, for comparison against the current one. + # Callers alias this as `predecessor`. Seeking the highest version below the + # current one through the (journable_type, journable_id, version) index keeps + # this a single-row lookup per journal; versions are incremental but may have + # gaps, so the seek matches on `< version` rather than `version - 1`. def predecessor_lateral_sql <<~SQL.squish LATERAL ( @@ -110,7 +110,7 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter AND p.version < journals.version ORDER BY p.version DESC LIMIT 1 - ) predecessor + ) SQL end @@ -158,7 +158,7 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter <<~SQL.squish SELECT 1 FROM #{table} curr - LEFT JOIN #{predecessor_lateral_sql} ON TRUE + LEFT JOIN #{predecessor_lateral_sql} predecessor ON TRUE LEFT JOIN #{table} pred ON pred.journal_id = predecessor.id AND #{join_conditions} @@ -176,7 +176,7 @@ class WorkPackages::ActivitiesTab::Paginator::JournalChangesFilter <<~SQL.squish SELECT 1 - FROM #{predecessor_lateral_sql} + FROM #{predecessor_lateral_sql} predecessor INNER JOIN #{table} pred ON pred.journal_id = predecessor.id LEFT JOIN #{table} curr