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

EasonIndie 2026-07-17 08:30 1

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


线上某张表 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)
  • unused 07-17 08:45
    1
    这个啊 https://bugs.mysql.com/bug.php?id=106207
  • opengps 07-17 08:49
    2
    联合主键的应用场景很少,但需要的场景往往只能用联合主键。
    op 这段业务里主键的三个字段里显然 status 是多余的,所以单纯从这一步来讲就应该先考虑调整主键结构
  • JYii 07-17 08:53
    3
    这得把(id DESC, status) 索引谁创建的挖出来鞭尸
  • EasonIndie 楼主 07-17 08:54
    4
    @opengps 是的
    @unused 嗯,现在就是调整了主键 ,现在去掉了后效率也没啥影响。
  • opengps 07-17 09:01
    5
    @EasonIndie #4 首先你提到了数据量不大,所以这个时候索引本身的意义就不明显,因为全表扫描和走索引去找,提效时间对于应用层来说感知太少,微乎其微。
    对于数据量小,我甚至不建议一开始建表就设置上索引,因为索引真正有价值是在明确知道了查询特征之后才是最高收益的创建时机。在我的实际业务中也是上线之后过一段时间在通过监控慢 sql 来调整索引。
  • EasonIndie 楼主 07-17 09:10
    6
    @opengps #5 我们这的习惯是在出设计文档建表时候就加一些常用查询的索引。后面再加索引也会做。
  • exiaoxing 07-17 09:20
    7
    感谢分享,学习了
  • xubeiyou 07-17 09:25
    8
    数据量小 索引意义太小了 除非多表关联
  • callmecaiyuyu 07-17 09:29
    9
    终于有分享讨论技术的了,好评
  • qW7bo2FbzbC0 07-17 09:35
    10
    现在生产真的有人上 8.0 了吗,谢谢楼主提出的问题,8.0 最新的小版本有没有修复?
  • GuryYu 07-17 09:45
    11
    楼主用的什么版本的,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 07-17 10:43
    12
    @qW7bo2FbzbC0

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

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

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

    数据库版本升级其实挺重要的,但是又挺麻烦的。像我公司项目用的 postgres ,从 9.6 升级到 14/16 ,优化是真的多,bug 也少很多,还多了很多新特性。
    之前印象深刻的是某个表,用 col = 'some_value'查,也是查不到,莫名其妙。
    然后用正则匹配 col ~ 'some_value' 查到了,各种检查前后是不是有特殊字符,没有,还是莫名其妙。
    最后看了一下 explain ,发现正则不走索引,突发奇想 reindex 了一下,终于查到了,原来是索引的问题。
    好像是从库升主库,但是索引有问题导致的。不记得是具体哪个 pg 版本了。
  • sunwang 07-17 15:47
    33
    感觉不了解 mysql 的细节很难排查这种问题啊,老哥是如何学习这方面的知识的,看书吗?
  • pangdundun996 07-17 16:04
    34
    @sunwang 不需要太细节, 实际 explain 出来就能看到 diff 了, 就算没有 ai, 以前拿关键字[intersect]去 google 一般就能找到 bug 记录了,除非你是第一个遇到这个 bug 的人, 那确实很难了
  • cellsyx 07-17 16:15
    35
    还能这样,学习了。
    PRIMARY KEY (`id` DESC, `status`) USING BTREE,
    想知道之前这么设计的主键的理由是什么呢,难道只是为了方便查询?
  • adoal 07-17 16:38
    36
    @woodfizky 我记得老版本的 PG 里索引 method 用 hash 的时候是不能复制到从库的,从库升主库要 reindex ,或者干脆建索引时用 btree 。从某个版本开始才可以。
  • S4msara 07-17 16:41
    37
    很久没看到纯硬核技术分享,特地登录收藏、点赞👍
* 帖子来源V2EX
返回