Skip to content

perf: key-only DELETE row decode (B1) - #370

Merged
MPCoreDeveloper merged 4 commits into
masterfrom
perf/keyonly-delete
Sep 3, 2026
Merged

perf: key-only DELETE row decode (B1)#370
MPCoreDeveloper merged 4 commits into
masterfrom
perf/keyonly-delete

Conversation

@MPCoreDeveloper

Copy link
Copy Markdown
Owner

Stack

Inhoud

B1 (d37fad51) — key-only DELETE row-decode: de structured batch-DELETE-path (DeleteMultipleKeys) bouwt via BuildDeleteKeyColumns de benodigde kolomindexen (PK + alle geladen hash-indexkolommen) en decodeert met DeserializeDeleteKeyRow alleen díe kolommen uit het legacy variabele-lengte record (payload-parsing van overige kolommen wordt overgeslagen, minimal 1-3-entry dictionary i.p.v. full row). DeleteRecordsCore doet daarna dezelfde PK/hash-lookups op die subset. Fixed-width layouts en corrupte rijen vallen terug op de volledige DeserializeRowFromSpan.

Meting (Release, comparative DELETE 10K op ~100K rijen, median van 3)

#369 (C4+B3) B1
SQL DELETE ~42K ops/s ~39K ops/s (39,2 / 38,5 / 44,7)
Direct DELETE ~57K ops/s ~57K ops/s (50,0 / 56,5 / 58,2)

Neutraal binnen run-ruis op deze small-row workload — de decode was niet de bottleneck (kleine rijen, gepoolde full-row dict). De winst zit in workloads met brede rijen/lange on-geïndexeerde TEXT-payloads (minder UTF8-decode, boxing en dict-writes). Veilig: full suite + 4 CI-filter-suites EXIT=0.

Conclusie van de meetreeks

De grote sprong zat in #368 (commit-time tombstones: SQL DELETE 0,82s → 0,24s). C4/B3/B1 zijn robuustheids-/allocatiewinsten die op deze benchmark binnen de ruis vallen. De volgende structurele hefboom is A1 (contiguous variabele-lengte single-pass UPDATE/DELETE — de docs-tabel heeft fysiek oplopende posities bij oplopende keys, maar alleen fixed-width gebruikt nu de één-range-read) en daarna bulk-B-tree/index-onderhoud.

MPCoreDeveloper added 4 commits September 3, 2026 20:18
… flush rewrite)

Stap-0 profiling showed batch DELETE (ExecuteBatchSQL wraps every batch in a storage transaction) was NOT using the tombstone path: the rollback-safe deferral only counted _pendingLogicalDeletes, so db.Flush() still ran the #366 full-file CompactPendingDeletes rewrite (~0.5-0.7s; DELETE stuck at ~12-16K ops/s).

- IStorage.BufferTombstoneForCommit (default no-op) + Storage implementation: buffered offsets per file, applied as in-place negative-prefix markers by ApplyBufferedTombstones() inside FlushBufferedAppendsAndOverwrites() (the commit path) AFTER buffered appends are on disk; rollback discards the buffer via ClearBufferedAppends().

- DeleteRecordsCore + the contiguous FW bulk path now buffer the deleted offsets when IsInTransaction instead of incrementing _pendingLogicalDeletes, so the flush-time full-file rewrite is off the batch-DELETE path entirely.

- Regression: batch DELETE survives reopen even WITHOUT an explicit Flush after ExecuteBatchSQL (commit already tombstoned the rows).

Measured (same machine, Release, comparative harness DELETE 10K of ~100K rows): SQL 0.82s/12K -> 0.24s/41K ops/s, Direct 0.63s/16K -> 0.17s/58K ops/s; --pk legacy 0.16s/62K, fixed-width 0.13s/78K ops/s. Full suite EXIT=0.
- IStorage.TombstoneRecords (default loops TombstoneRecord); Storage batches the in-place negative-prefix markers over one cached read handle and evicts each affected page-cache page once instead of once per row.

- ApplyBufferedTombstones (commit path) and Table.TombstoneDeletedPositions (direct non-transactional deletes) route through the batch API; offsets that are not physical records (EOF/already-marked) are skipped safely.
…parse (B3)

- TryParseDeleteForBatch now also returns the canonical WHERE column + raw literal; ExecuteBatchSQL groups (where, column, literal) instead of plain WHERE strings.

- Table.DeleteMultipleKeys mirrors DeleteMultiple (contiguous-FW gate -> PK fast path -> hash fast path -> generic fallback) but resolves keys directly from the pre-parsed column/literal; the 'col = literal' string and the second TryParseSimpleWhereClause pass are built only when a generic fallback actually runs.

- Non-canonical statements keep the string path (mixed tables rebuild on the rare path).
…index columns

The structured batch DELETE path (DeleteMultipleKeys) now decodes a minimal row: BuildDeleteKeyColumns computes the needed columns (PK + every loaded hash-index column), and DeserializeDeleteKeyRow walks the legacy variable-length record skipping the unneeded columns' payload parsing entirely, building a 1-3 entry dictionary instead of a full row. DeleteRecordsCore performs the identical PK/hash lookups on that subset. Fixed-width layouts and corrupt rows fall back to the full DeserializeRowFromSpan.

Measured (Release, comparative harness DELETE 10K of ~100K rows, median of 3): SQL ~39-42K and Direct ~57K ops/s — neutral within run noise on this small-row workload; the win shows on wider rows / long unindexed TEXT payloads where payload parsing and boxing are skipped. Full suite + 4 CI-filter suites EXIT=0.
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

@MPCoreDeveloper
MPCoreDeveloper changed the base branch from perf/b3c4-dml to master September 3, 2026 19:35
@MPCoreDeveloper
MPCoreDeveloper merged commit 5e9594a into master Sep 3, 2026
2 checks passed
@MPCoreDeveloper
MPCoreDeveloper deleted the perf/keyonly-delete branch September 3, 2026 19:41
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