JOIN中用LIKE '%xxx%'必然慢,因B+树索引仅支持左前缀匹配,无法定位起始位置,导致全表扫描;嵌套循环下更恶化为O(m×n)字符串比对。

为什么JOIN里用LIKE '%xxx%' 一定会慢
因为B+树索引只认“从头开始”的匹配,LIKE '%xxx%'没有确定起始位置,优化器根本没法跳到索引某一段,只能对被驱动表逐行扫描。嵌套循环JOIN会让这事雪上加霜:驱动表1万行 × 被驱动表1万行 = 1亿次字符串比对。执行计划里type=ALL、key=NULL、rows接近总行数,就是它在报警。
把LIKE从ON挪到WHERE或EXISTS里
这是最简单也最安全的改造方式——不改语义、不赌兼容性、还能让优化器有机会走索引(如果模式允许)。关键在于明确区分“怎么连”和“连完怎么筛”。
-
LEFT JOIN t2 ON t1.id = t2.t1_id AND t1.name LIKE '%北京%':t1行只要name不含“北京”,t2字段全为NULL,但t1行仍保留 -
LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name LIKE '%北京%':先完成所有关联,再过滤,t1不满足条件的整行直接丢弃(LEFT JOIN退化成INNER JOIN) - 更推荐用
EXISTS替代模糊JOIN:比如WHERE EXISTS (SELECT 1 FROM t2 WHERE t1.name LIKE CONCAT('%', t2.keyword, '%')),逻辑清晰,且多数引擎能提前终止子查询
前缀匹配就加普通索引,别折腾函数索引
如果业务能接受“以某串开头”(比如查name LIKE '张%'或mobile LIKE '138%'),直接给该字段建普通索引就行,CREATE INDEX idx_name ON t1(name)足够。MySQL 5.6+的索引条件下推(ICP)还能顺便把其他WHERE条件一起下推到存储引擎层过滤。
- 别给
UPPER(name) LIKE 'ABC%'建函数索引——先确保字段没被函数包裹 - 前缀索引要测区分度:
SELECT COUNT(DISTINCT LEFT(name, 3)) / COUNT(*) FROM t1,值>0.95再用name(3) - 联合索引中,LIKE字段必须是第一个,否则
(status, name(4))对name LIKE 'abc%'完全无效
后缀/中缀匹配别硬扛,换方案
真要查“以xxx结尾”或“包含xxx”,LIKE '%xxx'或LIKE '%xxx%'加索引没用。这时候得换思路:
- 反向存储+反向索引:加生成列
name_rev VARCHAR(50) AS (REVERSE(name)) STORED,再建索引CREATE INDEX idx_name_rev ON t1(name_rev),查时写REVERSE(name) LIKE REVERSE('%张三%') - 全文索引只用于
MATCH() AGAINST(),不是加了就能加速LIKE——WHERE name LIKE '%张%'哪怕有FULLTEXT(name)也完全不走索引 - 中文全文索引必须显式指定解析器:
CREATE FULLTEXT INDEX idx_title ON articles(title) WITH PARSER ngram,且innodb_ft_min_token_size要调小才能搜单字 - 应用层过滤更可控:先用
city_id、category_id等精确条件把结果集压到几千行以内,再用Python做str.contains()或正则
真正难的不是写对SQL,而是判断“这个模糊需求到底能不能用数据库原生能力扛住”。一旦涉及%开头、多关键词交叉、或需要高亮/分词,就该果断切到Elasticsearch或应用层——别在JOIN里拼CONCAT('%', t2.keyword, '%'),那不是优化,是埋雷。

















