Skip to content

libstore: drop redundant IndexReferrer index on Refs table#15439

Open
randomizedcoder wants to merge 2 commits intoNixOS:masterfrom
randomizedcoder:drop-redundant-indexreferrer
Open

libstore: drop redundant IndexReferrer index on Refs table#15439
randomizedcoder wants to merge 2 commits intoNixOS:masterfrom
randomizedcoder:drop-redundant-indexreferrer

Conversation

@randomizedcoder
Copy link
Contributor

Motivation

Drop the redundant IndexReferrer index on Refs(referrer) to reduce database size and improve write performance.

The IndexReferrer index is fully redundant with sqlite_autoindex_Refs_1 on Refs(referrer, reference) — SQLite's leftmost-prefix optimization means the composite primary key index already serves all single-column lookups on the referrer column. EXPLAIN QUERY PLAN confirms identical plans with and without the index for all 6 Refs-related queries. The query planner never selects IndexReferrer.

The practical benefits are modest:

  • ~10 MB freed on a 131 MB database (~70k Refs entries)
  • ~3-4% write-path improvement at scale (one fewer B-tree to maintain per INSERT/DELETE), measured via Google Benchmark at N=100k
  • No read-path impact (confirmed by both EXPLAIN QUERY PLAN and benchmarks)

Context

EXPLAIN QUERY PLAN output is identical with and without the index:

-- QueryReferences: select path from Refs join ValidPaths on reference = id where referrer = ?
QUERY PLAN
|--SEARCH Refs USING COVERING INDEX sqlite_autoindex_Refs_1 (referrer=?)
`--SEARCH ValidPaths USING INTEGER PRIMARY KEY (rowid=?)

-- QueryReferrers: select path from Refs join ValidPaths on referrer = id where reference = (subquery)
QUERY PLAN
|--SEARCH Refs USING INDEX IndexReference (reference=?)
|--SCALAR SUBQUERY 1
|  `--SEARCH ValidPaths USING COVERING INDEX sqlite_autoindex_ValidPaths_1 (path=?)
`--SEARCH ValidPaths USING INTEGER PRIMARY KEY (rowid=?)

Note that sqlite_autoindex_Refs_1 is used as a covering index for referrer=? lookups — it contains all the columns needed to satisfy the query without touching the table, making the separate IndexReferrer strictly redundant.

Changes

File Change
src/libstore/schema.sql Remove IndexReferrer from schema
src/libstore/local-store.cc Add migration "20260309-drop-redundant-indexreferrer" to drop the index on existing databases
packaging/dev-shell.nix Add sqlite-interactive to dev shell for EXPLAIN QUERY PLAN analysis
src/libstore-tests/refs-index-bench.cc New benchmark for A/B comparison of Refs operations with/without the index
src/libstore-tests/meson.build Add benchmark to build

Schema migration note

This introduces a schema migration that drops the index on existing databases at next open. This is my first time contributing a Nix database schema change, so maintainer review of the migration approach is appreciated — the change itself is low-risk (dropping an unused index is a no-op for correctness) but I want to make sure it follows project conventions.

Benchmark results (N=100k, 3 repetitions)

Write path (CPU time, mean):

  • WithIndex: 2356 ms (42.5k items/s)
  • WithoutIndex: 2272 ms (44.0k items/s)
  • 3.6% faster without the index

Read path (CPU time, mean):

  • WithIndex: 2228 ms (89.8k items/s)
  • WithoutIndex: 2647 ms (75.6k items/s)
  • Apparent regression is not real — EXPLAIN QUERY PLAN proves identical plans; difference is due to benchmark ordering and system cache effects

Mixed path (10:2 write:read ratio, CPU time, mean):

  • WithIndex: 3288 ms (30.4k items/s)
  • WithoutIndex: 3325 ms (30.1k items/s)
  • Within noise (~1%)

Verified on a live system

  • All 6 EXPLAIN QUERY PLAN outputs identical with/without the index
  • nix path-info, nix-store -q --references/--referrers, nix-store --verify all work correctly
  • Full test suites pass (641 store tests, 200 functional tests)

Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

The IndexReferrer index on Refs(referrer) is fully redundant with
sqlite_autoindex_Refs_1 on Refs(referrer, reference) — SQLite's
leftmost-prefix optimization means the composite primary key index
already serves all single-column lookups on the referrer column.

EXPLAIN QUERY PLAN confirms identical plans with and without the index
for all 6 Refs-related queries (QueryReferences, QueryReferrers,
DeleteSelfRefs trigger, etc.). The query planner never selects
IndexReferrer.

The practical benefits are modest:
- ~10 MB freed on a 131 MB database (~70k Refs entries)
- ~3-4% write-path improvement at scale (one fewer B-tree to maintain
  per INSERT/DELETE), measured via Google Benchmark at N=100k
- No read-path impact (confirmed by both EXPLAIN QUERY PLAN and
  benchmarks)

This introduces a schema migration ("20260309-drop-redundant-indexreferrer")
that drops the index on existing databases at next open. This is my first
time contributing a Nix database schema change, so maintainer review of
the migration approach is appreciated — the change itself is low-risk
(dropping an unused index is a no-op for correctness) but I want to make
sure it follows project conventions.

Also adds sqlite-interactive to the dev shell for convenient EXPLAIN
QUERY PLAN analysis, and a new refs-index-bench.cc benchmark for A/B
comparison of Refs table operations with and without the index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added the store Issues and pull requests concerning the Nix store label Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

store Issues and pull requests concerning the Nix store

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant