最左前缀底层原理

最左前缀底层原理

问题

面试不仅会问”什么是最左前缀匹配”,更会问”底层是怎么实现的”。理解 B+树中联合索引的存储结构是关键。

B+树联合索引的存储

排序规则

联合索引 (a, b) 在 B+树中的排序规则:

-- 排序规则:
-- 1. 先比较 a 的值
-- 2. a 相同时比较 b 的值
-- 3. a 不同时 b 的顺序无意义
graph TD
    subgraph B+树叶子节点[叶子节点存储顺序]
        A[('赵', 18)  id=10]
        B[('赵', 20)  id=15]
        C[('赵', 25)  id=20]
        D[('钱', 19)  id=8]
        E[('钱', 22)  id=12]
        F[('孙', 17)  id=5]
        G[('孙', 30)  id=25]
        H[('李', 18)  id=30]
    end

    A <--> B <--> C <--> D <--> E <--> F <--> G <--> H

完整的 B+树结构

graph TD
    subgraph 根页
        Root[索引节点<br/>(,18) (,17) ...]
    end

    subgraph 非叶子节点
        N1[节点1<br/>(,18)  A]
        N1[节点1<br/>(,19)  B]
        N2[节点2<br/>(,17)  C]
        N2[节点2<br/>(,18)  D]
    end

    subgraph 叶子节点[叶子节点]
        L1[A<br/>(,18) (,20) (,25)]
        L2[B<br/>(,19) (,22)]
        L3[C<br/>(,17) (,30)]
        L4[D<br/>(,18)]
    end

    Root --> N1
    Root --> N2
    N1 --> L1
    N1 --> L2
    N2 --> L3
    N2 --> L4

为什么 WHERE b=xxx 走不了索引

graph TD
    subgraph B+树叶子节点[叶子节点]
        L1[('赵', 18)] --> L2[('赵', 20)] --> L3[('赵', 25)]
        L3 --> L4[('钱', 19)] --> L5[('钱', 22)]
        L5 --> L6[('孙', 17)]
    end

    Query[查询 WHERE b=18] --> Search{B+树怎么搜?}
    Search --> Compare{比较公式: (a, b)}
    Compare -->|传入 (?, 18)| Problem["? 无法比较
因为没有第一列的值"
] Problem --> Result[只能从第一页遍历到最后一页<br/> 全索引扫描而不是索引查找]

关键点:B+树的内部节点是通过比较 (a, b) 复合键来定位的。如果缺少 a,根本无法与非叶子节点的索引项进行比较。

// 伪代码:B+树节点查找逻辑
int compare(CompositeKey k1, CompositeKey k2) {
    if (k1.a != k2.a) {
        return compare(k1.a, k2.a);  // 先比 a
    } else {
        return compare(k1.b, k2.b);  // a 相同才比 b
    }
}

// 如果 WHERE b=18:
// 传入节点比较的是 (?, 18)
// 无法与节点中的 (赵, 18) 比较
// 因为不知道 ? 和 '赵' 谁大谁小

为什么 WHERE a=1 AND c=3 只能用到 a

-- 索引 idx_a_b_c(a, b, c)

-- 查询:WHERE a = 1 AND c = 3
graph LR
    subgraph B+树中
        A[(1, 2, x)] --> B[(1, 3, y)]
        B --> C[(1, 3, z)]
        C --> D[(1, 4, w)]
        D --> E[(2, 1, v)]
    end

    Query1[WHERE a=1] --> B
    Query1 --> C
    Query1 --> D
    Note["找到所有 a=1 的范围
但无法跳到 c=3
因为 b 字段不确定"
] Filter[在回表后<br/> WHERE c=3 过滤]

过程
1. B+树定位到 a=1 的范围(用到索引)
2. 在 a=1 范围内,B+树无法确定 c=3 的位置(因为 b 是中间的维度,索引只保证 b 有序)
3. 只能扫描所有 a=1 的行,回表后过滤 c=3

范围查询后列失效的原因

-- 索引 idx_a_b_c(a, b, c)

-- 查询:WHERE a = 1 AND b > 10 AND c = 3
graph LR
    A[索引条目<br/>a=1, b=5, c=1] --> B[a=1, b=8, c=2]
    B --> C[a=1, b=10, c=1]
    C --> D[a=1, b=12, c=3]
    D --> E[a=1, b=15, c=1]
    E --> F[a=1, b=18, c=2]

    Start[ b>10 开始] --> D
    Start --> E
    Start --> F

    Note["索引保证了 (a,b) 有序
但在 b>10 范围内
c 是无序的!
c=3: 3, 1, 2
无法利用索引查找c"
]
  1. b > 10 是一个范围条件
  2. b > 10 的范围内,c 列的值是无序
  3. 因此 c = 3 不能在索引中直接定位,只能回表后过滤

面试要点

  • 排序本质:联合索引按列顺序逐层排序,后面的列的有序性依赖前面的列
  • B+树比较:节点比较是按复合键整体比较的,缺少前面的列无法比较
  • 跳列失效:跳过的列破坏了有序性依赖,后面的列索引失效
  • 范围失效:范围查询后,后面的列不再有序,无法利用索引

一句话总结:最左前缀的底层原理是 B+树复合键的比较规则——先比较第一列,相同才比较第二列,跳过了前面的列就失去了比较基准。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容