1. 问题背景:为什么 LIKE '%abc' 慢到哭?
当我们在MySQL中执行 LIKE '%abc' 这类模糊查询时,经常会遇到性能瓶颈。这背后涉及数据库索引的工作原理——B+树索引是按照字段值的正向顺序构建的。当使用 LIKE 'abc%' 时,数据库可以利用索引的有序性快速定位到以"abc"开头的记录;但当使用 LIKE '%abc' 时,由于通配符在前,索引的有序性完全失效,导致必须进行全表扫描。
关键点:索引的最左匹配原则决定了
%在前时索引失效,这是B+树索引的固有特性。
2. 传统解决方案的局限性
常见的优化方案如全文索引、ES等外部搜索引擎都存在明显短板:
- 全文索引:占用空间大,维护成本高,且不支持所有字符集
- 外部搜索引擎:引入系统复杂度,存在数据同步延迟
- 函数索引:MySQL原生不支持,Oracle/PostgreSQL等数据库才有
我在实际项目中遇到一个典型案例:用户表有200万数据,WHERE nickname LIKE '%用户' 查询耗时超过3秒,严重影响了登录体验。
3. 反向存储大法的实现方案
3.1 基础版:手动创建反向列
sql复制-- 原始表结构
CREATE TABLE users (
id INT PRIMARY KEY,
nickname VARCHAR(50),
INDEX idx_nickname (nickname)
);
-- 添加反向列
ALTER TABLE users ADD COLUMN nickname_reverse VARCHAR(50);
UPDATE users SET nickname_reverse = REVERSE(nickname);
CREATE INDEX idx_nickname_reverse ON users(nickname_reverse);
-- 优化后的查询
SELECT * FROM users
WHERE nickname_reverse LIKE REVERSE('用户') + '%';
实测效果:200万数据下查询从3000ms降到30ms,提升100倍。
注意事项:
- 需要维护原始列和反向列的数据一致性
- 批量更新大表时建议分批次进行
- 字符串反转可能影响特殊字符(如emoji)
3.2 进阶版:MySQL虚拟列(5.7.6+)
sql复制-- 创建带虚拟列的表
CREATE TABLE users (
id INT PRIMARY KEY,
nickname VARCHAR(50),
nickname_reverse VARCHAR(50) GENERATED ALWAYS AS (REVERSE(nickname)) STORED,
INDEX idx_nickname_reverse (nickname_reverse)
);
-- 查询方式
EXPLAIN SELECT * FROM users
WHERE nickname_reverse LIKE CONCAT(REVERSE('用户'), '%');
虚拟列的优势:
- 自动维护反向逻辑,无需应用层干预
- STORED类型实际存储数据,查询性能更好
- 支持在线添加,不影响现有业务
4. 性能对比测试
使用200万测试数据对比不同方案的执行效率:
| 查询方式 | 执行时间(ms) | 是否走索引 |
|---|---|---|
| LIKE '%用户' | 3200 | ❌ |
| 手动反向列 | 35 | ✅ |
| 虚拟列(STORED) | 28 | ✅ |
| 虚拟列(VIRTUAL) | 45 | ✅ |
关键发现:
- 反向方案比原生LIKE快2个数量级
- STORED虚拟列性能最优
- 即使VIRTUAL虚拟列也比原生快70倍
5. 生产环境实施建议
5.1 存量数据迁移方案
sql复制-- 分批次更新(每次5万条)
SET @row = 0;
UPDATE users
SET nickname_reverse = REVERSE(nickname)
WHERE id BETWEEN @row AND @row + 50000;
SET @row = @row + 50001;
5.2 新数据写入方案
推荐使用触发器自动维护:
sql复制CREATE TRIGGER before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
SET NEW.nickname_reverse = REVERSE(NEW.nickname);
5.3 混合查询场景处理
当需要同时支持 LIKE '用户%' 和 LIKE '%用户' 时:
sql复制SELECT * FROM (
SELECT * FROM users WHERE nickname LIKE '用户%'
UNION ALL
SELECT * FROM users WHERE nickname_reverse LIKE REVERSE('用户') + '%'
) tmp;
6. 避坑指南
- 字符集问题:确保反向列与原列字符集一致,特别是utf8mb4
- 索引选择性:低区分度的字段(如性别)不适合此方案
- 复合索引:反向列可以参与复合索引,但要注意顺序
- 内存消耗:大批量更新时注意innodb_buffer_pool配置
我在电商项目中实施此方案时,曾因忽略字符集导致索引失效。后来通过以下检查SQL确认:
sql复制SHOW FULL COLUMNS FROM users LIKE 'nickname%';
7. 替代方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 反向存储 | 性能提升显著 | 需要额外存储 | 中大型表 |
| 全文索引 | 支持复杂搜索 | 占用空间大 | 文本搜索 |
| ES | 分布式扩展 | 运维复杂 | 海量数据 |
| 前缀索引 | 节省空间 | 功能有限 | 短字段 |
对于大多数MySQL场景,反向存储仍是性价比最高的方案。特别是当业务已经成型,无法调整查询方式时,这种方案对代码侵入性最小。
