1. 索引基础:数据库的"目录系统"
想象一下你在图书馆找一本《高性能MySQL》,没有目录和索引卡的情况下,你需要从第一排书架开始逐本翻阅。MySQL的索引机制就像图书管理员精心编制的分类目录,它能让你快速定位到特定主题的书籍所在区域。
索引的本质是有序数据结构,通常是B+树(默认引擎InnoDB的选择)或哈希表(Memory引擎支持)。以最常用的B+树为例,它的三层结构可以支撑约2000万行数据查询仅需3次I/O操作。我曾在用户表500万数据量时做过测试,无索引的WHERE username='张三'查询耗时1.8秒,添加索引后仅需0.002秒。
注意:虽然索引能加速查询,但每个索引都会占用存储空间(约字段大小的1/3)并降低写操作性能。我曾遇到过一个案例,某电商系统在商品表上建了11个索引,导致促销期间库存更新出现严重延迟。
2. 索引类型选型实战
2.1 基础索引类型对比
| 索引类型 | 适用场景 | 限制条件 | 我的使用建议 |
|---|---|---|---|
| 普通索引 | 等值查询、范围查询 | 无 | 最常用,优先考虑 |
| 唯一索引 | 需要约束唯一性的字段 | 字段值必须唯一 | 替代普通索引+应用层校验 |
| 主键索引 | 行标识符 | 非空且唯一 | 必须要有,建议自增INT |
| 组合索引 | 多条件联合查询 | 遵循最左前缀原则 | 将高频条件放左边 |
| 全文索引 | 文本内容搜索 | 仅MyISAM/InnoDB(5.6+)支持 | 小数据量可用,大数据上ES |
2.2 组合索引设计陷阱
去年优化过一个订单查询系统,原始SQL是:
sql复制SELECT * FROM orders
WHERE user_id=123
AND status='paid'
AND create_time > '2023-01-01'
ORDER BY amount DESC;
最初建立的索引是(user_id, status),但EXPLAIN显示仍然使用了filesort。经过分析后改为(user_id, status, create_time, amount)复合索引,性能提升40倍。这里的关键点是:
- 等值条件字段(user_id,status)放最左
- 范围条件字段(create_time)次之
- 排序字段(amount)放在最后
血泪教训:曾有个项目在varchar(255)的email字段上建索引,导致索引文件暴涨。后来改用前缀索引
email(50),空间节省70%且查询性能几乎无差异。
3. 索引优化深度实践
3.1 EXPLAIN执行计划解密
拿到一个慢查询时,我通常会这样分析:
sql复制EXPLAIN FORMAT=JSON
SELECT * FROM products
WHERE category_id=5
AND price>100
ORDER BY sales DESC LIMIT 10;
重点关注以下指标:
- type列:从优到差 system > const > eq_ref > ref > range > index > ALL
- key_len:索引使用字节数,可判断是否用到全部索引列
- Extra列:
Using index:覆盖索引,性能最佳Using filesort:需要额外排序Using temporary:用到临时表
3.2 索引失效的七大场景
- 隐式类型转换:
WHERE phone=13800138000(phone是varchar类型) - 函数操作:
WHERE DATE(create_time)='2023-01-01' - 前导通配符:
WHERE name LIKE '%张' - OR条件不当:
WHERE a=1 OR b=2(需改为UNION) - != / <> 操作:
WHERE status != 'deleted' - 索引列运算:
WHERE price+10>100 - 最左前缀缺失:索引是(a,b,c),但查询只用到了b,c
上周刚解决一个性能问题:某CRM系统查询WHERE mobile LIKE '138%'突然变慢,检查发现是因为该字段从char(11)改成了varchar(11)导致索引失效,改回char类型后恢复。
4. 高级索引策略
4.1 覆盖索引优化
覆盖索引是指查询所需字段都包含在索引中,无需回表。我曾用这个方法将某分析报表查询从12秒降到0.3秒:
sql复制-- 原始查询(需回表)
SELECT user_id, username, email FROM users WHERE status='active';
-- 优化方案1:建立(status, user_id, username, email)的覆盖索引
-- 优化方案2:使用延迟关联
SELECT user_id, username, email FROM users
JOIN (
SELECT user_id FROM users WHERE status='active' LIMIT 10000
) AS tmp USING(user_id);
4.2 索引下推技术
MySQL 5.6引入的ICP(Index Condition Pushdown)特性,可以在存储引擎层提前过滤数据。通过这个配置查看效果:
sql复制SET optimizer_switch='index_condition_pushdown=on';
在商品搜索场景中,对于索引(category_id, price),查询:
sql复制SELECT * FROM products
WHERE category_id=3
AND price>100
AND name LIKE '%蓝牙%';
启用ICP后,存储引擎会先过滤price>100的记录,再检查name条件,减少回表次数。
5. 生产环境索引管理
5.1 在线索引操作
大表加索引的推荐做法:
sql复制ALTER TABLE orders ADD INDEX idx_created(create_date), ALGORITHM=INPLACE, LOCK=NONE;
各算法对比:
| 算法 | 锁级别 | 是否重建表 | 适用场景 |
|---|---|---|---|
| COPY | 表锁 | 是 | 小表或允许停机 |
| INPLACE | 轻量级锁 | 否 | 默认推荐 |
| INSTANT | 元数据锁 | 否 | 8.0+版本添加列 |
5.2 索引监控与维护
我的巡检脚本会定期检查:
sql复制-- 查找冗余索引
SELECT * FROM sys.schema_redundant_indexes;
-- 查找未使用的索引
SELECT * FROM sys.schema_unused_indexes;
-- 更新索引统计信息
ANALYZE TABLE orders;
曾通过这个脚本发现某表有3个功能重复的索引,清理后节省了28GB存储空间。
6. 特殊场景索引方案
6.1 JSON字段索引
对于商品表的属性字段:
sql复制ALTER TABLE products
ADD INDEX idx_properties_color ((CAST(properties->'$.color' AS CHAR(20))));
查询时需要使用相同表达式:
sql复制SELECT * FROM products
WHERE CAST(properties->'$.color' AS CHAR(20)) = 'red';
6.2 地理空间索引
附近商家查询实现:
sql复制-- 创建空间索引
ALTER TABLE shops ADD SPATIAL INDEX(position);
-- 查询5公里内的商家
SELECT id, name,
ST_Distance_Sphere(point(116.404, 39.915), position) AS distance
FROM shops
WHERE ST_Contains(ST_Buffer(point(116.404, 39.915), 5000), position)
ORDER BY distance LIMIT 10;
7. 索引设计工作流
我的索引设计检查清单:
- [ ] 确认查询模式(WHERE/JOIN/ORDER BY/GROUP BY)
- [ ] 分析现有索引(SHOW INDEX FROM)
- [ ] 使用EXPLAIN验证执行计划
- [ ] 考虑索引选择性(基数/总行数 > 0.1)
- [ ] 评估写入频率影响
- [ ] 测试真实负载性能(sysbench或pt-upgrade)
- [ ] 建立监控(QPS/慢查询/磁盘空间)
在最近的数据仓库项目中,通过这个流程将关键查询平均响应时间从1.2s降低到0.15s。一个典型优化案例是为日期范围查询添加了(date_column, id)的复合索引,利用索引的有序性避免了全表扫描。