Skip to content

perf(sqlite): rebuild search_index family indexes as partial (schema v20) - #944

Merged
smunini merged 2 commits into
mainfrom
perf/partial-search-indexes
Sep 4, 2026
Merged

perf(sqlite): rebuild search_index family indexes as partial (schema v20)#944
smunini merged 2 commits into
mainfrom
perf/partial-search-indexes

Conversation

@angela-helios

Copy link
Copy Markdown
Contributor

Why

Write-path instrumentation on the bulk-import benchmark measured index-row INSERTs at 53% of total import time (0.40 ms/row): search_index carries 15 secondary indexes and every row updated all of them — including the family indexes for value columns that are NULL on that row (a token row still paid the date/number/quantity/string/uri B-trees). This is the configuration-free follow-up promised when #932 was closed, and the "review whether all ~15 secondary indexes earn their write cost" item from @edson's analysis (Lever 4).

What

The nine per-family indexes become partial (WHERE value_X IS NOT NULL; OR over system/code for token and identifier-type), in fresh creation and as a v19→v20 migration. Each row now maintains only its own family's structures. No settings, no user-facing behavior change, identical row counts.

Measured

10k inline benchmark (all parameters + FTS, worst case): 302 s → 181 s (1.67×), reproduced twice. Also shortens SQLite write-lock holds, mitigating the database is locked failure at high fan-out (#942).

Query-plan verification

  • Family predicates (value_date >= ?, value_token_code = ?) imply the partial predicates — EXPLAIN QUERY PLAN confirms they keep using their indexes.
  • :missing resolves by entry presence (a (tenant, type, param) prefix probe) served by idx_search_string_folded, which deliberately stays full-width for that role.
  • A dedicated presence index was tried and rejected with numbers: its highly-duplicated keys cost ~60 s of the 120 saved (242 s measured, reproduced), and string_folded already covers the probe.

Gate

…ly, WIP)

Every search_index row uses one value-column family for its parameter
type, but all 15 secondary indexes were updated per insert — NULL columns
included, so a token row still paid the date/number/quantity/string/uri
B-trees. Write-path instrumentation measured those inserts at 53% of
total import time (0.40ms/row).

The 9 family indexes become partial (WHERE value_X IS NOT NULL; OR over
system/code for token and identifier-type), so each row maintains only
the structures relevant to its type. Measured on the 10k inline benchmark
(all parameters + FTS): 302s -> 181s, 1.67x, reproduced twice, identical
index-row counts.

WIP: fresh-create path only. Still owed before a PR: the migration for
existing databases, EXPLAIN QUERY PLAN verification that search queries
imply the predicates (value comparisons do; audit :missing paths), and
the search suite + live battery as the gate.
…v20)

Every search_index row populates one value-column family for its
parameter type, but each INSERT maintained all fifteen secondary
indexes, NULL columns included. Write-path instrumentation measured
those inserts at 53% of bulk-import time (0.40ms/row).

The nine family indexes become partial (WHERE value_X IS NOT NULL;
OR over system/code for token and identifier-type), in fresh creation
and as a v19->v20 migration, so each row maintains only its own
family's structures. Measured on the 10k inline benchmark (all
parameters + FTS): 302s -> 181s, 1.67x, reproduced twice, identical
row counts. No configuration and no user-facing behavior change.

Search plans verified with EXPLAIN QUERY PLAN: family predicates
(value_date >= ?) imply the partial predicates and use their indexes;
:missing resolves by entry presence, a (tenant, type, param) prefix
probe that idx_search_string_folded serves — it deliberately stays
full-width for that role. A dedicated presence index was tried and
rejected: its highly-duplicated keys cost ~60s of the 120 saved
(242s measured, reproduced), and string_folded already covers the
probe.
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

2 participants