FIND_IN_SET不能替代JOIN,因其无法使用索引导致全表扫描、不支持NULL匹配、仅做字符串子串匹配易误判,且性能远低于规范化关联表JOIN查询。

为什么不能直接用 FIND_IN_SET 做 JOIN?
因为 FIND_IN_SET 无法利用索引,字段上建了索引也没用。只要被 JOIN 的字段是逗号分隔的字符串(比如 "1,3,7"),FIND_IN_SET(id, tag_ids) 就会触发全表扫描——数据量一过万,查询就明显变慢。
常见错误写法:
SELECT * FROM posts p JOIN users u ON FIND_IN_SET(u.id, p.author_ids);
这种写法在 EXPLAIN 里一定看到 type: ALL,且 rows 等于整张表行数。
推荐方案:用 JSON_CONTAINS + JSON 数组替代逗号字符串
MySQL 5.7+ 支持原生 JSON,把 "1,3,7" 存成 [1,3,7] 后,就能用 JSON_CONTAINS 配合索引加速查询。
- 先确保字段类型是
JSON(不是TEXT或VARCHAR) - 插入或更新时用
JSON_ARRAY()构造,例如:INSERT INTO posts (author_ids) VALUES (JSON_ARRAY(1,3,7)); - JOIN 写法:
SELECT * FROM posts p JOIN users u ON JSON_CONTAINS(p.author_ids, CAST(u.id AS JSON));
- 必须给
author_ids字段加一个生成列 + 索引才能真正提速:ALTER TABLE posts ADD COLUMN author_ids_json JSON AS (author_ids) STORED;<br>CREATE INDEX idx_author_json ON posts (author_ids_json);
如果必须保留逗号字符串,怎么临时补救?
升级前或存量系统无法改结构时,可借助 REGEXP 配合边界匹配,比 FIND_IN_SET 略快(但依然不推荐用于高频大表):
- 错误示例(会误匹配):
p.tag_ids REGEXP '3'→ 匹配到"13,30"也成立 - 正确写法(加边界):
CONCAT(',', p.tag_ids, ',') REGEXP CONCAT(',(', u.id, '),') - 更稳妥的写法(兼容开头/结尾/单值):
p.tag_ids REGEXP CONCAT('(^|,)(', u.id, ')(,|$)') - 注意:仍无法走索引,仅适合几千行以内的表;
REGEXP在 MySQL 8.0.4+ 才支持完整 PCRE 语法,旧版请用RLIKE
最彻底的解法:拆表,别存逗号拼接字段
所有“高效 JOIN”技巧都是权宜之计。真实业务中,只要涉及多对多关系(如文章-标签、订单-商品),就应该用关联表:
- 新建
post_tags表:post_id INT, tag_id INT,主键设为(post_id, tag_id) - 给
tag_id单独加索引,JOIN 时直接ON pt.tag_id = t.id,毫秒级响应 - 如果担心写入变慢,可用批量 INSERT:
INSERT INTO post_tags VALUES (1,1),(1,3),(1,7); - 读取时用
GROUP_CONCAT拼回字符串(只在最终展示层做,不在 JOIN 条件里用)
很多人卡在“改表结构太重”,但真正拖慢系统的,从来不是拆表动作本身,而是每次查询都在字符串里硬搜——这个成本日积月累,远超一次 ALTER TABLE。










