EasonIndie
0.11D
V2EX  ›  数据库

SQL 查不到数据,数据却明明存在——MySQL 降序主键与 index_merge intersect 的一次诡异排查

  •  5
     
  •   EasonIndie · 18h 24m ago · 2684 views

    一、现象:看得见,却查不到

    线上某张表 t_invoice 出现一个反常现象:同一条记录,去掉某个等值条件能查到,加上却查不到。

    -- 查询 A:带状态等值条件 —— 返回空
    SELECT biz_id, status, serial_no
    FROM t_invoice
    WHERE biz_id = 537
      AND status = 'PASSED';
    
    -- 查询 B:不带状态条件 —— 返回 6 行,其中 3 行 status = 'PASSED'
    SELECT biz_id, status, serial_no
    FROM t_invoice
    WHERE biz_id = 537;
    

    查询 B 的结果:

    biz_id status serial_no
    537 PASSED 2692*************8629
    537 PASSED 2692*************4377
    537 PASSED 2692*************1155
    537 REJECTED 2692*************6649
    537 CANCELLED 2692*************6599
    537 REJECTED 2692*************7967

    明明有 3 条 PASSED,查询 A 却返回空——典型的"看得见却匹配不到"。数据没丢,问题出在哪?


    二、排查

    第 1 步:先怀疑隐藏字符 —— 排除

    这类问题的头号嫌疑是字段值里混入了不可见字符(尾部空格、零宽字符、BOM )。用 HEX 看真实字节:

    SELECT status,
           LENGTH(status)       AS byte_len,
           CHAR_LENGTH(status)  AS char_len,
           HEX(status)          AS hex_val
    FROM t_invoice
    WHERE biz_id = 537;
    

    结果:PASSEDHEX = 504153534544,纯 ASCII ,6 字节 6 字符,干干净净,无任何隐藏字符。排除数据问题,方向转向优化器。

    第 2 步:看表结构 —— 发现一个"不对劲"的主键

    SHOW CREATE TABLE t_invoice;
    

    关键字段与索引:

    `status` varchar(32) ... COLLATE utf8mb4_general_ci NOT NULL,
    ...
    PRIMARY KEY (`id` DESC, `status`) USING BTREE,   -- ⚠ 异常:降序 + 业务列进了主键
    KEY `idx_biz` (`biz_id`),
    KEY `idx_status` (`status`),
    KEY `idx_biz_status` (`biz_id`, `status`)
    

    字符集与排序规则一切正常,但主键 (id DESC, status) 极不寻常:

    • idAUTO_INCREMENT 自增列,单独做主键就够了,没必要把业务状态列 status 塞进来;
    • DESC 降序索引对自增主键毫无意义,反而有害——自增插入会变成 B+ 树头部插入,引发页分裂。

    这个设计是后面所有麻烦的源头。

    第 3 步:对比执行计划 —— 锁定 Using intersect

    EXPLAIN SELECT ... WHERE biz_id = 537 AND status = 'PASSED';
    EXPLAIN SELECT ... WHERE biz_id = 537;
    
    查询 type key Extra 结果
    A (查不到) index_merge idx_biz_status, idx_status Using intersect(...) ❌ 空
    B (能查到) ref idx_biz - ✅ 6 行

    查询 A 触发了 index_merge + Using intersect:优化器对两个索引分别扫描后取交集。这就是头号嫌疑犯。

    第 4 步:两个验证 —— 确认 intersect 就是元凶

    -- ① 改用 LIKE ,走 range 而非 intersect
    WHERE biz_id = 537 AND status LIKE 'PASSED%';
    -- 执行计划:type=range, key=idx_biz_status, Extra=Using index condition
    -- 结果:✅ 返回 3 条
    
    -- ② 用 hint 关闭 index_merge
    SELECT /*+ SET_VAR(optimizer_switch='index_merge=off') */ ...
    WHERE biz_id = 537 AND status = 'PASSED';
    -- 结果:✅ 返回 3 条
    

    LIKE 能查到,说明复合索引物理完好、数据都在;关闭 index_merge 后等值查询也正常——铁证。问题既不是数据、也不是索引损坏,而是 intersect 算法本身算错了。

    第 5 步:NO_INDEX 模拟删索引 —— 区分"索引问题"还是"算法问题"

    NO_INDEX hint 让优化器假装某个索引不存在,无需真正 DDL 就能模拟"删索引后"的执行计划:

    -- 模拟删复合索引 idx_biz_status
    EXPLAIN SELECT /*+ NO_INDEX(t idx_biz_status) */ ...
    -- -> 仍走 intersect(idx_biz, idx_status),返回空 ❌
    
    -- 模拟删单列索引 idx_status
    EXPLAIN SELECT /*+ NO_INDEX(t idx_status) */ ...
    -- -> 改走 idx_biz_status ref(const,const),返回 3 条 ✅
    
    模拟操作 执行计划 结果
    删复合索引 idx_biz_status index_merge / Using intersect(idx_biz, idx_status) ❌ 空
    删单列索引 idx_status ref / idx_biz_status / const,const ✅ 3 条

    关键结论:删复合索引没用(还有两个索引继续 intersect ),删 idx_status 才有用(消除了 intersect 的候选)。说明问题不在某个索引,而在"多索引共存触发 intersect + 降序主键"这个组合。


    三、根因:降序主键 × index_merge intersect

    MySQL 8.0.26-cluster 的 index_merge intersection 算法与降序复合主键不兼容,触发链如下:

    1. 等值查询命中多个索引(idx_biz_statusidx_status 都覆盖 status 列);
    2. 优化器选择 index_merge intersect,对两个索引的 rowid 集合取交集;
    3. 主键含降序列 id DESC,二级索引的 rowid (= 主键值)在交集比较时字节序处理出错
    4. 本应匹配的主键被判为不相等 → 交集为空 → 查询返回空集。

    LIKErange、不带状态条件走单索引 ref,都不触发 intersect ,所以只有"biz_id = ? AND status = ?"这类等值查询会中招。


    四、解决:

    止血:应用层加 hint (立即生效)

    -- 方式 A:精准指定复合索引(最优,直接定位)
    SELECT /*+ INDEX(t idx_biz_status) */
           t.biz_id, t.status, t.serial_no
    FROM t_invoice t
    WHERE t.biz_id = 537 AND t.status = 'PASSED';
    
    -- 方式 B:关闭 index_merge (兜底)
    SELECT /*+ SET_VAR(optimizer_switch='index_merge=off') */
           t.biz_id, t.status, t.serial_no
    FROM t_invoice t
    WHERE t.biz_id = 537 AND t.status = 'PASSED';
    

    注意:该表所有 biz_id=? AND status=? 等值查询都受影响,需统一加 hint ,不能只改一处。

    根治:修正主键(最终采用方案)

    由于数据量不大,备份表后,在业务量少时,把主键从 (id DESC, status) 改回标准自增主键 (id)

    -- 1. 备份(务必)
    CREATE TABLE t_invoice_bak AS SELECT * FROM t_invoice;
    
    -- 2. 修正主键
    ALTER TABLE t_invoice
      DROP PRIMARY KEY,
      ADD PRIMARY KEY (`id`);
    
    -- 3. 验证(应返回 3 条)
    SELECT biz_id, status, serial_no
    FROM t_invoice
    WHERE biz_id = 537 AND status = 'PASSED';
    
    -- 4. 行数核对
    SELECT
      (SELECT COUNT(*) FROM t_invoice)     AS now_cnt,
      (SELECT COUNT(*) FROM t_invoice_bak) AS bak_cnt;
    

    安全性id 为自增全局唯一,(id, status) 唯一 ⟸ id 唯一,改主键不改变任何唯一性语义;自增计数器保留;二级索引 rowid 由 MySQL 自动重建,无需手动干预。

    37 replies    2026-07-17 16:41:37 +08:00
    unused
        1
    unused  
       18h 8m ago via Android   ❤️ 2
    opengps
        2
    opengps  
       18h 5m ago
    联合主键的应用场景很少,但需要的场景往往只能用联合主键。
    op 这段业务里主键的三个字段里显然 status 是多余的,所以单纯从这一步来讲就应该先考虑调整主键结构
    JYii
        3
    JYii  
       18h 1m ago
    这得把(id DESC, status) 索引谁创建的挖出来鞭尸
    EasonIndie
        4
    EasonIndie  
    OP
       17h 59m ago
    @opengps 是的
    @unused 嗯,现在就是调整了主键 ,现在去掉了后效率也没啥影响。
    opengps
        5
    opengps  
       17h 52m ago
    @EasonIndie #4 首先你提到了数据量不大,所以这个时候索引本身的意义就不明显,因为全表扫描和走索引去找,提效时间对于应用层来说感知太少,微乎其微。
    对于数据量小,我甚至不建议一开始建表就设置上索引,因为索引真正有价值是在明确知道了查询特征之后才是最高收益的创建时机。在我的实际业务中也是上线之后过一段时间在通过监控慢 sql 来调整索引。
    EasonIndie
        6
    EasonIndie  
    OP
       17h 43m ago
    @opengps #5 我们这的习惯是在出设计文档建表时候就加一些常用查询的索引。后面再加索引也会做。
    exiaoxing
        7
    exiaoxing  
       17h 33m ago
    感谢分享,学习了
    xubeiyou
        8
    xubeiyou  
       17h 28m ago
    数据量小 索引意义太小了 除非多表关联
    callmecaiyuyu
        9
    callmecaiyuyu  
       17h 25m ago
    终于有分享讨论技术的了,好评
    qW7bo2FbzbC0
        10
    qW7bo2FbzbC0  
       17h 19m ago
    现在生产真的有人上 8.0 了吗,谢谢楼主提出的问题,8.0 最新的小版本有没有修复?
    GuryYu
        11
    GuryYu  
       17h 9m ago
    楼主用的什么版本的,1 楼贴的 Bug 看上去在 24 年的 8.0.41 修复了
    ---
    [6 Nov 2024 22:37] Philip Olson
    Posted by developer:

    Fixed as of the upcoming MySQL Server 8.0.41, 8.4.4, and 9.2.0 releases, and here's the proposed changelog entry from the documentation team:

    Queries with a descending primary key and index_merge optimization could
    result in incorrect results, such as missing rows.

    Thank you for the bug report.
    [29 Apr 9:04] Chaithra Marsur Gopala Reddy
    Bug#112055 is marked as duplicate of this one.
    Rache1
        12
    Rache1  
       16h 10m ago
    @qW7bo2FbzbC0

    8.0 第一个版本发布于 「 8 years ago (08 Apr 2018)」,还有啥不能用的

    9.0 倒是新一点,第一个版本发布于「 2 years ago (07 Jun 2024)」,采用的或许会少一些。
    xingzi10000
        13
    xingzi10000  
       16h 9m ago
    好久没看到技术文章了,赞
    qW7bo2FbzbC0
        14
    qW7bo2FbzbC0  
       16h 4m ago
    @Rache1 #12 那挺好,你们还有新业务需要建新实例。
    liangc230323
        15
    liangc230323  
       16h 2m ago
    @qW7bo2FbzbC0 现在难道不都是 8.x 吗?云厂商的主流版本吧
    conglovely
        16
    conglovely  
       15h 58m ago
    学习了,谢谢佬哥
    qW7bo2FbzbC0
        17
    qW7bo2FbzbC0  
       15h 44m ago
    @liangc230323 #15 新建的实例肯定是 8.0 了
    EmbraceNew
        18
    EmbraceNew  
       15h 33m ago
    看的很过瘾,AI 时代,还有这么高质量的踩坑并给出排查思路和方案的帖子,实属一股不可多得的清流
    pengtao2001
        19
    pengtao2001  
       15h 30m ago
    联合主键,真的有人这么做?
    EasonIndie
        20
    EasonIndie  
    OP
       15h 22m ago
    @GuryYu 文中提及了 MySQL 8.0.26-cluster
    @EmbraceNew 过瘾就好,虽然但是不好意思,我排查问题还是用的 ai ,对话只用了一轮我定位到了问题,描述了现象,使用了连接数据库的 mcp ,1 分钟 glm5.2 给我定位了。。
    658
        21
    658  
       14h 59m ago
    挺好的内容 就是有点 ai 味
    privil
        22
    privil  
       14h 55m ago
    @qW7bo2FbzbC0 #10 都出了几百年了,你们还 5.7 啊
    EasonIndie
        23
    EasonIndie  
    OP
       14h 53m ago
    不要着相。
    不用技术洁癖,本身 ai 就是现在技术的浪潮上。
    我是 ai 排查的帮我极大节省时间,我觉得这个 bug 也是有意义发个博客,ai 整理输出博客也方便,无可厚非。

    @658
    @EmbraceNew
    qbqbqbqb
        24
    qbqbqbqb  
       13h 12m ago
    现在 8.0 应该已经 EOL 了,LTS 是 8.4
    adoal
        25
    adoal  
       13h 5m ago
    这里其实有两个问题,一是表的字段和索引设计有不合理之处,二是 DBMS 的实现有 bug ,正好凑在一起遇到了。
    EasonIndie
        26
    EasonIndie  
    OP
       12h 53m ago
    @adoal 是的,在这个业务场景确实不该这样用。
    我看到还有个表也有联合主键,看了下业务逻辑是:用户可能多部门,主键是 用户 id + 部门 id,但不存在:"多个索引覆盖同一查询列"导致优化器选 intersect 。这样就没问题。
    @opengps #2
    @pengtao2001
    EeveeRibbon
        27
    EeveeRibbon  
       12h 51m ago
    学习到了,我们有些老数据库也在往 8 版本迁移,下次遇见了就知道问题了
    EasonIndie
        28
    EasonIndie  
    OP
       12h 45m ago
    @qbqbqbqb 确实,但是跟架构反馈了,他说“升不了一点”。。
    back0893
        29
    back0893  
       12h 29m ago
    啊?还有这个问题了。。我以为是空字符串的问题。。
    没想到索引会导致的这个问题
    qW7bo2FbzbC0
        30
    qW7bo2FbzbC0  
       12h 12m ago
    @qbqbqbqb #24 8.3 开始,为了响应政治化正确,slave --> replica ,很多工具和脚本都要重新配套
    DUNAI
        31
    DUNAI  
       11h 22m ago
    老老实实给 MJJ 大军开路就完事儿了
    woodfizky
        32
    woodfizky  
       11h 16m ago
    MySQL8.x 在 2026 年看到这个真的有种。。哈哈哈。。。恍惚感。。

    数据库版本升级其实挺重要的,但是又挺麻烦的。像我公司项目用的 postgres ,从 9.6 升级到 14/16 ,优化是真的多,bug 也少很多,还多了很多新特性。
    之前印象深刻的是某个表,用 col = 'some_value'查,也是查不到,莫名其妙。
    然后用正则匹配 col ~ 'some_value' 查到了,各种检查前后是不是有特殊字符,没有,还是莫名其妙。
    最后看了一下 explain ,发现正则不走索引,突发奇想 reindex 了一下,终于查到了,原来是索引的问题。
    好像是从库升主库,但是索引有问题导致的。不记得是具体哪个 pg 版本了。
    sunwang
        33
    sunwang  
       11h 6m ago
    感觉不了解 mysql 的细节很难排查这种问题啊,老哥是如何学习这方面的知识的,看书吗?
    pangdundun996
        34
    pangdundun996  
       10h 50m ago
    @sunwang 不需要太细节, 实际 explain 出来就能看到 diff 了, 就算没有 ai, 以前拿关键字[intersect]去 google 一般就能找到 bug 记录了,除非你是第一个遇到这个 bug 的人, 那确实很难了
    cellsyx
        35
    cellsyx  
       10h 39m ago
    还能这样,学习了。
    PRIMARY KEY (`id` DESC, `status`) USING BTREE,
    想知道之前这么设计的主键的理由是什么呢,难道只是为了方便查询?
    adoal
        36
    adoal  
       10h 16m ago
    @woodfizky 我记得老版本的 PG 里索引 method 用 hash 的时候是不能复制到从库的,从库升主库要 reindex ,或者干脆建索引时用 btree 。从某个版本开始才可以。
    S4msara
        37
    S4msara  
       10h 12m ago
    很久没看到纯硬核技术分享,特地登录收藏、点赞👍
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   1039 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 92ms · UTC 18:54 · PVG 02:54 · LAX 11:54 · JFK 14:54
    ♥ Do have faith in what you're doing.