作为后端开发工程师,我们每天都在和数据库打交道。记得刚入行时,我接手过一个订单查询接口优化任务——原本需要3秒的查询,在添加合适的索引后直接降到30毫秒。这种性能提升的震撼让我深刻认识到:索引不是可选项,而是数据库高效运作的生命线。今天,我将结合多年踩坑经验,带你深入MySQL索引的实战应用。
在开始优化前,我们需要建立科学的测试方法。以用户表为例(假设有500万条数据):
sql复制-- 创建测试表
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- 未建索引时查询
SELECT * FROM users WHERE phone = '13800138000';
在我的测试环境中,这个查询平均耗时2.3秒。通过EXPLAIN分析发现进行了全表扫描(type=ALL)。
sql复制-- 添加普通索引
ALTER TABLE users ADD INDEX idx_phone (phone);
-- 再次查询
SELECT * FROM users WHERE phone = '13800138000';
添加索引后,同样的查询仅需8毫秒!EXPLAIN显示使用了索引(type=ref)。这里有个关键细节:InnoDB的二级索引存储的是主键值,所以通过二级索引查询需要两次查找(回表),但依然比全表扫描高效得多。
注意:测试时务必关闭查询缓存(SET GLOBAL query_cache_size = 0),否则会干扰结果准确性
假设我们在用户表上创建联合索引:
sql复制ALTER TABLE users ADD INDEX idx_username_phone (username, phone);
这个索引的B+树结构会先按username排序,username相同时再按phone排序。就像电话簿先按姓氏排序,同姓再按名字排序。
完全匹配(索引生效):
sql复制SELECT * FROM users WHERE username = '张三' AND phone = '13800138000';
只使用最左列(索引部分生效):
sql复制SELECT * FROM users WHERE username = '张三';
跳过最左列(索引失效):
sql复制SELECT * FROM users WHERE phone = '13800138000';
不连续使用(部分失效):
sql复制SELECT * FROM users WHERE username = '张三' AND create_time > '2023-01-01';
在我的电商项目中,曾因不了解这个原则导致用户搜索接口性能下降。后来将WHERE条件的列顺序调整为与索引一致,QPS从200提升到1500。
sql复制-- 范围查询右侧的phone索引失效
SELECT * FROM users
WHERE username = '张三' AND phone > '13800000000' AND create_time = '2023-01-01';
解决方案:调整查询顺序或使用IN代替范围查询:
sql复制SELECT * FROM users
WHERE username = '张三' AND phone IN ('13800138000','13800138001') AND create_time = '2023-01-01';
sql复制-- 错误示例(索引失效)
SELECT * FROM users WHERE YEAR(create_time) = 2023;
-- 正确写法
SELECT * FROM users WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';
sql复制-- phone是varchar类型,用数字查询会导致索引失效
SELECT * FROM users WHERE phone = 13800138000;
-- 正确写法
SELECT * FROM users WHERE phone = '13800138000';
sql复制-- 头部模糊查询导致索引失效
SELECT * FROM users WHERE username LIKE '%张%';
-- 尾部模糊查询可以使用索引
SELECT * FROM users WHERE username LIKE '张%';
sql复制-- 如果create_time无索引,整个查询都不会使用索引
SELECT * FROM users WHERE phone = '13800138000' OR create_time = '2023-01-01';
-- 解决方案:改为UNION ALL
SELECT * FROM users WHERE phone = '13800138000'
UNION ALL
SELECT * FROM users WHERE create_time = '2023-01-01' AND phone != '13800138000';
当MySQL优化器发现使用索引需要回表大量数据时(通常超过30%),会放弃使用索引。这时可以考虑:
覆盖索引是指查询的列都包含在索引中,无需回表:
sql复制-- 创建包含更多字段的联合索引
ALTER TABLE users ADD INDEX idx_covering (username, phone, create_time);
-- 使用覆盖索引
SELECT username, phone FROM users WHERE username = '张三' AND phone = '13800138000';
在我的日志分析系统中,使用覆盖索引使查询速度提升了10倍,因为避免了大量的随机IO。
对于长文本字段(如地址),可以使用前缀索引:
sql复制-- 计算合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(address, 5))/COUNT(*) AS selectivity5,
COUNT(DISTINCT LEFT(address, 10))/COUNT(*) AS selectivity10
FROM users;
-- 创建前缀索引
ALTER TABLE users ADD INDEX idx_address (address(10));
经验值:选择性(selectivity)应大于0.3,且前缀长度不宜过长(通常不超过20个字符)。
| 对比维度 | 联合索引 | 单列索引 |
|---|---|---|
| 存储空间 | 更节省 | 占用更多 |
| 查询条件组合 | 必须遵循最左前缀 | 灵活但效率低 |
| 覆盖索引可能性 | 更高 | 较低 |
| 维护成本 | 写入时维护一个索引 | 需要维护多个索引 |
在订单系统中,我们将原本5个单列索引合并为2个联合索引,不仅提升了查询性能,还减少了60%的索引存储空间。
原始查询:
sql复制SELECT * FROM products
WHERE category_id = 5
AND price BETWEEN 100 AND 500
AND status = 1
ORDER BY sales_volume DESC
LIMIT 20;
优化步骤:
最终方案:
sql复制SELECT id, name, price FROM products
WHERE category_id = 5
AND status = 1
AND price BETWEEN 100 AND 500
ORDER BY sales_volume DESC
LIMIT 20;
难点:需要按时间倒序且过滤多种条件
解决方案:
sql复制-- 第一页
SELECT * FROM feeds
WHERE time_created <= NOW()
AND topic_id = 8
ORDER BY time_created DESC
LIMIT 20;
-- 下一页(使用上一页最后一条的时间作为游标)
SELECT * FROM feeds
WHERE time_created < '2023-06-01 12:00:00'
AND topic_id = 8
ORDER BY time_created DESC
LIMIT 20;
sql复制-- 查看未使用的索引
SELECT * FROM sys.schema_unused_indexes;
-- 查看索引统计信息
SHOW INDEX FROM users;
sql复制-- 查看表碎片情况
SELECT table_name, data_free/1024/1024 AS frag_mb
FROM information_schema.tables
WHERE data_free > 0;
-- 优化表(会锁表)
OPTIMIZE TABLE users;
建议在业务低峰期定期(如每周)对核心表进行优化。
记得有一次我接手一个系统,发现有个表建了20多个索引,删除冗余索引后写入性能提升了8倍。索引就像盐——适量提鲜,过量坏事。