本文目录导读:

在PHP项目(或任何数据库项目)中使用 LIKE 查询时,如果模式以 开头(%keyword 或 %keyword%),数据库索引通常会失效,导致全表扫描,这是由 B+树索引的物理存储结构决定的。
核心原因:B+树索引的“最左前缀”原则
大多数关系型数据库(MySQL、PostgreSQL等)的索引默认使用 B+树 结构。
- B+树索引的工作方式:索引是按照从左到右的顺序对数据进行排序和存储的,当执行查询时,数据库会从索引树的根节点开始,根据值的前缀逐步向下查找。
- LIKE 'keyword%'(尾匹配)为何能走索引:
LIKE 'abc%'相当于要求“所有以abc开头的值”,数据库引擎可以拿着abc这个前缀,直接在B+树中进行范围扫描(从abc开始,到abd结束),这本质上是等值查询 + 范围查询,完全符合最左前缀原则。 - LIKE '%keyword'(首匹配)为何不走索引:
LIKE '%abc'相当于“所有以abc结尾的值”,数据库无法确定前缀是什么,索引中可能存储了xabc、yabc、zabc,要找到所有以abc结尾的记录,数据库必须遍历整个索引树的叶子节点,逐一检查每个值是否以abc遍历整个索引通常比全表扫描更慢(因为索引回表查询的额外开销),所以优化器会选择直接全表扫描。 - LIKE '%keyword%'(模糊匹配)为何不走索引:同理,数据库不知道开头是什么,也无法利用B+树的有序性进行快速定位,它只能全索引扫描或全表扫描。
特殊情况(MySQL为例)
在特定条件下,LIKE '%keyword' 可能利用 覆盖索引 或 索引条件下推。
-
覆盖索引(Covering Index):
- 如果查询的所有字段都包含在同一个索引中,且查询只返回这些字段,数据库可能仍然会扫描整个索引,但不会回表,这比全表扫描快一些,但本质仍是全索引扫描(慢)。
SELECT id FROM users WHERE name LIKE '%abc'(假设id和name都在联合索引中)。
-
索引条件下推(ICP,Index Condition Pushdown,MySQL 5.6+):
- 对于 InnoDB 存储引擎,
LIKE '%keyword'虽然无法利用索引的排序特性,但 MySQL 可以将WHERE条件直接推送到存储引擎层,存储引擎在扫描索引的过程中,会提前过滤掉不符合LIKE '%abc'条件的行,减少回表次数,但索引本身仍然需要全扫描。
- 对于 InnoDB 存储引擎,
总结一句话: 在开头时,数据库无法从B+树根节点确定搜索的起始位置,只能暴力扫描,所以不走索引。
解决方案
如果必须使用首尾模糊查询且需要高性能,可以考虑以下方案:
| 方案 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 全文索引(Full-Text Index) | 使用倒排索引(Inverted Index),专门为模糊搜索/分词搜索设计,语法:MATCH (col) AGAINST ('keyword') |
大量文本搜索、搜索引擎类型功能 | 只支持 MyISAM/InnoDB;对中文需要分词器;不支持精确前缀匹配 |
| 反向索引(Reverse Index) | 存储字段的反转字符串,查询 LIKE '%abc' 时,改为 LIKE 'cba%' 查反向字段。 |
常用后缀查询(如邮箱域名搜索 @gmail.com、路径搜索) |
需要维护反转后的字段;不支持中间匹配 |
| ES / Sphinx | 外部搜索引擎(Elasticsearch、Sphinx),专门做模糊查询和分词。 | 大数据量、高并发、复杂模糊搜索 | 增加系统复杂度,需要额外运维 |
| 变更业务逻辑 | 将 %keyword% 改为 keyword%(前缀匹配)。 |
很多场景下,用户习惯从左到右输入(如商品名、用户名) | 限制用户输入习惯,可能无法满足需求 |
| 使用 Like 加覆盖索引 | 确保查询字段(WHERE name LIKE '%abc')和 SELECT 字段全被覆盖。 |
非常小的数据量或表中只有少数几行 | 不通用,数据量大时性能仍差 |
最佳实践建议
- 优先使用前缀匹配:
LIKE 'keyword%'而非%keyword%。 - 考虑全文索引:对文本搜索需求,使用
FULLTEXT INDEX。 - 数据量较小时(万级别):即使
%keyword%全表扫描,在内存足够的情况下,MySQL 也能很快响应,不要过早优化。 - 数据量大时:务必使用外部搜索引擎或变更为可索引的查询方式。
LIKE '%keyword%' 不走索引是由B+树的内存结构决定的,不是bug,解决方案取决于你的具体业务场景和数据量。