From 8d9376381b4edd8943e6f31423e1fcead0f04b1d Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Tue, 1 Sep 2026 14:28:29 +0300 Subject: [PATCH] fix: the Running exchange filter took 24 seconds Measured on 1M exchanges: 24,185ms to 229ms. Same rows, same counts. --- SW.Bitween.Api/Resources/Xchanges/Search.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/SW.Bitween.Api/Resources/Xchanges/Search.cs b/SW.Bitween.Api/Resources/Xchanges/Search.cs index cadf7daf..a3767259 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Search.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Search.cs @@ -127,7 +127,16 @@ from delayedRetry in drGroup.DefaultIfEmpty() switch (statusFilter.Value) { case "0": - query = query.Where(i => i.Status == null); + // "Still running" means no result row exists yet. Asking for it as + // Status == null reads as `x0.success IS NULL` on the left join, and + // Postgres cannot estimate that: it guesses one row, plans every join + // above it for one row, and picks per-row sequential scans of the small + // side tables. Measured on 1M exchanges that was 22.8s for 25 rows. + // NOT EXISTS asks the same question as an anti-join, which it can + // estimate — 34ms. Equivalent because success is NOT NULL, so a result + // row can never itself carry a null status. + query = query.Where(i => + !dbContext.Set().Any(r => r.Id == i.Id)); break; case "1": query = query.Where(i => i.Status == true && i.ResponseBad != true);