Skip to content

Be more liberal in allowing for index merges - #24

Open
ben-thul wants to merge 1 commit into
release-8.4.5-501from
bento/index-merges-r-cool
Open

Be more liberal in allowing for index merges#24
ben-thul wants to merge 1 commit into
release-8.4.5-501from
bento/index-merges-r-cool

Conversation

@ben-thul

Copy link
Copy Markdown

Attempts to address this issue.

@ben-thul
ben-thul requested a review from a team August 14, 2025 01:39
@ben-thul

Copy link
Copy Markdown
Author

Here's a transcript of my "vibe debugging" session with Claude


MySQL Index Merge Sort Union Regression Analysis

Problem Statement

A performance regression was identified between MySQL 8.0.34 and 8.4.5 where queries that previously used index_merge_sort_union optimization no longer consider this strategy, even when explicitly hinted with /*+ INDEX_MERGE(...) */.

Example Query:

SELECT * FROM myTable WHERE col1 = 1 AND (col2 = 2 OR col3 = 3)

In MySQL 8.0.34, this would use index merge optimization. In 8.4.5, it falls back to less efficient single-index strategies.

Investigation Summary

Key Findings

  1. Hypergraph optimizer is not the cause - Verified that hypergraph_optimizer=off and index_merge_sort_union=on
  2. INDEX_MERGE hints don't work - Even explicit hints fail, indicating broken functionality rather than cost-based selection issues
  3. Root cause identified - Logic replacement with failing assertions in the join optimizer

Root Cause Analysis

The regression is caused by changes in sql/join_optimizer/join_optimizer.cc where functional logic was replaced with assertions that fail for common query patterns.

MySQL 8.0.34 (Working):

// Set inexact flag based on conditions
merge.inexact = (new_tree->merges.size() > 1);
merge.inexact |= !new_tree->keys_map.is_clear_all();

MySQL 8.4.5 (Broken):

// Replaced logic with assertions that can FAIL
assert(merge.inexact || new_tree->merges.size() == 1);
assert(merge.inexact || new_tree->keys_map.is_clear_all());

When These Assertions Fail

Condition 1: new_tree->merges.size() > 1

Occurs with complex OR conditions creating multiple merge candidates:

SELECT * FROM table WHERE 
  (col1 = 'A' AND col2 = 'B') OR 
  (col1 = 'C' AND col2 = 'D') OR
  (col1 = 'E' AND col3 = 'F');

Condition 2: !new_tree->keys_map.is_clear_all()

Occurs when predicates can use both regular range scans AND index merges:

SELECT * FROM table WHERE 
  col1 BETWEEN 100 AND 200 AND  -- Can use range scan on col1
  col2 = 'value' AND             -- Can use range scan on col2  
  col3 IN (1,2,3);               -- Can use range scan on col3

User's Specific Case

The query SELECT * FROM myTable WHERE col1 = 1 AND (col2 = 2 OR col3 = 3) triggers Condition 2 because:

  • Individual indexes can be used on col1, col2, or col3 (making keys_map non-empty)
  • Index merges are possible between col1+col2 and col1+col3
  • The inexact flag is not properly set by earlier logic
  • The assertion fails and index merge consideration is abandoned

Related Commits

Key commits between versions that contributed to this issue:

  1. WL#14808: Row-ID ordered scans in the hypergraph optimizer [3/9, needed_fields] (81e4576e5c3)

    • Removed needed_fields parameter handling
    • Changed cost calculation methodology
  2. Logic changes in join optimizer

    • Replaced functional inexact flag setting with failing assertions
    • Changed from permissive to strict validation
  3. Multiple "wrong result" fixes related to inexact handling suggest the 8.4.5 approach had correctness issues

Risk Assessment for Fix

Proposed Solution

Revert to the 8.0.34 behavior:

// Revert to 8.0.34 safe behavior  
merge.inexact = new_tree->inexact;
merge.inexact |= (new_tree->merges.size() > 1);
merge.inexact |= !new_tree->keys_map.is_clear_all();

