feat(db): maintain statistics rollups of query_logs - #280
Merged
Merged
Conversation
Every dashboard and Statistics reading is a count, sum or histogram over a window. With the default retention that window is the whole table, so answering it from query_logs reads an index entry per logged query whichever index it uses. Migration 16 adds five rollup tables in which one row stands for every query sharing a key within a unit of time: query_stats_quarter, and per hour domain, client, upstream and metrics. Nothing reads them yet; the dashboard and the Statistics page move onto them in follow-up changes. - Inserts are maintained by an AFTER INSERT trigger, so rows written outside the logger (the e2e fixtures use the sqlite3 CLI) are counted too. Replaying 1.48 M queries in 500-row batches, the trigger writes 886 480 pages, against 885 858 for one grouped upsert per batch and 846 079 with no rollups. - prune_logs_before unwinds the rollups in its transaction: whole units before the cutoff are dropped, and the rows before the cutoff in the quarter and hour it falls inside are recounted and subtracted, so the prune keeps its exact cutoff. A one-day prune writes 5 151 pages against 5 020. - Clear All empties the rollups. - The migration fills them from existing rows (101 420 pages read, 3 714 written on the 1.48 M-row database) with a replacing upsert, so an interrupted migration can run again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #280 +/- ##
==========================================
+ Coverage 91.16% 91.30% +0.13%
==========================================
Files 31 31
Lines 11400 11587 +187
==========================================
+ Hits 10393 10579 +186
- Misses 1007 1008 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every dashboard and Statistics reading is a count, a sum or a histogram over a time window. With the default seven-day retention that window is the whole table, so answering one from
query_logsreads an index entry per logged query, whichever index it uses. On a 1.48 M-row database the dashboard's summary alone is 9 475 pages, every tick.This PR adds the data structure that fixes that, and keeps it correct. Nothing reads it yet. The dashboard and the Statistics page move onto it in follow-up PRs, and the indexes that only they use are dropped after that.
WITHOUT ROWIDrollup tables. In each, one row stands for every query that shares a key within a unit of time:query_stats_quarter: blocked and cached, with count and summed response time per quarter hour.query_stats_domain_hour,query_stats_client_hour,query_stats_upstream_hour,query_stats_metrics_hour: the grain the outcome, query-type and latency folds read.AFTER INSERTtrigger,query_logs_maintain_stats, rather than by the logger, so rows written any other way are counted too. The e2e fixtures write theirs with thesqlite3CLI.prune_logs_before: callsunwind_stats_rollupsin its transaction. Whole units before the cutoff are dropped. The quarter and hour the cutoff falls inside have the rows before the cutoff recounted and subtracted, so the prune keeps its exact cutoff instead of rounding retention to the hour.DELETEtrigger, which would unwind a prune row by row and turn off SQLite's truncate optimisation.Measurements
All numbers are cold page counts. The rollups were replayed through the
sqlite3CLI with the same trigger and unwind statements, over the real 370 677-row week and a 1 482 708-row, 31-day database built by replaying it, in the logger's 500-row batches.On both databases, the trigger and the per-batch upsert produced identical contents in all five tables (row-for-row hash match).
On the 1 482 708-row database:
Tests
rollups_follow_every_write_that_changes_query_logs: compares every rollup with its recount fromquery_logsafter each of these steps:Before the implementation it failed with
no such table: query_stats_quarter. Withunwind_stats_rollupsremoved from the prune it fails atquery_stats_quarter disagrees with query_logs after a prune inside a quarter and an hour.migration_v16_backfills_the_rollups: winds a populated database back to version 15, reopens it, and checks the fill, then checks that an insert afterwards is still counted. Before the implementation it failed withno such trigger: query_logs_maintain_stats.Test plan
cargo fmt --checkcargo clippy --all-targets -- -D warningscargo nextest run: 683 passed, 8 skipped🤖 Generated with Claude Code