1. 问题背景:为什么 LIKE '%abc' 查询慢如蜗牛?
最近在优化公司用户管理系统时,遇到了一个典型性能问题:运营同事需要导出所有Gmail邮箱用户,SQL语句SELECT * FROM users WHERE email LIKE '%@gmail.com'在百万级数据表上执行耗时超过8秒。这种后缀匹配查询(LIKE '%abc')是数据库性能的经典痛点。
根本原因在于B+树索引的工作机制。索引就像一本按字母顺序排列的字典,只能高效查找以特定字母开头的单词(前缀匹配)。当我们想查以"ing"结尾的单词时,传统索引完全失效,数据库只能逐行扫描整个表(Full Table Scan)。在千万级数据量下,这种查询可能耗时数分钟。
2. 核心原理:逆向思维的索引优化
2.1 B+树索引的局限性
B+树索引采用字典序排列,对于字符串"hello@domain.com",索引会按照h-e-l-l-o-@-d-o-m...的顺序建立。当使用LIKE '%domain.com'查询时,由于通配符在最前面,索引无法确定比较起点,只能放弃使用索引。
2.2 反向存储的魔法
解决方案简单却巧妙:既然索引擅长前缀匹配,我们就把字符串反转存储。例如:
- 原字符串:"hello@gmail.com"
- 反转存储:"moc.liamg@olleh"
现在查询@gmail.com结尾的记录,就变成了查询以"moc.liamg@"开头的反转字符串。这正是B+树索引最擅长的前缀匹配场景,索引利用率从0%提升到100%。
3. MySQL 5.7+ 的优雅实现方案
3.1 虚拟生成列(Generated Columns)
MySQL 5.7引入的虚拟列功能让我们无需修改业务代码就能实现这一优化。虚拟列的值由表达式自动计算,分为:
- VIRTUAL:仅计算不存储(默认)
- STORED:实际存储计算结果
对于我们的场景,使用VIRTUAL类型最为合适:
sql复制ALTER TABLE users
ADD COLUMN email_reverse VARCHAR(255)
GENERATED ALWAYS AS (REVERSE(email)) VIRTUAL;
3.2 索引创建与查询改写
创建反转列的索引后,查询语句需要相应调整:
sql复制-- 创建索引
CREATE INDEX idx_email_reverse ON users(email_reverse);
-- 优化后的查询
SELECT * FROM users
WHERE email_reverse LIKE REVERSE('@gmail.com') + '%';
注意:在MySQL中字符串连接应使用CONCAT函数,这里为示意简化写法。实际应为
LIKE CONCAT(REVERSE('@gmail.com'), '%')
4. 性能对比实测
在AWS RDS MySQL 5.7实例上(db.m5.large),对1000万条用户数据测试:
| 查询类型 | 执行时间 | 扫描行数 | 索引使用 |
|---|---|---|---|
| 原始LIKE '%@gmail.com' | 4.82s | 1000万 | 无 |
| 反向索引查询 | 0.02s | 532 | idx_email_reverse |
性能提升达240倍!EXPLAIN结果显示查询类型从ALL(全表扫描)变为range(索引范围扫描)。
5. 四大典型应用场景
5.1 用户身份验证
客服系统中,用户可能只提供手机号后4位验证身份:
sql复制-- 传统方式(全表扫描)
SELECT * FROM users WHERE phone LIKE '%8888';
-- 优化方案
CREATE INDEX idx_phone_reverse ON users(REVERSE(phone));
SELECT * FROM users WHERE REVERSE(phone) LIKE '8888%';
5.2 邮箱域名分析
统计不同邮箱域名的用户分布:
sql复制SELECT
SUBSTRING_INDEX(email, '@', -1) AS domain,
COUNT(*) AS user_count
FROM users
WHERE email_reverse LIKE REVERSE('@company.com') + '%'
GROUP BY domain;
5.3 文件类型检索
网盘系统按扩展名筛选文件:
sql复制-- 查找所有PDF文件
SELECT * FROM files
WHERE REVERSE(name) LIKE 'fdp.%';
5.4 交通管理系统
城市限行政策常基于车牌尾号:
sql复制-- 查找尾号为1或6的车辆
SELECT * FROM vehicles
WHERE REVERSE(plate_number) LIKE '1%'
OR REVERSE(plate_number) LIKE '6%';
6. 实现细节与注意事项
6.1 字符集与排序规则
如果字段包含多字节字符(如中文),需要确保字符集支持:
sql复制ALTER TABLE users
MODIFY email_reverse VARCHAR(255) CHARACTER SET utf8mb4
COLLATE utf8mb4_bin;
6.2 写性能影响评估
虽然VIRTUAL列不占存储空间,但索引维护会增加写操作开销。测试显示:
- 无索引:每秒插入约4500条
- 有反向索引:每秒插入约3800条
下降约15%,在可接受范围内。
6.3 查询优化技巧
对于固定长度的后缀查询(如身份证后6位),可以进一步优化:
sql复制-- 只存储反转后的后缀
ALTER TABLE users
ADD COLUMN id_card_suffix_reverse CHAR(6)
GENERATED ALWAYS AS (REVERSE(RIGHT(id_card, 6))) VIRTUAL;
CREATE INDEX idx_id_card_suffix ON users(id_card_suffix_reverse);
7. 局限性及替代方案
7.1 不适用场景
- 中间包含查询(LIKE '%abc%')
- 不定长度的复杂模式匹配
- 需要模糊匹配(如拼写纠正)的场景
7.2 替代技术选型
当反向索引方案不适用时,考虑:
- 全文索引(FULLTEXT):适合单词级别的搜索
- Elasticsearch:专业的文本搜索引擎
- 触发器维护冗余列:保持数据一致性的传统方案
8. 生产环境部署建议
- 灰度发布:先在从库测试,观察查询性能和资源消耗
- SQL审计:修改DAO层代码,自动转换后缀查询
- 注释规范:在SQL中添加提示注释
sql复制/* 使用反向索引优化后缀查询 */
SELECT * FROM users
WHERE email_reverse LIKE CONCAT(REVERSE('@gmail.com'), '%');
- 监控指标:重点关注
- 查询响应时间P99值
- 索引大小增长趋势
- 写操作延迟变化
我在实际项目中采用此方案后,原本超时的统计报表查询全部优化到毫秒级响应。一个特别有用的技巧是创建视图封装反转逻辑,使业务代码保持简洁:
sql复制CREATE VIEW v_user_email_search AS
SELECT id, name, email,
REVERSE(email) AS email_reverse
FROM users;
-- 业务查询只需
SELECT * FROM v_user_email_search
WHERE email_reverse LIKE 'moc.liamg@%';