Risk Analysis: LOW RISK

Why the reversion is safe:

  1. Conservative Approach: 8.0.34 behavior over-sets the inexact flag rather than under-setting it

    • Over-setting = Extra filtering (slower but correct results)
    • Under-setting = Missing filtering (faster but wrong results)
  2. Correctness Priority: The inexact flag controls whether additional filtering is applied after index merge execution. Over-filtering cannot produce wrong results.

  3. Performance vs Correctness Trade-off:

    • 8.0.34: Slightly slower (extra filtering) but always correct
    • 8.4.5: Potentially faster but completely broken (no index merge at all)
  4. Well-tested Logic: 8.0.34 behavior was in production for years and is known to be stable

What the inexact Flag Controls

From code documentation:

// If true, the index merge does not faithfully represent the entire
// predicate (it could return more rows), and needs to be re-checked
// with a filter.
bool inexact;

When inexact = true, MySQL adds an additional filter step to ensure correctness. This is much faster than falling back to single-index scans.

Recommendation

PROCEED WITH REVERSION - The fix is low-risk because:

  1. It fixes a clear regression without introducing new logic
  2. It uses conservative, well-tested code from a stable release
  3. It prioritizes correctness over micro-optimizations
  4. The performance cost is minimal compared to the current broken state

This change will restore index_merge_sort_union functionality for affected query patterns while maintaining result correctness through conservative filtering.

Technical Details

Files Affected

  • sql/join_optimizer/join_optimizer.cc - Main logic change needed
  • Assertions at lines ~1500 and ~1506 need to be replaced with assignment logic

Code Location

The problematic assertions are in the FindIndexRangeScans() method in the join optimizer, specifically in the section that processes PossibleIndexMerge structures.

Testing

Queries with patterns like col1 = value AND (col2 = value OR col3 = value) should be tested to verify index merge optimization is properly selected after the fix.

ben-thul pushed a commit that referenced this pull request Aug 19, 2026
https://perconadev.atlassian.net/browse/PS-11242

percona.clone_consistent_snapshot crashed in a debug build with:

  [InnoDB] Assertion failure: trx0sys.cc:821:
           prev_trx == nullptr || prev_trx->id > trx->id
   #5 trx_sys_validate_trx_list  trx0sys.cc:821
   #6 trx_erase_lists            trx0trx.cc:1838
   #8 trx_commit_in_memory       trx0trx.cc:2009
   ...
  #21 Clone_persist_gtid::write_to_table  clone0repl.cc:475
  #24 clone_gtid_thread                   clone0repl.cc:723

trx_sys->rw_trx_list is required to be ordered by descending trx->id,
and trx_sys_validate_trx_list() (UNIV_DEBUG only) enforces it on every
erase.

The consistent snapshot clone-view feature preallocates a transaction
id for a donor read-only transaction (ReadView::clone() in read0read.cc).
When that donor is later promoted to read-write via trx_set_rw_mode() ->
trx_assign_id_for_rw(), it reuses the preallocated id, which - as the
existing comment notes - "might not be received in ascending order" and
can therefore be smaller than ids handed out to other transactions in
the meantime.

trx_assign_id_for_rw() already accounts for this when maintaining the
sorted rw_trx_ids vector (the std::upper_bound insert), but
trx_add_to_rw_trx_list() always did UT_LIST_ADD_FIRST, which assumes the
new id is the greatest. Prepending an out-of-order preallocated id breaks
the descending-id ordering of rw_trx_list. The corruption goes unnoticed
at insertion time because trx_set_rw_mode() (unlike trx_start_low()) has
no post-insert validation, and is only caught later when an unrelated
transaction - here the clone GTID persister thread - commits and runs the
validation during trx_erase_lists().

Fix trx_add_to_rw_trx_list() to insert a transaction carrying a
preallocated_id at the position that preserves descending-id order,
mirroring the rw_trx_ids handling. The common case (a freshly allocated
id, which is always the greatest) keeps the O(1) prepend, so there is no
hot-path regression.
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