find_in_set不能用索引加速,因其本质是逐行逐字符扫描strlist,mysql无法利用b+树索引跳过比较,即使字段建有索引,explain仍显示type: all,导致全表扫描。

为什么 FIND_IN_SET 不能用索引加速查询
FIND_IN_SET(str, strlist) 是 MySQL 提供的字符串查找函数,用于判断 str 是否出现在以逗号分隔的字符串 strlist 中。但它本质是逐字符扫描,MySQL 无法利用 B+ 树索引跳过比较——哪怕 strlist 字段加了索引也没用。
常见错误现象:EXPLAIN 显示 type: ALL,哪怕表有百万行,查询也变全表扫描。
- 它不等价于
IN(后者走索引的前提是列值独立存储) - 它不支持通配符或正则,
FIND_IN_SET('12', '1,12,123')返回 2,但FIND_IN_SET('1', '10,20')返回 0(不会误匹配子串) - 如果
strlist为空字符串或 NULL,整个表达式返回 NULL,WHERE 条件会过滤掉这些行
怎么写一个安全可用的 FIND_IN_SET 查询
典型场景:权限字段存为 '1,5,7',查拥有权限 ID=5 的用户。
正确写法必须处理边界情况,尤其是开头、中间、结尾和单值情形:
SELECT * FROM users
WHERE FIND_IN_SET('5', permissions) > 0;
注意:FIND_IN_SET 返回位置(从 1 开始),所以要显式判断 > 0,不能只写 FIND_IN_SET('5', permissions)——因为返回 0 在 WHERE 中等价于 FALSE,但返回 NULL 会导致该行被排除(而你可能想保留 NULL 权限记录做后续判断)。
- 参数顺序不能颠倒:
FIND_IN_SET('5', permissions)对;FIND_IN_SET(permissions, '5')错(第二个参数必须是逗号分隔字符串) - 两个参数都转为字符串比较,
FIND_IN_SET(5, permissions)会隐式转成'5',但不推荐依赖隐式转换 - 如果
permissions是 TEXT 类型且超长(比如 > 1MB),函数执行会明显变慢,建议限制字段长度
替代方案:什么时候该放弃 FIND_IN_SET
当查询频率高、数据量大,或需要多条件组合(如“权限包含 5 且状态为 active”),FIND_IN_SET 就成了性能瓶颈和维护隐患。
更合理的做法是拆表:
CREATE TABLE user_permissions ( user_id INT, perm_id INT, PRIMARY KEY (user_id, perm_id) );
- 用
JOIN或EXISTS替代,能走索引,支持统计、去重、批量增删 - 避免更新时字符串拼接引发的并发冲突(如两个事务同时读出
'1,2',各自加上'3'再写回,结果只剩一个'3') - 如果业务已上线且无法改表结构,至少把 FIND_IN_SET 查询封装进视图或存储过程,便于后续统一替换
容易忽略的字符编码与空格问题
FIND_IN_SET 严格按字面匹配,前后空格、全角逗号、不可见字符都会导致失败。
例如:permissions 值为 '1, 5 ,7'(含空格),FIND_IN_SET('5', permissions) 返回 0。
- 修复方法:查询前用
REPLACE(REPLACE(permissions, ' ', ''), ' ', '')清理(注意全角空格U+3000) - 更稳妥的是在写入时就规范格式:
TRIM(REPLACE(REPLACE(@input, ' ', ''), '\t', '')) - 如果字段用了 utf8mb4_bin 排序规则,大小写敏感,
FIND_IN_SET('Admin', 'admin,user')也返回 0
真正麻烦的不是函数怎么写,而是字段里混进了谁也不知道什么时候塞进去的空格、换行或 BOM 头——上线前务必用 HEX(permissions) 抽样检查原始字节。











