LIKE '%xxx%' 在JOIN中拖垮性能是因为无法使用B树索引,导致全表扫描且每行左表都需遍历右表;根本原因是字符串匹配在JOIN阶段执行,引发中间结果集爆炸。

为什么LIKE '%xxx%' 在JOIN里拖垮性能
因为这种通配符开头的模糊匹配无法使用B树索引,MySQL/PostgreSQL都会退化为全表扫描;更糟的是,如果它出现在ON条件里,连接引擎得对左表每一行都扫一遍右表——数据量一上万,响应直接卡死。
- 常见错误现象:
Using where; Using join buffer (Block Nested Loop)出现在EXPLAIN输出里,说明没走索引、在硬扛 - 真实场景:用户表
users和日志表logs按logs.message LIKE CONCAT('%', users.name, '%')关联——名字越长、日志越多,越慢 - 根本原因:字符串匹配逻辑在JOIN阶段执行,不是WHERE后过滤,导致中间结果集爆炸
全文索引不是万能解药,用错字段类型照样失效
MySQL的FULLTEXT索引只支持CHAR/VARCHAR/TEXT,且仅对自然语言模式(MATCH ... AGAINST)生效;如果你在JSON字段或ENUM上建全文索引,或者用IN BOOLEAN MODE却漏写+/-操作符,查询会静默退化成普通LIKE。
- 必须确保字段已建
FULLTEXT索引:ALTER TABLE logs ADD FULLTEXT(message) - JOIN中不能直接
MATCH,得先子查询或CTE预过滤:(SELECT id FROM logs WHERE MATCH(message) AGAINST('张三' IN NATURAL LANGUAGE MODE)) AS filtered_logs - PostgreSQL要装
pg_trgm扩展+GIST索引才能加速ILIKE,不是建完索引就自动提速
把字符串匹配从JOIN条件里“摘出来”才是关键
绝大多数业务场景其实不需要实时JOIN做模糊匹配——比如“找出发过含‘支付成功’日志的用户”,完全可以先查出日志ID,再反查用户,避免嵌套扫描。
- 优先用两步法:
SELECT user_id FROM logs WHERE message LIKE '%支付成功%'→ 再SELECT * FROM users WHERE id IN (...) - 如果必须单条SQL,改用
EXISTS代替JOIN:SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM logs l WHERE l.message LIKE CONCAT('%', u.name, '%') AND l.user_id = u.id),让优化器有机会用user_id索引快速定位 - 注意
CONCAT拼接会导致参数化失败,预编译时可能缓存低效执行计划
字符集和排序规则不一致会让所有优化白忙
当users.name是utf8mb4_unicode_ci,而logs.message是utf8mb4_general_ci,MySQL在JOIN时会隐式转换,强制对其中一个字段调用CONVERT()函数,索引直接失效。
- 检查一致性:
SHOW FULL COLUMNS FROM users LIKE 'name'; SHOW FULL COLUMNS FROM logs LIKE 'message'; - 统一改用
utf8mb4_0900_as_cs(大小写敏感+音调敏感),尤其当匹配区分大小写时 - 连接条件里显式转码反而更慢:
ON CONVERT(u.name USING utf8mb4) = CONVERT(l.message USING utf8mb4)—— 别这么干
真正卡住性能的,往往不是算法多高深,而是字符集、索引类型、JOIN写法这三处细节没对齐。改之前先EXPLAIN FORMAT=TREE看一眼实际执行路径,比猜强得多。










