开始改变第六天 MySQL(2)
MySQL索引深度解析:从原理到实战优化
一、为什么需要索引?
想象一下,如果没有索引,数据库查询就像在一本没有目录的厚书中查找特定内容,只能一页页翻看。而索引就像书的目录,让我们能够快速定位到需要的数据。
二、MySQL索引底层原理
B+树索引结构
B+树示例:
根节点: [指针1 | 键值20 | 指针2 | 键值40 | 指针3]/        |         \/         |          \
非叶节点       非叶节点       非叶节点
[5|10|15]    [25|30|35]    [45|50|55]/  \         /   \         /   \
叶子节点     叶子节点      叶子节点
[1,2,3,4] [6,7,8,9] [11,12,13,14]...
B+树的核心优势
- 所有数据都在叶子节点,查询路径长度固定
- 叶子节点形成双向链表,便于范围查询
- 非叶子节点只存索引,可以缓存更多数据
- 树高更低,减少磁盘I/O次数
三、索引类型详解
1. 聚簇索引(Clustered Index)
-- InnoDB中,主键就是聚簇索引
CREATE TABLE users (id INT PRIMARY KEY,           -- 聚簇索引name VARCHAR(50),age INT,email VARCHAR(100)
);
特点:
- 叶子节点存储完整行数据
- 每张表只能有一个聚簇索引
- 物理存储顺序与索引顺序一致
2. 二级索引(Secondary Index)
-- 创建二级索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
特点:
- 叶子节点存储主键值
- 查询时需要"回表"操作
- 每张表可以有多个二级索引
四、索引创建实战指南
单列索引 vs 复合索引
-- 单列索引:适合单个条件查询
CREATE INDEX idx_email ON users(email);-- 复合索引:适合多条件查询
CREATE INDEX idx_name_age ON users(name, age);
CREATE INDEX idx_name_age_city ON users(name, age, city);
复合索引的最左前缀原则
-- 索引: (name, age, city)-- 使用索引的情况:
SELECT * FROM users WHERE name = '张三';
SELECT * FROM users WHERE name = '张三' AND age = 25;
SELECT * FROM users WHERE name = '张三' AND age = 25 AND city = '北京';-- 不使用索引的情况:
SELECT * FROM users WHERE age = 25;                    -- 违反最左前缀
SELECT * FROM users WHERE age = 25 AND city = '北京';   -- 违反最左前缀
SELECT * FROM users WHERE city = '北京';                -- 违反最左前缀
索引选择性原则
-- 计算索引选择性
SELECT COUNT(DISTINCT gender) / COUNT(*) as gender_selectivity,    -- 选择性低COUNT(DISTINCT email) / COUNT(*) as email_selectivity       -- 选择性高
FROM users;-- 选择性 > 0.1 的列适合建立索引
五、回表查询与覆盖索引
回表查询过程
-- 假设有二级索引 idx_name
SELECT * FROM users WHERE name = '张三';-- 执行流程:
-- 1. 在idx_name索引树中找到'张三'对应的主键id
-- 2. 用主键id到聚簇索引中查找完整数据行
-- 3. 返回完整数据
-- 这个过程就是"回表"
覆盖索引优化
-- 创建覆盖索引
CREATE INDEX idx_name_age ON users(name, age);-- 覆盖索引查询:直接从索引获取数据,避免回表
SELECT name, age FROM users WHERE name = '张三';-- 检查是否使用覆盖索引
EXPLAIN SELECT name, age FROM users WHERE name = '张三';
-- 在Extra列看到: Using index
覆盖索引扩展应用
-- 创建包含更多字段的覆盖索引
CREATE INDEX idx_name_age_city ON users(name, age, city);-- 以下查询都可以使用覆盖索引
SELECT name FROM users WHERE name = '张三';
SELECT name, age FROM users WHERE name = '张三';
SELECT name, age, city FROM users WHERE name = '张三';
SELECT COUNT(*) FROM users WHERE name = '张三';
六、索引失效的完整场景分析
1. 违反最左前缀原则
-- 索引: (name, age, city)
SELECT * FROM users WHERE age = 25;                    -- ❌ 失效
SELECT * FROM users WHERE city = '北京';                -- ❌ 失效
SELECT * FROM users WHERE name = '张三' AND city = '北京'; -- ✅ 部分使用
2. 在索引列上使用函数或计算
-- 索引: (create_time)
SELECT * FROM users WHERE YEAR(create_time) = 2023;    -- ❌ 失效
SELECT * FROM users WHERE create_time + 1 > NOW();     -- ❌ 失效-- 优化方案:
SELECT * FROM users WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01'; -- ✅
3. 类型转换导致失效
-- 假设phone是varchar类型
SELECT * FROM users WHERE phone = 13800138000;         -- ❌ 失效(数字转字符串)
SELECT * FROM users WHERE phone = '13800138000';       -- ✅ 使用索引-- 假设id是int类型
SELECT * FROM users WHERE id = '100';                  -- ✅ 使用索引(字符串转数字)
4. LIKE模糊查询
-- 索引: (name)
SELECT * FROM users WHERE name LIKE '%张三%';           -- ❌ 失效
SELECT * FROM users WHERE name LIKE '%张三';            -- ❌ 失效
SELECT * FROM users WHERE name LIKE '张三%';            -- ✅ 使用索引
5. 使用不等于条件
SELECT * FROM users WHERE status != 1;                 -- ❌ 可能全表扫描
SELECT * FROM users WHERE status <> 1;                 -- ❌ 可能全表扫描-- 优化方案:
SELECT * FROM users WHERE status IN (0, 2, 3);         -- ✅ 使用索引
SELECT * FROM users WHERE status > 1 OR status < 1;    -- ✅ 使用索引
6. OR条件处理
-- 假设name有索引,age无索引
SELECT * FROM users WHERE name = '张三' OR age = 25;   -- ❌ 索引失效-- 优化方案:
SELECT * FROM users WHERE name = '张三'
UNION ALL
SELECT * FROM users WHERE age = 25 AND name != '张三'; -- ✅ 分别使用索引
7. 范围查询后的索引列失效
-- 索引: (name, age, city)
SELECT * FROM users WHERE name = '张三' AND age > 20 AND city = '北京';
-- ✅ name使用索引,✅ age使用索引,❌ city无法使用索引-- 优化方案:调整索引顺序或拆分为多个查询
CREATE INDEX idx_name_city_age ON users(name, city, age);
七、索引设计最佳实践
1. 选择合适的索引列
-- 优先为以下列创建索引:
-- 1. WHERE条件中的列
-- 2. JOIN关联条件的列  
-- 3. ORDER BY排序的列
-- 4. GROUP BY分组的列
2. 复合索引设计技巧
-- 设计原则:等值查询列在前,范围查询列在后
CREATE INDEX idx_status_create_time ON orders(status, create_time);-- 查询:等值 + 范围
SELECT * FROM orders WHERE status = 1 AND create_time > '2023-01-01';
3. 前缀索引优化
-- 对于长文本列,使用前缀索引
CREATE INDEX idx_content ON articles(content(100));  -- 只索引前100个字符-- 计算合适的前缀长度
SELECT COUNT(DISTINCT LEFT(content, 50)) / COUNT(*) as selectivity_50,COUNT(DISTINCT LEFT(content, 100)) / COUNT(*) as selectivity_100,COUNT(DISTINCT LEFT(content, 200)) / COUNT(*) as selectivity_200
FROM articles;
八、索引性能监控与分析
使用EXPLAIN分析查询
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25;-- 重点关注:
-- type: const > ref > range > index > ALL
-- key: 实际使用的索引
-- rows: 预估扫描行数
-- Extra: Using index(覆盖索引), Using where, Using filesort, Using temporary
索引使用情况统计
-- 查看索引使用频率
SELECT OBJECT_SCHEMA,OBJECT_NAME,INDEX_NAME,COUNT_READ,COUNT_FETCH
FROM performance_schema.table_io_waits_summary_by_index_usage
ORDER BY COUNT_READ DESC;-- 查找未使用的索引
SELECT * FROM sys.schema_unused_indexes;
九、实际案例分析
电商系统索引设计
-- 订单表索引设计
CREATE TABLE orders (id BIGINT PRIMARY KEY,user_id BIGINT,status TINYINT,create_time DATETIME,total_amount DECIMAL(10,2),-- 其他字段...INDEX idx_user_status (user_id, status),INDEX idx_create_time (create_time),INDEX idx_status_create (status, create_time)
);-- 商品表索引设计  
CREATE TABLE products (id BIGINT PRIMARY KEY,category_id INT,price DECIMAL(10,2),name VARCHAR(200),-- 其他字段...INDEX idx_category_price (category_id, price),INDEX idx_name (name)
);
总结
索引是数据库性能优化的核心,正确的索引设计可以提升查询性能几个数量级。记住以下要点:
- 理解B+树原理:知道索引如何工作才能更好使用
- 掌握最左前缀原则:复合索引设计的基石
- 善用覆盖索引:避免回表,提升性能
- 警惕索引失效:避免常见的索引使用陷阱
- 持续监控优化:根据实际查询模式调整索引策略
索引不是越多越好,合适的索引才是最好的索引。在实际应用中,要根据具体的业务场景和查询模式来制定索引策略。
