Skip to content

perf: skip fragment-reuse index for compactions with no indexed data#7774

Merged
jackye1995 merged 1 commit into
lance-format:mainfrom
xuanyu-z:fix/skip-fri-for-unindexed-rewrites
Jul 14, 2026
Merged

perf: skip fragment-reuse index for compactions with no indexed data#7774
jackye1995 merged 1 commit into
lance-format:mainfrom
xuanyu-z:fix/skip-fri-for-unindexed-rewrites

Conversation

@xuanyu-z

@xuanyu-z xuanyu-z commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Motivation

A deferred-remap compaction (defer_index_remap = true) records a fragment-reuse index (FRI) version for every rewrite group, even when a compaction rewrites only fragments that no index covers. Such a version carries no useful remap — no index's fragment_bitmap overlaps the group's old fragments — but it isn't free: cleanup_frag_reuse_index can't trim it while a lagging index's dataset_version sits below it, and every read pays the load-time auto-remap cost for it. On a table whose churn is dominated by not-yet-indexed data, these no-op versions accumulate (bounded only by the eventual index rebuild that advances the index version), inflating FRI metadata and per-read overhead.

This is not a correctness issue — queries stay correct throughout, since scalar indexes apply the FRI at read time. It only affects metadata size and read overhead.

Change

Decide per compaction, all-or-nothing: write the FRI only when some rewrite group's old fragments are covered by

  • a data index directly, or
  • the existing FRI's new fragments (a not-yet-caught-up index reaches those through the composed remap chain).

Otherwise skip the FRI version entirely.

A remap advances the index's dataset_version to the current version on commit, so indexed-fragment FRIs still drain via remap with no rebuild; only compactions touching no indexed data change behavior (they now write no FRI).

Why not a per-group filter

A partial FRI (recording only the indexed groups) is unsound: a concurrent reindex can turn a fragment that was unindexed at the compaction's read snapshot into an indexed one, and the commit conflict resolver's FRI-present path does not re-check the skipped groups — which would leave the reindexed index referencing rewritten-away fragments. Skipping the FRI entirely produces the same no-FRI state a non-deferred (inline) compaction already produces, whose conflict-resolver path forces a retryable conflict if a concurrent reindex appears.

Also

  • Fixes a pre-existing off-by-one in load_index_fragmaps: max_fragment_id is inclusive, so the fragment_bitmap == None fallback (whose coverage this decision reads) now covers the highest fragment (0..max0..=max).
  • Logs the skip at debug! so operators can confirm the fix is taking effect without inspecting index metadata.

Tests

  • test_defer_index_remap_skips_fri_when_no_indexed_data — no data index → no FRI.
  • test_defer_index_remap_mixed_records_all_groups — a mixed compaction records the unindexed group too (all-or-nothing; a per-group filter would drop it).
  • test_defer_index_remap_multiple_compactions — chained FRI across successive deferred compactions.
  • Existing concurrent-compaction / cleanup-rebase tests updated to build a data index (an FRI now requires one to be meaningful) and to catch the index up before cleanup so the trim behaves as intended.

All dataset::optimize (103) and dataset::index::frag_reuse (4) tests pass; fmt + clippy clean.

Follow-up

commit_compaction loads index metadata twice (load_index_fragmaps + load_index_by_name); this could load once by threading pre-loaded indices through the helper (also touches the binning call site).

Summary by CodeRabbit

  • Bug Fixes

    • Improved deferred compaction so fragment-reuse metadata is created only when compaction touches indexed fragments, including during deferred index remapping.
    • Prevented fragment-reuse metadata creation for deferred compactions that only affect non-indexed data.
    • Fixed index fragment coverage loading to derive an empty coverage range when fragment bitmap info is missing and no max fragment id is available.
  • Tests

    • Expanded deferred-compaction and concurrent cleanup/rebase regression coverage with indexed compaction-column setup.
    • Added a regression case asserting deferred compaction merges fragments without creating the fragment-reuse index when no indices are touched.

@github-actions github-actions Bot added the bug Something isn't working label Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Deferred compaction now detects indexed and FRI-covered fragments before writing the fragment-reuse index. Tests cover indexed, unindexed, mixed, repeated, and concurrent compaction scenarios.

Changes

Deferred FRI gating

Layer / File(s) Summary
Indexed-fragment detection and FRI gating
rust/lance/src/dataset/optimize.rs
commit_compaction combines dataset index and existing FRI coverage, tracks indexed rewrite groups, and skips FRI creation when no indexed data is touched. Missing fragment bitmaps use max_fragment_id as an inclusive bound.
Deferred remap regression coverage
rust/lance/src/dataset/optimize.rs
Deferred-remap tests add scalar indexes where needed and validate FRI creation, omission, mixed coverage, repeated remapping, and compaction grouping.
Concurrent compaction and cleanup scenarios
rust/lance/src/dataset/optimize.rs
Concurrency tests update index creation, remapping, cleanup, and version behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant commit_compaction
  participant DatasetIndices
  participant FragReuseIndex
  commit_compaction->>DatasetIndices: collect indexed fragment coverage
  commit_compaction->>FragReuseIndex: include existing FRI coverage
  commit_compaction->>commit_compaction: detect indexed rewrite groups
  commit_compaction->>FragReuseIndex: build FRI only when indexed groups exist
Loading

Possibly related PRs

  • lance-format/lance#7773: Both changes adjust compaction behavior when no indexed or remappable fragments are present.

