|
| 1 | +# Uitvoerplan — UPDATE/DELETE-achterstand (combined) |
| 2 | + |
| 3 | +**Datum:** 2026-09-03 |
| 4 | +**Bron:** eigen metingen/root-causes (sessie: PR #367–#370) + `PERFORMANCE_DEEP_DIVE.md` (Grok/xAI, second opinion). |
| 5 | +**Status:** actief. Werkwijze: meet eerst (D2), bouw daarna (A1/A2), valideer met median-of-runs + full suite + CI. |
| 6 | + |
| 7 | +## 0. Wat al klaar is (deze sessie) |
| 8 | +| PR | Wat | Effect | |
| 9 | +|---|---|---| |
| 10 | +| #367 | In-place tombstones (directe deletes) | DELETE duurzaam in O(delete), geen flush-rewrite | |
| 11 | +| #368 | **Commit-time tombstones** (transactionele/batch deletes) | **SQL DELETE 0,82 s → 0,24 s** (~12K → ~41-58K ops/s) — de grote sprong | |
| 12 | +| #369 | C4 (batch markers + evict-dedup) + B3 (structured delete, geen dubbele parse) | veilig; neutraal binnen ruis op benchmark | |
| 13 | +| #370 | B1 (key-only decode: alleen PK + hash-indexkolommen) | veilig; neutraal binnen ruis op small-row benchmark | |
| 14 | + |
| 15 | +**Root cause (niet in Grok-doc):** `ExecuteBatchSQL` draait elke batch in een storage-transactie; zonder #368 deed batch-DELETE nog steeds de #366 full-file compactie (~690 ms in `tableFlushLoop`). Daardoor waren eerdere “winst”-metingen niet-duurzaam/logisch-only. |
| 16 | + |
| 17 | +## 1. Baselines & doel |
| 18 | +| | SCDB Direct | SCDB SQL | SQLite | LiteDB | |
| 19 | +|---|---:|---:|---:|---:| |
| 20 | +| INSERT | ~130-187K | ~80-112K | ~130-150K | ~72K | |
| 21 | +| READ | ~127K | ~71K | ~95K | ~15K | |
| 22 | +| UPDATE | ~40-49K | ~34-40K | ~240-275K | ~10K | |
| 23 | +| DELETE | ~55-58K | ~41K | ~325-375K | ~14K | |
| 24 | + |
| 25 | +Cijfers variëren per machine/run (±10-20%). Verdict (beide analyses): niet verloren; **UPDATE/DELETE ~3x achter** is structureel maar aanpakbaar. INSERT/READ + analytics/vector/encryptie zijn al sterke punten. |
| 26 | + |
| 27 | +## 2. Bottlenecks (gecombineerd) |
| 28 | +1. AppendOnly versie-appends: UPDATE/DELETE = pread+pwrite per rij + indexonderhoud (SQLite: in-place leaf + WAL-batch). |
| 29 | +2. Batch-DELETE zat onterecht op flush-compactie → **opgelost (#368)**. |
| 30 | +3. Per-rij punt-I/O + B-tree/hash per operatie (C4/B3/B1 raken de rand, niet de kern — gemeten neutraal). |
| 31 | +4. Volledige (de)serialisatie op in-place paden met leidende variabele-lengte kolommen (deels geraakt door B1). |
| 32 | +5. Contiguïteit wordt alleen voor fixed-width benut (B8/B9); de benchmark-docs-tabel is variabele-lengte met fysiek oplopende posities. |
| 33 | +6. Grove `rwLock` + per-op index-locks. |
| 34 | +7. WAL/fsync-duurzaamheid (bevestigd met split-flush-meting: fsync-tail ~0,5-0,7 s op het oude pad; weg door #368). |
| 35 | +8. Managed/GC + Dictionary/boxing in non-Direct paden. |
| 36 | +9. AES-GCM per record (toggle: `NoEncryptMode`). |
| 37 | + |
| 38 | +## 3. Werkwijze per stap (vóór elke bouw: meten) |
| 39 | +- **D2-first:** profile UPDATE/DELETE hot paths (`DeleteMultipleKeys`/`UpdateMultiple`/`DeleteRecordsCore`/commit-tombstones) met `dotnet-trace` of env-gated fase-timers; bepaal of de tijd zit in resolutie (hash-lookups), per-rij `engine.Read`+decode, indexonderhoud (B-tree/hash), of de commit-markers. |
| 40 | +- Bouw alleen wat het profiel aanwijst. Elke stap: eigen branch → median-of-N benchmark → full suite + 4 CI-filter-suites → doc-update. |
| 41 | + |
| 42 | +## 4. Uitvoeringsfasen |
| 43 | +### Fase A — Quick wins (P0) |
| 44 | +- **A1:** contiguous variabele-lengte single-pass voor UPDATE/DELETE (prefix-walk over oplopende posities; 1 range-read, markers/patches in 1 schrijfpassage). Doel: docs-DELETE/UPDATE ~80-120K. Alleen na D2-bevestiging dat range-I/O de bottleneck is. |
| 45 | +- **A2:** in-place UPDATE waar de slot-lengte gelijk blijft (geen append/stale-versie) voor niet-fastPatch-gevallen. |
| 46 | +- **A3:** batched index re-point per index over de hele batch (deels aanwezig). |
| 47 | + |
| 48 | +### Fase B — Structuur (P1, kern ~3x-achterstand) |
| 49 | +- **B1:** `FixedWidthTable`/in-place page-engine volwassen (vaste offsets, slot-free-space, tombstone+vacuum). Doel: ~120-200K ops/s → ~80-110% van SQLite. |
| 50 | +- **B2:** PageBased als aanbevolen OLTP-engine + storage-engine selector (OLTP→PageBased+FixedWidth; analytics/eventsourcing→AppendOnly/Columnar). |
| 51 | +- **B3:** source-generated typed/ref-struct accessors (geen Dictionary op hot paths) + `PreparedCommand` met herbruikbare buffers. |
| 52 | +- **B4:** fijnmaziger locking (per-page/per-index) + optimistische concurrency. |
| 53 | + |
| 54 | +### Fase C — Platform (P2, .NET 11) |
| 55 | +- **C1:** Native AOT + `Span<T>` + Runtime Async; SIMD lane-API's/AVX-VNNI-512 voor index-lookups. |
| 56 | +- **C2:** optionele “SQLite-compat mode” (NoEncrypt + fixed-width default). |
| 57 | + |
| 58 | +### Fase D — Hygiëne & observability (doorlopend) |
| 59 | +- **D1:** median-of-N + warm-up in de comparative-harness. |
| 60 | + |
| 61 | +## 7. D2-bevindingen (env-gated fase-timers, 2026-09-03) |
| 62 | +DELETE 10K op de docs-tabel (comparative, Release) — attributie in de structured batch path: |
| 63 | + |
| 64 | +| Fase | SQL | Direct | |
| 65 | +|---|---:|---:| |
| 66 | +| per-rij read+decode (`engine.Read`+`DeserializeDeleteKeyRow`) | 62-82 ms | ~66 ms | |
| 67 | +| commit-tombstones (markers, pread+pwrite per rij) | ~50 ms | ~48-50 ms | |
| 68 | +| delete core (index-onderhoud, B-tree/hash) | 23-56 ms | ~32 ms | |
| 69 | +| hash-index lookup | 5-10 ms | ~5 ms | |
| 70 | + |
| 71 | +**Ontdekte bug (gefixed):** `TryScanCanonicalDml`'s DELETE-tak consumente nooit de whitespace/het `WHERE`-keyword na de tabelnaam → elke canonieke `DELETE ... WHERE col = literal` viel terug op de regex-path → `DeleteMultipleKeys` + B1/W1 (PR #369/#370) waren **dead code in de benchmark-harness**. Fix + diagnostische teller `Database.CanonicalDeleteStatementsParsed` + regressietest `CanonicalBatchDelete_EngagesStructuredPath` (deze PR). |
| 72 | + |
| 73 | +**Gevolg voor de attributie:** de per-rij read blijft nodig zolang de delete-core de auto-`rowid`-PK-waarde per rij moet wissen (gate-onderzoek: docs heeft PK-achtige index >1 geregistreerd). Grootste resterende hefbomen, nu met cijfers onderbouwd: |
| 74 | +1. **PK-onderhoud vervangen door één stale/lazy-rebuild** na een grote delete-batch (i.p.v. per-rij `Index.Delete`) → verwijdert ~30-50% van `core` én maakt de per-rij read overbodig (grootste winst op deze workload). |
| 75 | +2. **Marker-batching over een range-read** (commit-tombstones ~50 ms → enkele ms) als vervolg op C4. |
| 76 | +3. W1 (key-only, geen read) vuurt alleen bij een tabel met exact één geregistreerde index en zonder PK-tree — daar direct ~60-80 ms winst per 10K deletes. |
| 77 | + |
| 78 | +- **D2:** `dotnet-trace`/fase-timers op `UpdateAffectedRows`, `DeleteMultipleKeys`, `DeleteRecordsCore`, commit-tombstones. |
| 79 | +- **D3:** AOT/R2R expliciet als aparte meet-as (we meten nu managed JIT + tiered PGO). |
| 80 | +- **D4:** `docs/manual/performance.md` + dit document bijwerken na elke stap. |
| 81 | + |
| 82 | +## 5. Beslisboom (storage-engine) |
| 83 | +- Veel UPDATE/DELETE (OLTP) → **PageBased + FixedWidth** (in-place). |
| 84 | +- Veel appends/analytics/eventsourcing → **AppendOnly/Columnar** (blijft dominant). |
| 85 | +- Pure-throughput scenario’s → `NoEncryptMode`; encryptie blijft default-differentiator. |
| 86 | + |
| 87 | +## 6. Volgende acties |
| 88 | +1. D2-profiel op de huidige DELETE/UPDATE hot paths. |
| 89 | +2. A1/A2 implementeren op basis van het profiel (branch bovenop #370). |
| 90 | +3. Fase B (fixed-width in-place + PageBased default) als aparte roadmap-track. |
0 commit comments