Speed up large report deletions#925
Conversation
🤖 Claude Code ReviewPR: #925 I have completed a thorough review. Here is my assessment. Code Review: Speed up large report deletionsOverall this is a well-constructed, well-documented change. The core idea (chunk the Correctness (verified, no blockers)
UUIDv7 change (
|
7d2697b to
def06f4
Compare
|
Addressed the review findings in def06f4:
Minor items:
|
|
| Branch | claude/report-delete-perf |
| Testbed | intel-v1 |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result microseconds (µs) (Result Δ%) | Upper Boundary microseconds (µs) (Limit %) |
|---|---|---|---|
| Adapter::Json | 📈 view plot 🚷 view threshold | 4.71 µs(+1.91%)Baseline: 4.62 µs | 4.90 µs (96.09%) |
| Adapter::Magic (JSON) | 📈 view plot 🚷 view threshold | 4.57 µs(+1.62%)Baseline: 4.50 µs | 4.72 µs (96.81%) |
| Adapter::Magic (Rust) | 📈 view plot 🚷 view threshold | 25.90 µs(+1.41%)Baseline: 25.54 µs | 26.64 µs (97.23%) |
| Adapter::Rust | 📈 view plot 🚷 view threshold | 3.54 µs(+1.47%)Baseline: 3.49 µs | 3.60 µs (98.24%) |
| Adapter::RustBench | 📈 view plot 🚷 view threshold | 3.53 µs(+1.49%)Baseline: 3.48 µs | 3.59 µs (98.39%) |
|
Addressed the round 2 nit in the latest push: On the two acks requested: (1) the loss of delete atomicity and (2) the global UUIDv7 timestamp exposure are both deliberate, discussed tradeoffs (see the PR description and the doc comment on The |
def06f4 to
77629cd
Compare
77629cd to
e567423
Compare
e567423 to
19ed381
Compare
|
Took the round 4 optional polish in 19ed381: the startup PRAGMA log now includes the resolved |
|
Sign-off on concern #1, confirmed with the maintainer before implementation: none of the typed UUIDs are treated as capability secrets. On observation #2: agreed it is a fragility note; the doc comment on No further changes planned; awaiting the remaining CI jobs. |
19ed381 to
4aa6969
Compare
Deleting an incident-scale report (hundreds of thousands of cascade rows) held the single SQLite writer connection for tens of seconds, blocking all other writes and generating a large WAL burst for Litestream to replicate. Validated against a copy of the production database: - Delete report results in bounded chunks (1024 report_benchmark rows per write statement) so the writer lock is released between chunks. Max writer stall drops from tens of seconds to about a second; other writers interleave. - Mint time-ordered UUIDv7 instead of UUIDv4 for typed uuids. Random v4 keys scatter one dirty page per row across the uuid indexes on bulk insert and delete (the large majority of the WAL churn); v7 clusters same-batch rows. - Add a configurable SQLite page cache size for the writer connection (default 64 MiB, was the 2 MiB SQLite default) to avoid re-reading evicted pages during large deletes and ingests. - Drop two redundant report_benchmark indexes: index_report_benchmark_benchmark is a strict prefix of index_report_benchmark_benchmark_report, and index_report_benchmark is covered by the UNIQUE(report_id, iteration, benchmark_id) autoindex. Verified query plans are unaffected. - Move the otel ReportDelete counter before the early return so all report deletes are counted, not just those that drop a dangling version.
4aa6969 to
ea23855
Compare
|
Replaces an earlier comment, redacted per repository disclosure policy. Round 3 recommendation: the UUIDv7 timestamp tradeoff is documented in the PR description (the changelog keeps to user-facing behavior only, per maintainer preference). On the |
Problem
Deleting an incident-scale report (hundreds of thousands of cascade rows) holds the single SQLite writer connection for 30+ seconds, blocking all other writes, and generates a WAL burst that Litestream must replicate (the iowait churn during the incident).
Analysis against a copy of the production database showed that every FK cascade level already uses an indexed seek (verified via
EXPLAINbytecode), so no index additions can help. The cost is raw B-tree write volume, with ~80% of the WAL churn coming from UUIDv4 index page scatter: nearly every deleted row dirties its own 4 KiB page in the uuid unique indexes.Changes
DELETE /v0/projects/{project}/reports/{report}now deletesreport_benchmarkrows in bounded chunks (1024 rows per write statement, cascading to metric, boundary, and alert), releasing the writer lock between chunks so other writers interleave. The final report row delete cascades any stragglers plus the job and rollup rows.typed_uuid!now mints time-ordered UUIDv7 instead of random UUIDv4, clustering same-batch rows in the uuid indexes for both inserts and deletes. Stored as hyphenated TEXT, v7 sorts chronologically; mixed v4/v7 rows coexist fine in the UNIQUE indexes, no backfill needed. None of the typed uuids are used as unguessable capabilities, so the reduced entropy (74 vs 122 random bits) and embedded timestamp are acceptable.cache_sizeconfig ondatabase(KiB, default 64 MiB, previously the 2 MiB SQLite default), applied to the single writer connection only.index_report_benchmark_benchmark(strict prefix ofindex_report_benchmark_benchmark_report) andindex_report_benchmark(covered by theUNIQUE(report_id, iteration, benchmark_id)autoindex). Query plans for the report results load, the detector query, and report+benchmark lookups verified unaffected.ReportDeletecounter sat after the early return for shared versions, so it only counted deletes that also dropped a dangling version; it now increments right after the report row delete.Validation
Measured against a copy of the production database with a synthesized incident-scale report, replaying the exact statement shape the new code produces:
Total WAL roughly doubles because scattered uuid index pages are re-dirtied across chunk boundaries, but it is spread over a minute instead of one burst. The UUIDv7 change collapses that scatter for new rows going forward.
New integration tests cover report deletion with results (below one chunk, and exactly 2x the chunk size to exercise multiple chunks and the exact-multiple boundary condition).