在日常开发中,我们经常会遇到需要根据字符串后缀进行查询的场景。比如查找所有Gmail邮箱用户(WHERE email LIKE '%@gmail.com'),或者查询尾号为1234的手机号(WHERE phone LIKE '%1234')。这类查询在数据量小的时候可能表现尚可,但当数据量达到百万级时,查询速度就会急剧下降。
关键问题:B+树索引是按照从左到右的顺序构建的,当使用LIKE '%abc'这样的查询条件时,由于通配符在最左侧,索引无法发挥作用,数据库只能进行全表扫描(Full Table Scan)。
我曾在实际项目中遇到过这样的案例:一个用户表有800万条记录,查询尾号为8888的手机号需要8-12秒才能返回结果。通过EXPLAIN分析发现,type显示为ALL,表示确实在进行全表扫描。
B+树索引之所以高效,是因为它按照字典序组织数据。以字符串"apple"、"banana"、"cherry"为例:
反向存储的核心思想是将字符串倒置后存储:
此时,查询以'e'结尾的原始字符串,就变成了查询以'e'开头的反向字符串:
sql复制-- 原始查询(无法使用索引)
SELECT * FROM fruits WHERE name LIKE '%e';
-- 优化后查询(可以使用索引)
SELECT * FROM fruits WHERE reversed_name LIKE 'e%';
这种转换使得原本无法使用索引的后缀查询,变成了可以利用索引的前缀查询。
MySQL 5.7引入的虚拟生成列功能,让我们无需修改应用层代码就能实现反向存储:
sql复制-- 原始表结构
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100),
phone VARCHAR(20)
);
-- 添加虚拟列
ALTER TABLE users
ADD COLUMN email_reverse VARCHAR(100)
GENERATED ALWAYS AS (REVERSE(email)) VIRTUAL;
-- 为虚拟列创建索引
CREATE INDEX idx_email_reverse ON users(email_reverse);
虚拟列的特点:
优化前后的查询对比:
sql复制-- 优化前(全表扫描)
EXPLAIN SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- 优化后(使用索引)
EXPLAIN SELECT * FROM users WHERE email_reverse LIKE REVERSE('@gmail.com')+'%';
实测效果:
需求:客服系统中,用户只提供了手机号后4位"8888"进行身份验证。
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%';
需求:统计使用企业邮箱(@company.com)的注册用户。
sql复制-- 优化方案
ALTER TABLE users
ADD COLUMN email_reverse VARCHAR(100) AS (REVERSE(email)) VIRTUAL,
ADD INDEX idx_email_reverse (email_reverse);
SELECT COUNT(*) FROM users
WHERE email_reverse LIKE REVERSE('@company.com')+'%';
需求:网盘系统中筛选所有.jpg文件。
sql复制-- 文件表结构
CREATE TABLE files (
id INT PRIMARY KEY,
name VARCHAR(255),
name_reverse VARCHAR(255) AS (REVERSE(name)) VIRTUAL,
INDEX idx_name_reverse (name_reverse)
);
-- 查询jpg文件
SELECT * FROM files WHERE name_reverse LIKE 'gpj.%';
需求:找出尾号为1或6的车辆进行限行。
sql复制-- 车辆表结构
CREATE TABLE vehicles (
plate_number VARCHAR(20),
plate_reverse VARCHAR(20) AS (REVERSE(plate_number)) VIRTUAL,
INDEX idx_plate_reverse (plate_reverse)
);
-- 查询尾号1或6的车辆
SELECT * FROM vehicles
WHERE plate_reverse LIKE '1%' OR plate_reverse LIKE '6%';
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 应用层反转 | 兼容所有MySQL版本 | 需要修改业务代码 | 老旧系统改造 |
| 触发器维护 | 自动保持数据一致 | 增加写操作开销 | 需要实时性 |
| 虚拟生成列 | 无需修改业务代码 | 需MySQL 5.7+ | 新建系统首选 |
在800万记录的users表上测试:
| 查询类型 | 无索引耗时 | 反向索引耗时 | 提升倍数 |
|---|---|---|---|
| LIKE '%@gmail.com' | 12.3s | 0.05s | 246x |
| LIKE '%8888' | 8.7s | 0.03s | 290x |
| LIKE '%.jpg' | 6.2s | 0.04s | 155x |
虚拟列本身不占用存储空间,但需要注意:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 反向索引 | 实现简单,实时性好 | 仅支持后缀查询 | 后缀精确匹配 |
| 全文索引 | 支持复杂模式匹配 | 配置复杂,占用空间大 | 内容搜索 |
| Elasticsearch | 高性能全文检索 | 维护成本高,非实时 | 搜索系统 |
| 冗余存储 | 灵活性强 | 数据一致性难保证 | 特殊业务需求 |
问题1:虚拟列导致写操作变慢
问题2:查询语句可读性差
java复制public List<User> findByEmailSuffix(String suffix) {
String reversed = new StringBuilder(suffix).reverse().toString();
// 执行SQL: WHERE email_reverse LIKE :reversed+'%'
}
问题3:复合查询索引失效
反向存储只是字符串查询优化的一个特例。在实际开发中,我们还可以考虑:
我曾经在一个电商项目中,通过组合反向存储和前缀压缩技术,将商品SKU查询性能提升了300倍。关键是在设计阶段就充分考虑查询模式,而不是等问题出现后再补救。