perf(store): index (Receiver, Date) so tell history streams sorted

GetTellHistoryWithSender filters on Receiver and orders by Date DESC. Without a
matching index SQLite sorted the whole receiver history into a temp b-tree
before yielding row one -- measured 10 ms to first row against 9621 tells, all
of it under TabsListLock, which defeats the early break in the caller.

(Receiver, ChatType, Date) does not help: the ChatType IN filter sits between
the equality prefix and the sort column, so the temp b-tree stays. Verified on
a real database: plan now reads SEARCH ... USING INDEX idx_messages_receiver_date
with no sort step.

No SQL LIMIT -- an earlier 500-row cap was removed in v1.4.10 because it cut
less-frequent partners off the back of the window, and 83% of partners have
fewer than 21 tells in total.

The migration dispatcher is cumulative, so Migrate5 is appended to every
existing case, not just the new one.
This commit is contained in:
2026-08-17 06:50:04 +02:00
parent d2da51a4f7
commit 89c66e0d3d
+26 -1
View File
@@ -279,18 +279,25 @@ internal class MessageStore : IDisposable
migrationsToDo.Add(Migrate2);
migrationsToDo.Add(Migrate3);
migrationsToDo.Add(Migrate4);
migrationsToDo.Add(Migrate5);
break;
case 1:
migrationsToDo.Add(Migrate2);
migrationsToDo.Add(Migrate3);
migrationsToDo.Add(Migrate4);
migrationsToDo.Add(Migrate5);
break;
case 2:
migrationsToDo.Add(Migrate3);
migrationsToDo.Add(Migrate4);
migrationsToDo.Add(Migrate5);
break;
case 3:
migrationsToDo.Add(Migrate4);
migrationsToDo.Add(Migrate5);
break;
case 4:
migrationsToDo.Add(Migrate5);
break;
}
@@ -430,6 +437,23 @@ internal class MessageStore : IDisposable
SetMigrationVersion(4);
}
private void Migrate5()
{
_logger.LogInformation("Running migration 5: Add (Receiver, Date) index for tell history");
// GetTellHistoryWithSender filters on Receiver and orders by Date DESC.
// Without a matching index SQLite sorts the whole receiver history into a
// temp b-tree before returning row one, which defeats the early break in
// the caller. (Receiver, ChatType, Date) does NOT help: the ChatType IN
// filter sits between the equality prefix and the sort column.
using var cmd = Connection.CreateCommand();
cmd.CommandText =
"CREATE INDEX IF NOT EXISTS idx_messages_receiver_date ON messages (Receiver, Date);";
cmd.ExecuteNonQuery();
SetMigrationVersion(5);
}
private void SetMigrationVersion(int version)
{
_logger.LogInformation($"Setting version {version}");
@@ -1014,7 +1038,8 @@ internal class MessageStore : IDisposable
}
// Returns up to `limit` tells exchanged with the named player, oldest-first.
// SQL narrows by Receiver + ChatType via the (Receiver, Date) index, then
// SQL narrows by Receiver + ChatType via the (Receiver, Date) index (migration
// 5; before that the ordering fell back to a temp b-tree over all rows), then
// the client-side loop runs PlayerPayload comparison and breaks once
// `limit` partner matches accumulate. Earlier versions had a hardcoded
// 500-row scan cap that cut less-frequent pinned partners off the back of