FIND_IN_SET会变慢是因为其内部逐字符扫描字符串,无法使用索引,导致全表扫描;即使字段有B-Tree索引,函数一用即失效,EXPLAIN显示type: ALL、key: NULL。

FIND_IN_SET 能查,但别当主键或索引字段用——它没法走索引,数据一多就慢得明显。
为什么 FIND_IN_SET 会变慢?
MySQL 对 FIND_IN_SET 内部是逐字符扫描字符串的,相当于对整个字段做隐式 SUBSTRING_INDEX + 循环匹配。哪怕你加了 B-Tree 索引,这个函数一用,索引直接失效。
常见错误现象:EXPLAIN 显示 type: ALL,key: NULL,哪怕字段上明明建了索引。
- 字段类型是
VARCHAR或TEXT,但内容像"1,3,7,12"这种逗号分隔值 - 写成
WHERE FIND_IN_SET('3', tag_ids) > 0,看起来简洁,实则全表扫描 - 在
ORDER BY或GROUP BY中嵌套使用,性能雪崩
什么时候可以凑合用?
小数据量、低频查询、临时分析场景下,FIND_IN_SET 写起来确实快。比如后台导出时查某几个固定 ID 是否在字段中,且表行数
示例(仅限测试/小表):
SELECT * FROM posts WHERE FIND_IN_SET('5', category_ids) > 0;注意点:
-
FIND_IN_SET第一个参数必须是纯字符串字面量或单值参数,不能是子查询或表达式(如FIND_IN_SET((SELECT id FROM users LIMIT 1), list)会报错) - 第二个参数字段里不能有空格:
"1, 2, 3"中的空格会导致FIND_IN_SET('2', category_ids)匹配失败 - 返回值是位置序号(从 1 开始),不是布尔值;查不到时返回 0,所以务必写
> 0,别写= 1
真正该怎么做?
把逗号分隔字段拆成关联表。这是唯一能兼顾查询效率和扩展性的解法。
比如原表 posts(id, title, tag_ids) 存着 "1,5,9",应该重构为:
- 新建
post_tags(post_id, tag_id),主键设为(post_id, tag_id) - 删掉原
tag_ids字段 - 查“带标签 5 的文章”就变成
SELECT p.* FROM posts p JOIN post_tags pt ON p.id = pt.post_id WHERE pt.tag_id = 5,能走索引
如果暂时无法改表结构,至少加个生成列(MySQL 5.7+)辅助过渡:
ALTER TABLE posts ADD COLUMN tag_id_5 TINYINT GENERATED ALWAYS AS (FIND_IN_SET('5', tag_ids) > 0) STORED;然后给这个生成列建索引:CREATE INDEX idx_tag5 ON posts(tag_id_5)。但注意:只对固定值有效,换一个 ID 就得新加一列。
最常被忽略的一点:FIND_IN_SET 不处理重复值或顺序语义——"3,3,1" 和 "3,1" 对它的结果完全一样,也没法知道 “3 出现在第几位”。真有这类需求,说明业务逻辑已经超出字符串拼接能承载的范围了。


















