MySQL索引详解
MySQL索引详解
概述
索引是数据库中用于快速查找数据的数据结构。MySQL中的索引主要基于B+树实现,能够显著提高查询性能,但也会增加存储空间和维护成本。
索引类型
按数据结构分类
B+树索引
- 特点:MySQL默认索引类型,支持范围查询
- 适用场景:大部分查询场景
- 存储引擎:InnoDB、MyISAM
sql
-- 创建B+树索引
CREATE INDEX idx_user_name ON users(name);
-- 查看索引信息
SHOW INDEX FROM users;
-- 创建B+树索引
CREATE INDEX idx_user_name ON users(name);
-- 查看索引信息
SHOW INDEX FROM users;
哈希索引
- 特点:等值查询速度快,不支持范围查询
- 适用场景:等值查询为主的场景
- 存储引擎:Memory、NDB
sql
-- Memory引擎默认使用哈希索引
CREATE TABLE hash_table (
id INT,
name VARCHAR(50),
INDEX USING HASH (name)
) ENGINE=MEMORY;
-- Memory引擎默认使用哈希索引
CREATE TABLE hash_table (
id INT,
name VARCHAR(50),
INDEX USING HASH (name)
) ENGINE=MEMORY;
全文索引
- 特点:支持文本搜索
- 适用场景:文本检索
- 存储引擎:InnoDB、MyISAM
sql
-- 创建全文索引
CREATE FULLTEXT INDEX idx_content ON articles(content);
-- 使用全文索引
SELECT * FROM articles
WHERE MATCH(content) AGAINST('MySQL 索引' IN NATURAL LANGUAGE MODE);
-- 创建全文索引
CREATE FULLTEXT INDEX idx_content ON articles(content);
-- 使用全文索引
SELECT * FROM articles
WHERE MATCH(content) AGAINST('MySQL 索引' IN NATURAL LANGUAGE MODE);
按字段数量分类
单列索引
sql
-- 单列索引
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_order_date ON orders(order_date);
-- 单列索引
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_order_date ON orders(order_date);
复合索引(多列索引)
sql
-- 复合索引
CREATE INDEX idx_user_age_city ON users(age, city);
CREATE INDEX idx_order_user_date ON orders(user_id, order_date, status);
-- 复合索引
CREATE INDEX idx_user_age_city ON users(age, city);
CREATE INDEX idx_order_user_date ON orders(user_id, order_date, status);
按功能分类
主键索引
sql
-- 主键自动创建聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
email VARCHAR(100)
);
-- 主键自动创建聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
email VARCHAR(100)
);
唯一索引
sql
-- 唯一索引
CREATE UNIQUE INDEX idx_user_email ON users(email);
-- 或者在创建表时定义
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE
);
-- 唯一索引
CREATE UNIQUE INDEX idx_user_email ON users(email);
-- 或者在创建表时定义
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE
);
普通索引
sql
-- 普通索引
CREATE INDEX idx_user_name ON users(name);
-- 普通索引
CREATE INDEX idx_user_name ON users(name);
索引原理
B+树结构
特点
- 非叶子节点:只存储键值,不存储数据
- 叶子节点:存储完整数据,通过指针连接
- 有序性:叶子节点按键值有序排列
java
// B+树节点结构示意
public class BPlusTreeNode {
private boolean isLeaf;
private List<Integer> keys;
private List<BPlusTreeNode> children; // 非叶子节点
private List<Record> records; // 叶子节点
private BPlusTreeNode next; // 叶子节点链表
}
// B+树节点结构示意
public class BPlusTreeNode {
private boolean isLeaf;
private List<Integer> keys;
private List<BPlusTreeNode> children; // 非叶子节点
private List<Record> records; // 叶子节点
private BPlusTreeNode next; // 叶子节点链表
}
查询过程
sql
-- 查询示例
SELECT * FROM users WHERE id = 100;
-- B+树查询步骤:
-- 1. 从根节点开始,比较键值
-- 2. 根据比较结果选择子节点
-- 3. 重复步骤1-2,直到叶子节点
-- 4. 在叶子节点中找到目标记录
-- 查询示例
SELECT * FROM users WHERE id = 100;
-- B+树查询步骤:
-- 1. 从根节点开始,比较键值
-- 2. 根据比较结果选择子节点
-- 3. 重复步骤1-2,直到叶子节点
-- 4. 在叶子节点中找到目标记录
聚簇索引 vs 非聚簇索引
聚簇索引(主键索引)
sql
-- InnoDB中主键就是聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
-- 数据按主键顺序物理存储
-- 叶子节点直接存储完整行数据
-- InnoDB中主键就是聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
-- 数据按主键顺序物理存储
-- 叶子节点直接存储完整行数据
非聚簇索引(二级索引)
sql
-- 二级索引
CREATE INDEX idx_user_name ON users(name);
-- 叶子节点存储索引键值 + 主键值
-- 需要回表查询获取完整数据
-- 二级索引
CREATE INDEX idx_user_name ON users(name);
-- 叶子节点存储索引键值 + 主键值
-- 需要回表查询获取完整数据
回表查询
sql
-- 会发生回表的查询
SELECT * FROM users WHERE name = 'John';
-- 查询过程:
-- 1. 在name索引中找到'John'对应的主键id
-- 2. 根据主键id在聚簇索引中查找完整记录(回表)
-- 避免回表的查询(覆盖索引)
SELECT id, name FROM users WHERE name = 'John';
-- 会发生回表的查询
SELECT * FROM users WHERE name = 'John';
-- 查询过程:
-- 1. 在name索引中找到'John'对应的主键id
-- 2. 根据主键id在聚簇索引中查找完整记录(回表)
-- 避免回表的查询(覆盖索引)
SELECT id, name FROM users WHERE name = 'John';
索引优化
覆盖索引
sql
-- 创建覆盖索引
CREATE INDEX idx_user_name_email ON users(name, email);
-- 使用覆盖索引(无需回表)
SELECT name, email FROM users WHERE name = 'John';
-- 查看执行计划
EXPLAIN SELECT name, email FROM users WHERE name = 'John';
-- Extra: Using index
-- 创建覆盖索引
CREATE INDEX idx_user_name_email ON users(name, email);
-- 使用覆盖索引(无需回表)
SELECT name, email FROM users WHERE name = 'John';
-- 查看执行计划
EXPLAIN SELECT name, email FROM users WHERE name = 'John';
-- Extra: Using index
最左前缀原则
sql
-- 复合索引
CREATE INDEX idx_abc ON table_name(a, b, c);
-- 能使用索引的查询
SELECT * FROM table_name WHERE a = 1; -- 使用索引
SELECT * FROM table_name WHERE a = 1 AND b = 2; -- 使用索引
SELECT * FROM table_name WHERE a = 1 AND b = 2 AND c = 3; -- 使用索引
SELECT * FROM table_name WHERE a = 1 AND c = 3; -- 部分使用索引
-- 不能使用索引的查询
SELECT * FROM table_name WHERE b = 2; -- 不使用索引
SELECT * FROM table_name WHERE c = 3; -- 不使用索引
SELECT * FROM table_name WHERE b = 2 AND c = 3; -- 不使用索引
-- 复合索引
CREATE INDEX idx_abc ON table_name(a, b, c);
-- 能使用索引的查询
SELECT * FROM table_name WHERE a = 1; -- 使用索引
SELECT * FROM table_name WHERE a = 1 AND b = 2; -- 使用索引
SELECT * FROM table_name WHERE a = 1 AND b = 2 AND c = 3; -- 使用索引
SELECT * FROM table_name WHERE a = 1 AND c = 3; -- 部分使用索引
-- 不能使用索引的查询
SELECT * FROM table_name WHERE b = 2; -- 不使用索引
SELECT * FROM table_name WHERE c = 3; -- 不使用索引
SELECT * FROM table_name WHERE b = 2 AND c = 3; -- 不使用索引
索引下推
sql
-- 索引下推优化
CREATE INDEX idx_name_age ON users(name, age);
-- 查询
SELECT * FROM users WHERE name LIKE 'J%' AND age > 25;
-- 优化前:在存储引擎层只能使用name条件,age条件在Server层过滤
-- 优化后:age条件也在存储引擎层过滤(索引下推)
-- 索引下推优化
CREATE INDEX idx_name_age ON users(name, age);
-- 查询
SELECT * FROM users WHERE name LIKE 'J%' AND age > 25;
-- 优化前:在存储引擎层只能使用name条件,age条件在Server层过滤
-- 优化后:age条件也在存储引擎层过滤(索引下推)
前缀索引
sql
-- 对于长字符串字段,使用前缀索引
CREATE INDEX idx_email_prefix ON users(email(10));
-- 选择合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS selectivity_5,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS selectivity_10,
COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) AS selectivity_15
FROM users;
-- 对于长字符串字段,使用前缀索引
CREATE INDEX idx_email_prefix ON users(email(10));
-- 选择合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS selectivity_5,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS selectivity_10,
COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) AS selectivity_15
FROM users;
索引设计原则
1. 选择性原则
sql
-- 计算字段选择性
SELECT
COUNT(DISTINCT column_name) / COUNT(*) AS selectivity
FROM table_name;
-- 选择性越高,索引效果越好
-- 选择性 = 不重复值数量 / 总记录数
-- 计算字段选择性
SELECT
COUNT(DISTINCT column_name) / COUNT(*) AS selectivity
FROM table_name;
-- 选择性越高,索引效果越好
-- 选择性 = 不重复值数量 / 总记录数
2. 频率原则
sql
-- 为经常出现在WHERE、ORDER BY、GROUP BY中的字段创建索引
CREATE INDEX idx_user_status ON users(status); -- WHERE条件
CREATE INDEX idx_order_date ON orders(order_date); -- ORDER BY
CREATE INDEX idx_sales_region ON sales(region); -- GROUP BY
-- 为经常出现在WHERE、ORDER BY、GROUP BY中的字段创建索引
CREATE INDEX idx_user_status ON users(status); -- WHERE条件
CREATE INDEX idx_order_date ON orders(order_date); -- ORDER BY
CREATE INDEX idx_sales_region ON sales(region); -- GROUP BY
3. 复合索引设计
sql
-- 复合索引字段顺序:选择性高的字段在前
CREATE INDEX idx_user_status_city_age ON users(status, city, age);
-- 考虑查询模式
-- 如果经常查询 WHERE status = ? AND city = ?
-- 那么 (status, city) 的顺序比 (city, status) 更好
-- 复合索引字段顺序:选择性高的字段在前
CREATE INDEX idx_user_status_city_age ON users(status, city, age);
-- 考虑查询模式
-- 如果经常查询 WHERE status = ? AND city = ?
-- 那么 (status, city) 的顺序比 (city, status) 更好
索引监控和分析
查看索引使用情况
sql
-- 查看索引统计信息
SELECT
TABLE_SCHEMA,
TABLE_NAME,
INDEX_NAME,
CARDINALITY,
SUB_PART,
NULLABLE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database';
-- 查看索引大小
SELECT
TABLE_NAME,
INDEX_NAME,
ROUND(STAT_VALUE * @@innodb_page_size / 1024 / 1024, 2) AS 'Size(MB)'
FROM mysql.innodb_index_stats
WHERE STAT_NAME = 'size'
AND DATABASE_NAME = 'your_database';
-- 查看索引统计信息
SELECT
TABLE_SCHEMA,
TABLE_NAME,
INDEX_NAME,
CARDINALITY,
SUB_PART,
NULLABLE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database';
-- 查看索引大小
SELECT
TABLE_NAME,
INDEX_NAME,
ROUND(STAT_VALUE * @@innodb_page_size / 1024 / 1024, 2) AS 'Size(MB)'
FROM mysql.innodb_index_stats
WHERE STAT_NAME = 'size'
AND DATABASE_NAME = 'your_database';
分析慢查询
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 分析执行计划
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE name = 'John' AND age > 25;
-- 关键指标:
-- type: 连接类型(const > eq_ref > ref > range > index > ALL)
-- key: 使用的索引
-- rows: 扫描的行数
-- Extra: 额外信息
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 分析执行计划
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE name = 'John' AND age > 25;
-- 关键指标:
-- type: 连接类型(const > eq_ref > ref > range > index > ALL)
-- key: 使用的索引
-- rows: 扫描的行数
-- Extra: 额外信息
索引优化工具
sql
-- 使用pt-index-usage分析索引使用情况
-- pt-index-usage --host=localhost --user=root --password=xxx database_name
-- 使用pt-duplicate-key-checker检查重复索引
-- pt-duplicate-key-checker --host=localhost --user=root --password=xxx
-- 使用pt-index-usage分析索引使用情况
-- pt-index-usage --host=localhost --user=root --password=xxx database_name
-- 使用pt-duplicate-key-checker检查重复索引
-- pt-duplicate-key-checker --host=localhost --user=root --password=xxx
索引维护
重建索引
sql
-- 重建索引(删除并重新创建)
DROP INDEX idx_user_name ON users;
CREATE INDEX idx_user_name ON users(name);
-- 或者使用ALTER TABLE
ALTER TABLE users DROP INDEX idx_user_name, ADD INDEX idx_user_name(name);
-- 重建索引(删除并重新创建)
DROP INDEX idx_user_name ON users;
CREATE INDEX idx_user_name ON users(name);
-- 或者使用ALTER TABLE
ALTER TABLE users DROP INDEX idx_user_name, ADD INDEX idx_user_name(name);
在线DDL
sql
-- MySQL 5.6+支持在线DDL
ALTER TABLE users ADD INDEX idx_user_email(email), ALGORITHM=INPLACE, LOCK=NONE;
-- ALGORITHM选项:
-- COPY: 创建临时表,复制数据(阻塞)
-- INPLACE: 原地修改(较少阻塞)
-- LOCK选项:
-- NONE: 允许并发读写
-- SHARED: 允许并发读,阻塞写
-- EXCLUSIVE: 阻塞所有操作
-- MySQL 5.6+支持在线DDL
ALTER TABLE users ADD INDEX idx_user_email(email), ALGORITHM=INPLACE, LOCK=NONE;
-- ALGORITHM选项:
-- COPY: 创建临时表,复制数据(阻塞)
-- INPLACE: 原地修改(较少阻塞)
-- LOCK选项:
-- NONE: 允许并发读写
-- SHARED: 允许并发读,阻塞写
-- EXCLUSIVE: 阻塞所有操作
索引统计信息更新
sql
-- 手动更新统计信息
ANALYZE TABLE users;
-- 自动更新配置
SET GLOBAL innodb_stats_auto_recalc = ON;
SET GLOBAL innodb_stats_persistent = ON;
-- 手动更新统计信息
ANALYZE TABLE users;
-- 自动更新配置
SET GLOBAL innodb_stats_auto_recalc = ON;
SET GLOBAL innodb_stats_persistent = ON;
常见问题和解决方案
1. 索引失效场景
sql
-- 函数操作导致索引失效
SELECT * FROM users WHERE UPPER(name) = 'JOHN'; -- 索引失效
SELECT * FROM users WHERE name = 'John'; -- 使用索引
-- 类型转换导致索引失效
SELECT * FROM users WHERE id = '123'; -- 可能导致索引失效
SELECT * FROM users WHERE id = 123; -- 使用索引
-- 前导模糊查询
SELECT * FROM users WHERE name LIKE '%John'; -- 索引失效
SELECT * FROM users WHERE name LIKE 'John%'; -- 使用索引
-- 函数操作导致索引失效
SELECT * FROM users WHERE UPPER(name) = 'JOHN'; -- 索引失效
SELECT * FROM users WHERE name = 'John'; -- 使用索引
-- 类型转换导致索引失效
SELECT * FROM users WHERE id = '123'; -- 可能导致索引失效
SELECT * FROM users WHERE id = 123; -- 使用索引
-- 前导模糊查询
SELECT * FROM users WHERE name LIKE '%John'; -- 索引失效
SELECT * FROM users WHERE name LIKE 'John%'; -- 使用索引
2. 索引选择错误
sql
-- 查看MySQL选择的索引
EXPLAIN SELECT * FROM users WHERE name = 'John' AND age = 25;
-- 强制使用特定索引
SELECT * FROM users FORCE INDEX(idx_user_name) WHERE name = 'John' AND age = 25;
-- 忽略特定索引
SELECT * FROM users IGNORE INDEX(idx_user_age) WHERE name = 'John' AND age = 25;
-- 查看MySQL选择的索引
EXPLAIN SELECT * FROM users WHERE name = 'John' AND age = 25;
-- 强制使用特定索引
SELECT * FROM users FORCE INDEX(idx_user_name) WHERE name = 'John' AND age = 25;
-- 忽略特定索引
SELECT * FROM users IGNORE INDEX(idx_user_age) WHERE name = 'John' AND age = 25;
3. 过多索引的问题
sql
-- 检查表的索引数量
SELECT
TABLE_NAME,
COUNT(*) AS index_count
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database'
GROUP BY TABLE_NAME
ORDER BY index_count DESC;
-- 删除不必要的索引
DROP INDEX idx_unused ON table_name;
-- 检查表的索引数量
SELECT
TABLE_NAME,
COUNT(*) AS index_count
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database'
GROUP BY TABLE_NAME
ORDER BY index_count DESC;
-- 删除不必要的索引
DROP INDEX idx_unused ON table_name;
最佳实践
- 合理设计索引:根据查询模式设计索引,避免过度索引
- 定期监控:监控索引使用情况,删除无用索引
- 复合索引优化:遵循最左前缀原则,合理安排字段顺序
- 避免索引失效:注意函数、类型转换等导致的索引失效
- 使用覆盖索引:减少回表查询,提高查询性能
- 前缀索引:对于长字符串字段,考虑使用前缀索引
- 在线DDL:使用在线DDL减少对业务的影响
总结
索引是MySQL性能优化的重要手段,正确使用索引能够显著提高查询性能。但索引也有成本,需要在查询性能和维护成本之间找到平衡。通过理解索引原理、掌握优化技巧、定期监控分析,可以设计出高效的索引方案。