Suggested reviewers: ddupg

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: skipping fragment-reuse index creation for compactions without indexed data.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@xuanyu-z xuanyu-z force-pushed the fix/skip-fri-for-unindexed-rewrites branch from 0d63b27 to 607c2af Compare July 14, 2026 06:44
@xuanyu-z xuanyu-z changed the title fix: skip fragment-reuse index for compactions with no indexed data perf: skip fragment-reuse index for compactions with no indexed data Jul 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 2163-2166: Add a routine debug log to the FRI skip branch
controlled by defer_index_remap and any_group_indexed, indicating that FRI
creation was skipped because no rewrite group touched indexed or chain data.
Keep the existing frag_reuse_index behavior unchanged and avoid warning-level
logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c5962f53-10b1-4e4f-bbae-1f9ede2c5fdb

📥 Commits

Reviewing files that changed from the base of the PR and between 2887837 and 0d63b27.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs

Comment thread rust/lance/src/dataset/optimize.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 2033-2048: The legacy index coverage expansion in
load_index_fragmaps must include the manifest’s inclusive max_fragment_id rather
than stopping before it. Update that range construction so fragment_bitmap ==
None covers the highest fragment as well, preserving the existing coverage union
in the indexed_frags calculation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 412f2bf5-c3ab-473d-b5a3-800f8fb23ee5

📥 Commits

Reviewing files that changed from the base of the PR and between 0d63b27 and 607c2af.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs

Comment thread rust/lance/src/dataset/optimize.rs
@xuanyu-z xuanyu-z force-pushed the fix/skip-fri-for-unindexed-rewrites branch from 607c2af to 97f45f7 Compare July 14, 2026 07:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/optimize.rs (1)

2152-2157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Still no log on the FRI-skip path.

A prior review on this PR suggested a debug! log when defer_index_remap is set but no group was indexed, so operators can confirm the fix is taking effect without inspecting index metadata. That log is still absent here.

♻️ Proposed addition
     let frag_reuse_index = if options.defer_index_remap && any_group_indexed {
         Some(build_new_frag_reuse_index(dataset, frag_reuse_groups, new_fragment_bitmap).await?)
     } else {
+        if options.defer_index_remap {
+            debug!(
+                "skipping fragment-reuse index: no rewritten fragments were covered by an index"
+            );
+        }
         None
     };

As per coding guidelines, "Log warnings for silent no-op skipped operations, but omit warnings before errors because the error message is sufficient" and "use debug! for routine/high-frequency operations."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/optimize.rs` around lines 2152 - 2157, The FRI-skip
branch in the frag_reuse_index construction lacks the requested diagnostic log.
Update the conditional around defer_index_remap and any_group_indexed to emit a
debug! message when index remapping is deferred but no group was indexed, while
preserving the existing None result and avoiding logs on error paths.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 2152-2157: The FRI-skip branch in the frag_reuse_index
construction lacks the requested diagnostic log. Update the conditional around
defer_index_remap and any_group_indexed to emit a debug! message when index
remapping is deferred but no group was indexed, while preserving the existing
None result and avoiding logs on error paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 34db534c-1a2c-43d5-9b93-dcade993f22d

📥 Commits

Reviewing files that changed from the base of the PR and between 607c2af and 97f45f7.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs

@xuanyu-z xuanyu-z force-pushed the fix/skip-fri-for-unindexed-rewrites branch 2 times, most recently from b263afc to 44a44c7 Compare July 14, 2026 07:23
A deferred-remap compaction records a fragment-reuse index (FRI) version for every rewrite group, even when a compaction rewrites only fragments no index covers. Such a version carries no useful remap -- no index's fragment bitmap overlaps the group's old fragments -- but it still accumulates: the cleanup trim cannot drop it while a lagging index's dataset_version sits below it, and every read pays the load-time auto-remap cost for it. On tables whose churn is dominated by not-yet-indexed data these no-op versions pile up until an index rebuild advances the index version.

Decide per compaction, all-or-nothing: write the FRI only when some rewrite group's old fragments are covered by a data index, or by the existing FRI's new fragments (a not-yet-caught-up index reaches those through the composed remap chain); otherwise skip it. This is deliberately not a per-group filter -- a partial FRI is unsound because a concurrent reindex can turn a skipped "unindexed" fragment into an indexed one, and the commit conflict resolver's FRI-present path does not re-check skipped groups. Skipping entirely yields the same no-FRI state a non-deferred (inline) compaction produces, whose resolver path forces a retry if a concurrent reindex appears.

Also fix an off-by-one in load_index_fragmaps: max_fragment_id is inclusive, so the fragment_bitmap==None fallback (whose coverage the skip decision reads) must include the highest fragment. Log the skip at debug.

Tests: no-index and mixed (records-all-groups) cases, the chained multi-compaction case, plus the existing concurrent/cleanup-rebase tests updated to build an index (an FRI now requires one).
@xuanyu-z xuanyu-z force-pushed the fix/skip-fri-for-unindexed-rewrites branch from 44a44c7 to a838aac Compare July 14, 2026 07:46
@github-actions github-actions Bot added the A-python Python bindings label Jul 14, 2026

@westonpace westonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice optimization

@jackye1995 jackye1995 merged commit d1fa63b into lance-format:main Jul 14, 2026
30 of 31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-python Python bindings bug Something isn't working performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants