LIKE '%xxx%' 在 JOIN 中必然慢,因其前导通配符使优化器无法使用普通 B+ 树索引,只能全表扫描并逐行字符串匹配;两张 10 万行表 JOIN 将触发 100 亿次比对,导致 CPU 与 I/O 严重过载。

为什么LIKE '%xxx%' 在 JOIN 中必然慢
因为无法使用普通 B+ 树索引:前导通配符 % 让优化器放弃索引扫描,只能走全表扫描 + 字符串逐行比对。两张表各 10 万行,笛卡尔积就是 100 亿次比对,CPU 和 I/O 都扛不住。
常见错误是试图给字段加普通索引后还写 ON t1.name LIKE CONCAT('%', t2.keyword, '%') —— 索引完全失效,执行计划里一定能看到 type: ALL 或 Extra: Using where。
- MySQL 5.6+ 的
FULLTEXT索引只支持MATCH ... AGAINST,不支持直接用于JOIN条件 - PostgreSQL 的
pg_trgm扩展可加速LIKE和正则,但需配合GIN索引和特定操作符(如%) - SQLite 没有原生全文索引 JOIN 支持,
FTS5表必须用子查询或视图“绕行”
MySQL 全文索引 + JOIN 的可行路径
不能直接 ON MATCH(t1.content) AGAINST(t2.keyword IN NATURAL LANGUAGE MODE),因为 MATCH 只接受字面量或列,不支持另一张表的字段作为搜索词。必须拆成两步:
- 先对大表(如
articles)建FULLTEXT(content)索引 - 用子查询把小表(如
keywords)的每个关键词单独查一次,再UNION ALL合并结果 - 或者改用
IN BOOLEAN MODE+ 应用层拼接+前缀,避免自然语言模式的停用词过滤干扰匹配
示例(关键词表仅 100 行以内适用):
SELECT a.id, a.title, k.word
FROM articles a
INNER JOIN (
SELECT id AS word_id, word FROM keywords
) k ON a.id IN (
SELECT article_id FROM (
SELECT id AS article_id FROM articles
WHERE MATCH(content) AGAINST(CONCAT('+', k.word, '*') IN BOOLEAN MODE)
) t
);
注意:CONCAT('+', k.word, '*') 是关键——+ 强制包含,* 支持词干匹配,否则单个词可能被忽略。
PostgreSQL 用 pg_trgm 实现模糊 JOIN 的实操要点
这是目前最接近“开箱即用”的方案,但必须严格满足三个条件,缺一不可:
- 安装扩展:
CREATE EXTENSION IF NOT EXISTS pg_trgm; - 在被搜索字段上建
GIN索引:CREATE INDEX idx_users_name_gin ON users USING GIN (name gin_trgm_ops); - JOIN 条件必须用
%操作符(不是LIKE):ON u.name % k.keyword
% 操作符会自动调用 similarity() 函数并按阈值(默认 0.3)判断是否“相似”。如果想调低匹配门槛,得显式设置:SET pg_trgm.similarity_threshold = 0.15;。这个值越低,召回越多,性能越差。
别用 similarity(u.name, k.keyword) > 0.2 写在 ON 里——它无法触发索引,只会退化为嵌套循环 + 全量计算。
真正要小心的边界情况
全文索引不是万能解药。当关键词含特殊字符(如 @、/)、长度 MATCH 直接返回空;PostgreSQL 的 pg_trgm 对超短字符串(如 “a”, “an”)相似度计算不稳定,容易误匹配。
更隐蔽的问题是数据倾斜:如果某个关键词(如 “test”)在大表中命中 50% 的行,那即使用了索引,JOIN 结果集膨胀也会拖垮后续聚合。这时得提前加 LIMIT 或引入采样逻辑,而不是硬扛。
实际部署前,务必用 EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL)或 EXPLAIN FORMAT=TREE(MySQL 8.0+)确认是否真走了索引扫描,而不是你以为的那样。










