Skip to content

Fix full table scans in reminder storage overview queries#92

Merged
Aaronontheweb merged 1 commit into
devfrom
fix/replace-full-table-scans-with-aggregate-queries
Mar 9, 2026
Merged

Fix full table scans in reminder storage overview queries#92
Aaronontheweb merged 1 commit into
devfrom
fix/replace-full-table-scans-with-aggregate-queries

Conversation

@Aaronontheweb
Copy link
Copy Markdown
Owner

Summary

  • Removed full table scans from GetNextRemindersAsync and GetRemindersOverviewAsync across all three SQL providers (SqlServer, PostgreSQL, SQLite)
  • Replaced GetOverviewSql (which returned all columns for all rows) with two efficient aggregate queries:
    • GetOverviewAggregateSql: SELECT COUNT(*), MIN(when_utc) WHERE is_completed = 0
    • GetNextReminderTimeSql: SELECT when_utc ... ORDER BY when_utc OFFSET @Skip LIMIT 1
  • Removed unused GetRemindersOverviewAsync call inside GetNextRemindersAsync (result was discarded)
  • Added XML doc comments to IReminderStorage.GetRemindersOverviewAsync clarifying it is for diagnostics/testing/monitoring only and must not be called from the scheduling hot path

Context

GetNextRemindersAsync had two performance bugs introduced during the provider package split (0ce1026):

  1. Called GetRemindersOverviewAsync and stored result in an unused variable — loading all rows and deserializing all payloads for nothing.
  2. Opened a second connection loading ALL rows (including completed ones, filtering in C#) into memory, just to compute COUNT and MIN(WhenUtc) of remaining reminders.

GetRemindersOverviewAsync itself had the same issue — loading every row to compute two scalar values.

Structural safeguard

GetOverviewSql has been removed from the ISqlDialect interface entirely. The only overview-related queries now return aggregate scalars, making it structurally impossible to accidentally reintroduce a full table scan through the overview path.

Test plan

  • All 28 InMemory storage tests pass (validates correctness of overview counts, TimeUntilNext, batch size limits)
  • CI validates SqlServer/PostgreSQL/SQLite against real databases

…wAsync

GetNextRemindersAsync had two performance bugs introduced during the
provider package split (0ce1026):

1. Called GetRemindersOverviewAsync and discarded the result, loading
   all rows and deserializing all payloads for nothing.
2. Opened a second connection to load ALL rows (including completed)
   into memory, just to compute COUNT and MIN(WhenUtc) of remaining
   reminders.

GetRemindersOverviewAsync itself had the same issue — loading all rows
to compute two scalar values.

Replace GetOverviewSql (SELECT * FROM table) with:
- GetOverviewAggregateSql: SELECT COUNT(*), MIN(when_utc)
- GetNextReminderTimeSql: SELECT when_utc ... OFFSET @Skip LIMIT 1

This eliminates all full table scans and payload deserialization from
the overview/summary code paths across all three SQL providers
(SqlServer, PostgreSQL, SQLite).

Also adds XML doc comments to IReminderStorage.GetRemindersOverviewAsync
clarifying it is for diagnostics/testing/monitoring only and must not
be called from the scheduling hot path.
@Aaronontheweb Aaronontheweb merged commit dce5515 into dev Mar 9, 2026
1 check passed
@Aaronontheweb Aaronontheweb deleted the fix/replace-full-table-scans-with-aggregate-queries branch March 9, 2026 18:24
@Aaronontheweb Aaronontheweb mentioned this pull request Mar 9, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant