Skip to content

Commit a25ced0

Browse files
docs(plan): DELETE has no cheap win left - the lever is deferred index maintenance (a decision, not a fix)
Continued item 1 by inspecting the two dominant DELETE buckets the split exposed, and the inspection removed both candidate fixes: - The classification path (parse 19.3%, 1.4us/statement) is already lean: IsInsertStatement is a span prefix check, TryScanCanonicalDml is a quotes-aware span scan with NO regex, and both TryParseUpdateForBatch and TryParseDeleteForBatch check their UPDATE/DELETE prefix BEFORE the regex fallback. The 1.4us is two canonical scans plus list bookkeeping plus the profiler's own two Interlocked calls - there is no allocation or regex to remove, so a "prepared DELETE statement" path would buy far less than the share suggests. - index-maintenance (58.6%, 42.2 ms in SEVEN calls) is not the hash index either: RemoveBatchKeys already defers duplicate-key removals and compacts in one pass, and BTree.DeleteBulk already sorts descending so consecutive deletes ride the rightmost leaf chain. The cost is most plausibly the PK B-tree bulk delete itself (10K individual deletes with separator promotions/rebalances), which is inherent to per-key deletion. So the real lever is to stop doing index maintenance per key: write the tombstone and let the existing tombstone-aware readers (ReadBytesFrom returns null on a negative prefix, ReadAllRecords skips the slot) and a rebuild/compaction drop the stale entries - the same "defer the maintenance" shape the update path already uses (DeferredIndexUpdater). That could remove most of the 58.6%, but it changes read-path behaviour for EVERY index consumer (stale entries must be tolerated in the whole-file slice fast paths too) and needs a bounded rebuild trigger. Deliberately not implemented here: it is a decision plus its own test matrix, not a micro-fix - which is exactly what the plan's culture asks for. No product code changed: measurement and documentation only. Gate unchanged (build 0 errors; suite 1857 / 0 failed / 16 skipped, with the one HashIndexPerformanceTests timing flake recorded).
1 parent 66540a2 commit a25ced0

1 file changed

Lines changed: 21 additions & 0 deletions

File tree

‎docs/performance/INSERT_UPDATE_PERFORMANCE_PLAN.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,27 @@ Stage shares (at-rest): `index-maintenance` **58.6%** (42.2 ms, only **7 calls**
11171117
UPDATE/DELETE phases;
11181118
- the tombstone (1.2%) is confirmed as noise for the third time.
11191119

1120+
**Verdict (2026-09-14): the two biggest DELETE costs contain no cheap win — the next step is a design
1121+
decision.** Both were inspected after the split:
1122+
- the **classification path is already lean**: `IsInsertStatement` is a span prefix check, `TryScanCanonicalDml`
1123+
is a quotes-aware span scan with no regex, and both `TryParseUpdateForBatch` and `TryParseDeleteForBatch` run
1124+
their cheap `UPDATE`/`DELETE` prefix check *before* the regex fallback. The measured 1.4 µs/statement is two
1125+
canonical scans plus per-table list bookkeeping plus the profiler's own two `Interlocked` calls — there is no
1126+
allocation or regex to delete. So a "prepared DELETE statement" path would buy far less than the 19–45% share
1127+
suggests, and is not the lever.
1128+
- `HashIndex.RemoveBatchKeys` already defers duplicate-key removals and compacts in a single pass, and
1129+
`BTree.DeleteBulk` already sorts descending so consecutive deletes ride the rightmost leaf chain. The 42.2 ms
1130+
in **seven** calls is therefore most plausibly the **PK B-tree bulk delete itself** (10K individual deletes
1131+
with separator promotions/rebalances) — inherent to per-key deletion, not a fixable inefficiency.
1132+
1133+
**The lever is to stop doing it per key**: write the tombstone and SKIP index maintenance, relying on machinery
1134+
that already exists — readers treat the negative length prefix as a deleted record (`ReadBytesFrom` returns
1135+
null, `ReadAllRecords` skips the slot) and a reopen/compaction rebuild drops the stale entries. That is the same
1136+
"defer the maintenance" shape the update path already uses (`DeferredIndexUpdater`), and it could remove most of
1137+
the 58.6%. It is deliberately NOT implemented here: it changes read-path behaviour for every index consumer
1138+
(stale entries must be tolerated everywhere, including the whole-file slice fast paths) and the accumulation
1139+
must be bounded by a rebuild trigger — that is a decision plus its own test matrix, not a micro-fix.
1140+
11201141
**Instrumentation coverage (2026-09-14, §2).** Covered now: `Table.InsertBatch` (Validate — validation *and*
11211142
serialization — plus RowLocate around the batch PK probes; the path had none), the fixed-width bulk-delete fast
11221143
path (RowLocate / IndexMaintenance / IndexDecode / EngineWrite), bulk-update per-row hash-index maintenance

0 commit comments

Comments
 (0)