FIND_IN_SET一定不走索引,因其在WHERE中对字段应用函数,破坏B+树索引的有序性与可定位性,导致MySQL只能全表扫描;根本解决法是拆分为关联表并为外键建索引,改用JOIN等值查询以命中索引。

为什么FIND_IN_SET一定不走索引
FIND_IN_SET() 是一个字符串函数,MySQL 无法对它做索引下推(Index Condition Pushdown),更无法将它转换成范围扫描或等值查找。只要 WHERE 条件里出现 FIND_IN_SET(col, 'a,b,c') 或 FIND_IN_SET('x', col),哪怕 col 上有索引,执行计划里的 type 也必然是 ALL —— 全表扫描。
根本原因在于:索引是按字段值有序组织的 B+ 树,而 FIND_IN_SET 要在逗号分隔字符串里做子串匹配,这破坏了索引的有序性和可定位性。MySQL 无法跳过无关数据页,只能逐行取值、拆解、比对。
用 JOIN 拆表替代 FIND_IN_SET
这是最彻底、效果最明显的优化方式,尤其适合高频查询或数据量超万的场景。
- 把原表中存逗号分隔值的列(比如
user_roles字段)拆成独立关联表,每条记录只存一个关系,例如:user_id和role_id两列 - 在关联表上为
role_id建普通索引:CREATE INDEX idx_role_id ON user_roles (role_id); - 原查询:
SELECT * FROM users WHERE FIND_IN_SET('admin', user_roles);
改为:SELECT DISTINCT u.* FROM users u JOIN user_roles ur ON u.id = ur.user_id WHERE ur.role_id = 'admin';
这样就能命中 idx_role_id,执行计划从 ALL 变成 ref 或 range,百万级数据查询耗时通常从秒级降到毫秒级。
临时绕过但不推荐的“急救”写法
如果暂时不能改表结构,又急需缓解慢查,可考虑以下两种折中方案,但都有明显局限:
-
LIKE模糊匹配(仅适用于值无歧义、无前缀冲突):WHERE roles LIKE '%admin%' OR roles LIKE 'admin,%' OR roles LIKE '%,admin' OR roles = 'admin'
→ 容易误匹配(如'superadmin'被命中),且仍无法走索引 - 把
GROUP_CONCAT结果提前算好、缓存到变量再传入FIND_IN_SET:SELECT @ids := GROUP_CONCAT(id) FROM card;SELECT cardName FROM diyCard WHERE FIND_IN_SET(id, @ids);
→ 避免子查询重复执行,但依然全表扫描,只是少了一次聚合开销
这两种写法本质没解决索引问题,只是减少中间计算,上线前务必用 EXPLAIN 确认 type 还是不是 ALL。
别忽略的细节:字符集与排序规则
即使你成功改成 JOIN 关联表,如果 role_id 字段用了 utf8mb4_general_ci 这类非二进制排序规则,且值含大小写混合(如 'Admin' vs 'admin'),可能因隐式转换导致索引失效。
- 检查字段实际排序规则:
SHOW FULL COLUMNS FROM user_roles LIKE 'role_id'; - 若需精确匹配,建议统一用
utf8mb4_bin或显式加BINARY:WHERE BINARY ur.role_id = 'admin' - JOIN 条件两端字段类型、字符集、排序规则必须完全一致,否则 MySQL 会自动做类型转换,索引直接失效
真正卡住性能的,往往不是大方向错了,而是这些字段定义层面的微小不一致 —— 查 EXPLAIN 输出里的 key_len 和 Extra 字段,比对是否出现 Using where; Using index 还是只剩 Using where,就能快速定位。


















