作为一名长期奋战在一线的数据库工程师,我深知索引对MySQL性能的关键作用。今天,我将结合多年实战经验,系统性地剖析MySQL索引的底层原理、核心类型和优化策略,帮助大家彻底掌握这一数据库性能优化的利器。
索引本质上是一种空间换时间的数据结构,它通过预先组织数据的关键信息,为数据库查询提供快速访问路径。就像图书馆的目录系统,让你无需遍历整个书架就能快速找到目标书籍。
在MySQL的InnoDB引擎中,索引主要采用B+树结构实现。这种数据结构具有以下核心特性:
索引的核心价值体现在:
但索引并非没有代价:
| 类型 | 核心特点 | 适用场景 |
|---|---|---|
| B+树索引 | 有序、平衡树,支持范围查询 | 绝大多数场景(默认索引类型) |
| 哈希索引 | 等值查询O(1),不支持范围/排序 | Memory引擎等精确匹配场景 |
| 全文索引 | 支持文本关键词匹配(MATCH AGAINST) | 文章内容、商品描述等文本搜索 |
| R-Tree索引 | 支持空间数据查询 | 地理信息系统(GIS) |
B+树索引的存储细节:
| 类型 | 创建语法示例 | 核心特性 |
|---|---|---|
| 主键索引 | PRIMARY KEY(id) | 唯一且非空,InnoDB的聚簇索引 |
| 唯一索引 | UNIQUE KEY(phone) | 值唯一但允许NULL |
| 普通索引 | INDEX idx_name(name) | 基础索引,无约束 |
| 复合索引 | INDEX idx_a_b(a,b) | 多列组合,遵循最左前缀原则 |
| 覆盖索引 | 查询字段全在索引中 | 避免回表,性能最佳 |
主键设计建议:
复合索引idx_a_b_c(a,b,c)的生效规则:
sql复制-- ✅ 完全生效
WHERE a=1 AND b=2 AND c=3
WHERE a=1 AND b=2
WHERE a=1
-- ⚠️ 部分生效
WHERE a=1 AND c=3 -- 只用a列
WHERE b=2 AND c=3 -- 索引失效
-- ❌ 完全失效
WHERE b=2
WHERE c=3
WHERE a LIKE '%x' -- 前导通配符
优化器自动调整:
sql复制WHERE b=2 AND a=1 -- 会被优化为a=1 AND b=2
实战案例:
sql复制-- 高频查询
SELECT id,name FROM users WHERE age=20 AND city='北京' ORDER BY create_time;
-- 最优索引
INDEX idx_age_city_time(age, city, create_time)
MySQL 8.0引入的索引跳跃扫描(Index Skip Scan)特性,可以在特定条件下突破最左前缀限制:
sql复制-- 表有索引idx_gender_age(gender, age)
SELECT * FROM users WHERE age=20;
-- 8.0+可能转化为
SELECT * FROM users WHERE gender IN ('M','F') AND age=20;
覆盖索引的判断标准:
性能对比测试:
sql复制-- 测试表
CREATE TABLE perf_test (
id INT PRIMARY KEY,
a INT, b INT, c VARCHAR(100),
INDEX idx_a_b(a,b)
) ENGINE=InnoDB;
-- 案例1:覆盖索引(0.5ms)
EXPLAIN SELECT a,b FROM perf_test WHERE a=1 AND b=2;
-- Extra: Using index
-- 案例2:回表查询(15ms)
EXPLAIN SELECT * FROM perf_test WHERE a=1 AND b=2;
-- Extra: NULL
索引下推(Index Condition Pushdown)的工作流程:
存储引擎层:
服务器层:
ICP生效条件:
sql复制-- 启用ICP(默认开启)
SET optimizer_switch='index_condition_pushdown=on';
-- 检查是否使用ICP
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age=20;
-- Extra: Using index condition
InnoDB的表数据本身就是按主键组织的聚簇索引,这种设计带来以下特性:
数据访问路径:
物理存储特点:
页分裂问题:
当插入无序主键时,可能导致页分裂和空间浪费:
sql复制-- 可能引发页分裂的插入模式
INSERT INTO t VALUES ('z100', ...);
INSERT INTO t VALUES ('a200', ...);
INSERT INTO t VALUES ('m150', ...);
减少回表操作的实用方法:
sql复制-- 回表代价示例
SELECT * FROM users WHERE name LIKE '张%'; -- 可能需要回表多次
-- 优化为批量查询
SELECT * FROM users WHERE id IN (
SELECT id FROM users WHERE name LIKE '张%' LIMIT 100
);
选择性计算公式:
code复制选择性 = 不重复的索引值数量 / 总记录数
前缀索引优化:
sql复制-- 长文本字段的前缀索引
ALTER TABLE articles ADD INDEX idx_title(title(20));
-- 计算合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(title,10))/COUNT(*) AS sel10,
COUNT(DISTINCT LEFT(title,20))/COUNT(*) AS sel20
FROM articles;
sql复制-- phone是varchar类型
SELECT * FROM users WHERE phone=13800138000; -- 索引失效
sql复制SELECT * FROM users WHERE DATE(create_time)='2023-01-01'; -- 索引失效
sql复制SELECT * FROM users WHERE a=1 OR b=2; -- 除非a和b都有索引
sql复制SELECT * FROM users WHERE status != 1; -- 通常全表扫描
查看索引使用情况:
sql复制-- 通过performance_schema
SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA='db_name';
-- 通过SHOW STATUS
SHOW STATUS LIKE 'Handler_read%';
定期索引维护:
sql复制-- 重建索引(InnoDB)
ALTER TABLE tbl_name ENGINE=InnoDB;
-- 分析索引统计信息
ANALYZE TABLE tbl_name;
InnoDB会自动为频繁访问的索引页建立哈希索引,特性包括:
监控AHI使用:
sql复制SHOW ENGINE INNODB STATUS\G
-- 在HASH部分查看使用情况
MySQL 8.0+支持索引列倒序存储:
sql复制-- 创建倒序索引
CREATE INDEX idx_name_desc ON users(name DESC);
-- 混合排序索引
CREATE INDEX idx_a_asc_b_desc ON t(a ASC, b DESC);
MySQL 8.0+支持标记索引为不可见(测试删除索引的影响):
sql复制-- 设置为不可见
ALTER TABLE users ALTER INDEX idx_name INVISIBLE;
-- 优化器忽略但保持维护
SET optimizer_switch='use_invisible_indexes=off'; -- 默认
sql复制SELECT * FROM orders o
JOIN customers c ON o.cust_id=c.id
JOIN products p ON o.prod_id=p.id
WHERE c.region='EAST' AND p.category='ELECTRONICS';
-- 推荐索引:
-- customers: INDEX(region,id)
-- products: INDEX(category,id)
-- orders: INDEX(cust_id,prod_id)
sql复制-- 低效写法
SELECT * FROM logs ORDER BY id LIMIT 10000, 20;
-- 优化写法(利用索引覆盖)
SELECT * FROM logs WHERE id >= (SELECT id FROM logs ORDER BY id LIMIT 10000,1) LIMIT 20;
sql复制-- 按月分区的表
CREATE TABLE events (
id INT,
event_time DATETIME,
data VARCHAR(100),
PRIMARY KEY(id,event_time)
) PARTITION BY RANGE (TO_DAYS(event_time)) (
PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01'))
);
-- 查询时自动分区裁剪
SELECT * FROM events WHERE event_time BETWEEN '2023-01-15' AND '2023-01-20';
在实际工作中,我经常使用这些技术来优化生产环境的数据库性能。特别是在处理亿级数据表时,合理的索引设计往往能将查询时间从分钟级降到毫秒级。记住,索引不是越多越好,而是要根据实际查询模式精心设计。