From a901c87ab4f2d2efad60016f7d39ce729c5597df Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 11 Jun 2026 16:10:48 +0530 Subject: [PATCH] perf: merge 3 conversation COUNT queries in /meta into single query (#13536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request Template ## Description - Merges 3 separate COUNT queries in `ConversationFinder#set_count_for_all_conversations` into a single query using PostgreSQL `COUNT(*) FILTER (WHERE ...)` - Every call to `/meta` and conversation index fires 3 COUNT queries against the conversations table — one for "mine", one for "unassigned", one for "all". These share the same base query and scan the same rows, but execute as 3 independent round-trips. from pg_stats | Query | Calls | Total DB Time | Avg Latency | |-------|-------|--------------|-------------| | COUNT unassigned | 4.1M | 141,081 sec | 34ms | | COUNT all | 4.1M | 132,302 sec | 32ms | | COUNT mine | 3.7M | 37,637 sec | 10ms | | **Total** | **~12M** | **311,020 sec** | | `/meta` alone runs at 2.3K RPM (NewRelic), triggered on every WebSocket event (conversation created/updated/status changed/assignee changed). Fixes https://linear.app/chatwoot/issue/INF-43/make-single-query-to-count-the-assigned-unassigned-and-all-instead-of ## Type of change - [x] Performance fix ## How Has This Been Tested? - [x] Verify conversation list shows correct mine/unassigned/assigned/all counts - [x] Verify /meta endpoint returns correct counts - [x] Verify counts update correctly when assigning/unassigning conversations - [x] Verify counts with team filter applied - [x] Monitor DB query count reduction in NewRelic after deploy ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sony Mathew --- app/finders/conversation_finder.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index fa437327d..1c27d8260 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -40,7 +40,7 @@ class ConversationFinder def perform set_up - mine_count, unassigned_count, all_count, = set_count_for_all_conversations + mine_count, unassigned_count, all_count = set_count_for_all_conversations assigned_count = all_count - unassigned_count filter_by_assignee_type @@ -184,6 +184,17 @@ class ConversationFinder end def set_count_for_all_conversations + return legacy_count_for_all_conversations if @conversations.limit_value || @conversations.offset_value || @conversations.eager_loading? + + counts = @conversations.unscope(:order).pick( + Arel.sql("COUNT(*) FILTER (WHERE assignee_id = #{current_user.id})"), + Arel.sql('COUNT(*) FILTER (WHERE assignee_id IS NULL)'), + Arel.sql('COUNT(*)') + ) + counts || [0, 0, 0] + end + + def legacy_count_for_all_conversations [ @conversations.assigned_to(current_user).count, @conversations.unassigned.count,