惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
A
About on SuperTechFans
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
H
Help Net Security
I
InfoQ
月光博客
月光博客
量子位
Blog — PlanetScale
Blog — PlanetScale
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
Engineering at Meta
Engineering at Meta
G
Google Developers Blog
D
DataBreaches.Net
宝玉的分享
宝玉的分享
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理

数据库

这样的 nql,大家能接受吗? - V2EX 用 sqlite 来当 toB 应用的数据库怎么样 - V2EX netbadb 应该做 mysql 兼容还是做 postgresql 兼容 - V2EX 怎么提升自然语言转 SQL 的准确性? - V2EX 各位现在还手写 sql 吗? [翻译] 为什么我要用 C# 构建数据库引擎 向量数据库的正确用法是什么? 明天就要软考了,我发现了数据库三范式之第一范式好像过时了 WM 到 IOS 用了二十年的数据表格软件 Listpro 准备退休了 异机备份方案 Oracle 裁员裁到大动脉了?官方软件 Oacle SQL Developer 居然出现恶性 BUG 了。 ubuntu 中 DataGrip 从数据表中复制的中文成了乱码 你们在用什么数据库管理软件? 大佬们,生产环境的 Mysql 和 Redis 都是部署在哪里的呢 不知道全国有多少数据系统被 Oracle 数据库的 VARCHAR2(X) 的默认单位给坑了 海量数据访问 刚问 AI 解了一个去年看书的一个疑惑:数据存储选择 lklv, llkv 有没有好用 GUI client 可以方便管理多个 Postgre 数据库 你们现在设计系统数据库的时候还在数据库层面搞外键约束吗? 有没有类似阿里云 mysql 数据库这样的数据恢复工具? 刚接触后端不久,帮忙推荐一个免费的数据库可视化工具 2026 年了,公司搞了一大堆没用的 Tracing 数据,存 ES 都快卡炸了 请教 oracle, mysql 不停机同步到达梦数据库实际工作中有什么方案吗 datagrip 切换查询界面的时候,结果集不随着跳转 我发现 TiDB Cloud 比较牛逼啊 请问有做过时序数据库的大佬么? 程序员玩具多系列:有什么 navicat/datagrip 替代品推荐 grafana 中 dashboard 里的数据显示为空,实际通过 Queries 查询是有数据的? 数据库性能测试的要点有哪些? 标签系统内容最大标签数放开到很大(比如三万)对性能影响有多大?
SQL 查不到数据,数据却明明存在——MySQL 降序主键与 index_me...
EasonIndie · 2026-07-17 · via 数据库

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

线上某张表 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 自动重建,无需手动干预。