find_in_set、IN、LIKE 谁更坑?90% 开发者都用错了!
在 MySQL 开发中,“判断某个值是否在集合里” 是高频需求。但面对 find_in_set、IN、LIKE 这三个常用工具,很多人凭感觉乱用,结果要么查不出数据,要么把数据库查崩!今天就用真实案例拆解它们的性能差异、优缺点和适用场景,帮你避开 90% 的坑,最后还邀请大家分享实战 SQL 技巧。
一、先看一个血案:用错函数,查询从 0.1 秒变 10 秒
前几天帮朋友排查项目故障:一个用户列表查询,原本 0.1 秒出结果,加了个 “筛选用户角色” 的条件后,直接卡到 10 秒!原来他写了这样的 SQL:
-- 想筛选出拥有"管理员"角色(角色编码1)的用户
SELECT * FROM user WHERE roles LIKE '%,1,%';
而用户表的 roles 字段存的是 “1,2,3” 这种逗号分隔字符串,表数据才 5 万行,却触发了全表扫描。后来把 SQL 改成FIND_IN_SET('1', roles),查询时间降到 2 秒;再重构表结构用 IN 查询,直接回到 0.1 秒。
这个案例暴露了很多人对这三个工具的认知误区:以为能实现 “包含判断” 就可以随便用,却忽略了性能天差地别。
二、3 分钟搞懂:三者核心差异对比
先上一张对比表,把关键区别讲透,后面再逐个拆解:
对比维度 | IN 运算符 | FIND_IN_SET 函数 | LIKE 运算符 |
操作对象 | 固定值列表 / 子查询结果 | 逗号分隔的字符串字段 | 任意字符串字段 |
匹配逻辑 | 直接匹配列表值 | 拆分字符串后精确匹配 | 通配符模糊匹配 |
索引支持 | 支持(字段有索引时) | 不支持(必全表扫描) | 仅前缀匹配支持(如 '% a' 不支持) |
性能(大数据量) | 优秀(毫秒级) | 较差(秒级) | 极差(分钟级,全表扫描) |
典型错误场景 | 用 IN 判断字符串字段集合 | 大数据表用它做筛选 | 用 '% 值 %' 判断包含关系 |
1. IN 运算符:性能王者,但只认 “固定列表”
什么是 IN?
IN 是 MySQL 里的 “多值匹配神器”,专门判断 “某个值是否在固定列表里”,语法超简单:
-- 例子1:判断id是否在[1,3,5]列表中(用索引,0.1秒出结果)
SELECT * FROM user WHERE id IN (1,3,5);
-- 例子2:结合子查询(动态获取列表,同样支持索引)
SELECT * FROM user WHERE dept_id IN (SELECT dept_id FROM dept WHERE status=1);
优点:
- 性能天花板:只要查询的字段有索引(比如 id、dept_id),MySQL 会直接走索引查找,大数据量下也能秒出结果。
- 语法直观:一看就懂,不用记复杂函数参数。
- 支持子查询:可以动态生成匹配列表,灵活度高。
缺点:
- 不能直接匹配字符串集合:比如字段 roles 存 “1,2,3”,写WHERE 1 IN (roles)会报错!因为 IN 只认 “(值 1, 值 2)” 这种列表,不认字符串。
- 列表长度有限制:如果 IN 后面跟几千个值,SQL 解析会变慢,建议用子查询替代。
适用场景:
- 匹配固定值列表(如筛选指定 ID 的用户)。
- 匹配子查询返回的结果(如筛选属于活跃部门的用户)。
2. FIND_IN_SET:字符串集合专用,但性能拉胯
什么是 FIND_IN_SET?
专门处理 “逗号分隔的字符串字段”,比如 roles 字段存 “1,2,3”,判断 “1” 是否在里面,语法:
-- 正确用法:判断1是否在roles字段的字符串里
SELECT * FROM user WHERE FIND_IN_SET('1', roles);
-- 错误用法:不要用它匹配固定列表(性能不如IN)
SELECT * FROM user WHERE FIND_IN_SET(id, '1,3,5');
优点:
- 精确匹配字符串集合:不会像 LIKE 那样 “误伤”,比如FIND_IN_SET('2', '1,23')会返回 0(不匹配),而 LIKE '%,2,%' 会误判为匹配。
- 解决 “字符串存集合” 的痛点:如果表结构没设计好,字段里存了逗号分隔值,用它能准确筛选。
缺点:
- 性能黑洞:无论字段有没有索引,FIND_IN_SET 都不会走索引!因为它要先把字符串拆分成集合,再逐个匹配,5 万行数据可能要 2-3 秒,100 万行直接卡崩。
- 依赖字符串格式:如果字段里有空格(比如 “1, 2,3”),会匹配失败,必须先清理格式。
- 不支持子查询:不能像 IN 那样写FIND_IN_SET(id, SELECT ...),只能传固定字符串。
适用场景:
- 字段存储了逗号分隔的字符串(如标签、角色),且数据量小(比如几百行)。
- 临时查询,不追求性能(比如后台小工具)。
3. LIKE:灵活但危险,90% 的人用错它
什么是 LIKE?
LIKE 是模糊匹配工具,用 %(任意字符)、_(单个字符)做通配符,比如:
-- 前缀匹配(支持索引,性能好)
SELECT * FROM user WHERE username LIKE '张%';
-- 包含匹配(不支持索引,性能差)
SELECT * FROM user WHERE roles LIKE '%,1,%';
优点:
- 灵活度最高:可以匹配任意位置的字符串,比如开头、结尾、中间包含。
- 语法简单:不用记复杂函数,新手也能上手。
缺点:
- 性能极差(包含匹配时):如果用%值%(中间包含),MySQL 会放弃索引,全表扫描,10 万行数据可能要几十秒。
- 匹配不精确:容易误判,比如LIKE '%,2,%'会匹配 “12,3”“22,4”,把包含 “2” 的字符串都算进去。
- 无法处理空值:如果字段是 NULL,LIKE 匹配不到,需要用 IS NULL。
适用场景:
- 前缀匹配(如张%):此时支持索引,适合模糊查询用户名、手机号(如查 138 开头的手机号)。
- 简单的字符串开头 / 结尾匹配(如%@qq.com查 QQ 邮箱)。
- 绝对不要用它做 “包含判断”(如%,1,%),除非数据量极小!
三、避坑指南:3 个实战原则
- 优先用 IN,避免 FIND_IN_SET 和 LIKE 的包含匹配
只要能拆成 “固定列表” 或 “子查询”,就用 IN,性能秒杀另外两个。比如原本用FIND_IN_SET('1', roles),如果能改成role_id IN (1)(把 roles 拆成关联表),性能会提升 100 倍。
- 拒绝 “字符串存集合” 的表设计
这是根源问题!很多人图方便,把角色、标签存成 “1,2,3” 这种字符串,后期查询全是坑。正确做法是拆成关联表,比如 user_roles 表(user_id, role_id),然后用 IN 或 JOIN 查询:
-- 拆表后用JOIN,性能优秀
SELECT u.* FROM user u
JOIN user_roles ur ON u.id = ur.user_id
WHERE ur.role_id = 1;
- 迫不得已用 FIND_IN_SET?加个限制条件
如果表结构改不了,必须用 FIND_IN_SET,一定要加其他筛选条件(如时间范围),减少扫描行数:
-- 不好:全表扫描5万行
SELECT * FROM user WHERE FIND_IN_SET('1', roles);
-- 好:先按create_time筛选,只扫描1000行
SELECT * FROM user
WHERE create_time > '2024-01-01'
AND FIND_IN_SET('1', roles);
各位看官老爷在项目里踩过 SQL 的坑,也总结出了自己的技巧:比如用 JOIN 替代子查询、用索引覆盖优化查询、用 EXPLAIN 分析执行计划……
欢迎在评论区分享你的实战技巧:比如你是怎么优化慢 SQL 的?有没有遇到过 FIND_IN_SET 或 LIKE 的坑?一起帮更多开发者避坑!