深入学习MySQL(二):索引原理与优化实战

内容分享2小时前发布
0 0 0

本文是”深入学习MySQL:从新手到高手”系列的第二篇,将深入讲解B+树索引原理、索引类型、索引设计原则以及索引失效的常见场景。

索引概述

索引是协助MySQL高效获取数据的有序数据结构。如果没有索引,查询数据时需要全表扫描,随着数据量增大,性能会急剧下降。

-- 创建测试表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    age INT,
    city VARCHAR(50),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_username (username),
    INDEX idx_age_city (age, city)
) ENGINE=InnoDB;

-- 插入100万条测试数据
INSERT INTO users (username, email, age, city)
SELECT 
    CONCAT('user_', LPAD(seq, 7, '0')),
    CONCAT('user_', LPAD(seq, 7, '0'), '@example.com'),
    FLOOR(RAND() * 60) + 18,
    ELT(FLOOR(RAND() * 5) + 1, '北京', '上海', '广州', '深圳', '杭州')
FROM (
    SELECT @rownum := @rownum + 1 AS seq
    FROM information_schema.columns a, 
         information_schema.columns b,
         (SELECT @rownum := 0) r
    LIMIT 1000000
) t;

B+树索引原理

为什么选择B+树?

MySQL选择B+树作为索引结构,主要基于以下缘由:

数据结构

查询复杂度

问题

哈希表

O(1)

不支持范围查询

二叉搜索树

O(log n)

可能退化为链表

平衡二叉树(AVL)

O(log n)

树高度大,磁盘IO次数多

B树

O(log n)

非叶子节点存数据,扫描效率低

B+树

O(log n)

叶子节点有序链表,范围查询高效

B+树结构详解

                        ┌─────────────────┐
                        │   根节点(索引页)  │
                        │  [10] [20] [30] │
                        └────┬───┬───┬────┘
                 ┌───────────┘   │   └───────────┐
                 ▼               ▼               ▼
        ┌────────────┐  ┌────────────┐  ┌────────────┐
        │  索引页     │  │  索引页     │  │  索引页     │
        │ [3][5][8]  │  │[12][15][18]│  │[22][25][28]│
        └──┬──┬──┬───┘  └──┬──┬──┬───┘  └──┬──┬──┬───┘
           │  │  │         │  │  │         │  │  │
           ▼  ▼  ▼         ▼  ▼  ▼         ▼  ▼  ▼
        ┌──────────────────────────────────────────────┐
        │              叶子节点(数据页)                  │
        │ [1,2,3]←→[4,5,6]←→[7,8,9]←→[10,11,12]←→...  │
        │    ↑                                    ↑    │
        │    └────────── 双向链表 ─────────────────┘    │
        └──────────────────────────────────────────────┘

B+树的特点

  1. 非叶子节点只存索引:不存储数据,可以存储更多索引项,降低树高度
  2. 叶子节点存储数据:所有数据都在叶子节点
  3. 叶子节点有序链表:支持高效的范围查询
  4. 树高度低:3层B+树可存储约2000万条数据

计算B+树高度

假设:
- 主键为BIGINT(8字节)
- 指针大小6字节
- 页大小16KB

非叶子节点可存储索引项数:16384 / (8 + 6)  1170
叶子节点假设每条记录1KB,可存储:16384 / 1024  16

树高度计算:
- 2层:1170 × 16  1.8万条
- 3层:1170 × 1170 × 16  2190万条
- 4层:1170 × 1170 × 1170 × 16  256亿条

聚簇索引与二级索引

聚簇索引(Clustered Index)

聚簇索引决定了数据的物理存储顺序,InnoDB的数据文件本身就是按聚簇索引组织的。

┌─────────────────────────────────────────────────────────────┐
│                      聚簇索引 (主键索引)                      │
│                                                             │
│                        ┌─────────┐                          │
│                        │  根节点  │                          │
│                        │ [50][100]│                          │
│                        └────┬────┘                          │
│                    ┌────────┴────────┐                      │
│                    ▼                 ▼                      │
│              ┌──────────┐      ┌──────────┐                 │
│              │ [10][30] │      │ [70][90] │                 │
│              └────┬─────┘      └────┬─────┘                 │
│                   │                 │                       │
│         ┌─────────┼─────────┐       │                       │
│         ▼         ▼         ▼       ▼                       │
│    ┌─────────┬─────────┬─────────┬─────────┐               │
│    │ id=1    │ id=10   │ id=30   │ id=50   │               │
│    │ 全部列  │ 全部列   │ 全部列  │ 全部列   │  ← 叶子节点    │
│    │ 数据    │ 数据     │ 数据    │ 数据     │    存完整数据  │
│    └─────────┴─────────┴─────────┴─────────┘               │
│         ↑                                 ↑                 │
│         └────────── 双向链表 ──────────────┘                 │
└─────────────────────────────────────────────────────────────┘

聚簇索引的选择规则

-- 1. 如果定义了主键,主键就是聚簇索引
CREATE TABLE t1 (
    id INT PRIMARY KEY,  -- 聚簇索引
    name VARCHAR(50)
);

-- 2. 如果没有主键,选择第一个非空唯一索引
CREATE TABLE t2 (
    id INT NOT NULL UNIQUE,  -- 聚簇索引
    name VARCHAR(50)
);

-- 3. 如果都没有,InnoDB自动生成一个隐藏的row_id作为聚簇索引
CREATE TABLE t3 (
    name VARCHAR(50)
);

二级索引(Secondary Index)

二级索引的叶子节点存储的是索引列的值 + 主键值,而不是完整的数据行。

┌─────────────────────────────────────────────────────────────┐
│                   二级索引 (idx_username)                    │
│                                                             │
│                        ┌─────────┐                          │
│                        │  根节点  │                          │
│                        │ [M][Z]  │                          │
│                        └────┬────┘                          │
│                    ┌────────┴────────┐                      │
│                    ▼                 ▼                      │
│              ┌──────────┐      ┌──────────┐                 │
│              │ [A][G]   │      │ [P][T]   │                 │
│              └────┬─────┘      └────┬─────┘                 │
│                   │                 │                       │
│         ┌─────────┼─────────┐       │                       │
│         ▼         ▼         ▼       ▼                       │
│    ┌─────────┬─────────┬─────────┬─────────┐               │
│    │ Alice   │ Bob     │ Mike    │ Tom     │               │
│    │ id=5    │ id=3    │ id=8    │ id=1    │  ← 叶子节点    │
│    │         │         │         │         │    存索引值+主键│
│    └─────────┴─────────┴─────────┴─────────┘               │
└─────────────────────────────────────────────────────────────┘

回表查询

当通过二级索引查询时,如果需要获取索引中不包含的列,就需要回表

-- 假设有索引 idx_username(username)

-- 需要回表:需要获取email列,但索引中没有
SELECT id, username, email FROM users WHERE username = 'Alice';

-- 执行过程:
-- 1. 通过idx_username找到username='Alice'的记录,获取主键id=5
-- 2. 通过主键id=5回表到聚簇索引,获取完整数据行
-- 3. 返回id, username, email
查询过程:

二级索引 idx_username                    聚簇索引(主键索引)
┌─────────────┐                         ┌─────────────────┐
│ username    │                         │ id (主键)        │
│ ─────────── │                         │ ───────────────  │
│ Alice → id=5│ ───── 回表查询 ────────→ │ id=5 → 完整数据  │
└─────────────┘                         └─────────────────┘

覆盖索引

如果索引包含了查询所需的所有列,就不需要回表,这称为覆盖索引

-- 创建覆盖索引
CREATE INDEX idx_username_email ON users(username, email);

-- 覆盖索引查询:不需要回表
SELECT username, email FROM users WHERE username = 'Alice';

-- 通过EXPLAIN验证
EXPLAIN SELECT username, email FROM users WHERE username = 'Alice';
-- Extra列显示:Using index(表明使用了覆盖索引)
-- 覆盖索引的典型场景

-- 1. 只查询索引列
SELECT username FROM users WHERE username LIKE 'A%';

-- 2. 查询索引列 + 主键
SELECT id, username FROM users WHERE username = 'Alice';

-- 3. COUNT查询
SELECT COUNT(*) FROM users WHERE username LIKE 'A%';

索引类型详解

按数据结构分类

B+树索引

-- 默认索引类型
CREATE INDEX idx_name ON users(username);
-- 等价于
CREATE INDEX idx_name ON users(username) USING BTREE;

Hash索引

-- Memory引擎默认使用Hash索引
CREATE TABLE hash_test (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    INDEX idx_name (name) USING HASH
) ENGINE=MEMORY;

-- InnoDB的自适应哈希索引(AHI)由系统自动管理
SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';

Hash索引的限制

  • 只支持等值查询(=, IN)
  • 不支持范围查询(>, <, BETWEEN)
  • 不支持排序
  • 不支持部分索引列匹配

全文索引

-- 创建全文索引
CREATE TABLE articles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200),
    content TEXT,
    FULLTEXT INDEX ft_content (title, content)
) ENGINE=InnoDB;

-- 使用全文索引
SELECT * FROM articles 
WHERE MATCH(title, content) AGAINST('MySQL 索引' IN NATURAL LANGUAGE MODE);

-- 布尔模式
SELECT * FROM articles 
WHERE MATCH(title, content) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE);

按功能分类

主键索引

-- 创建主键索引
CREATE TABLE t1 (
    id INT PRIMARY KEY,  -- 方式1
    name VARCHAR(50)
);

CREATE TABLE t2 (
    id INT,
    name VARCHAR(50),
    PRIMARY KEY (id)     -- 方式2
);

ALTER TABLE t3 ADD PRIMARY KEY (id);  -- 方式3

唯一索引

-- 创建唯一索引
CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(100) UNIQUE,  -- 方式1
    phone VARCHAR(20),
    UNIQUE INDEX idx_phone (phone)  -- 方式2
);

-- 唯一索引允许NULL值,且可以有多个NULL
INSERT INTO users VALUES (1, NULL, NULL);
INSERT INTO users VALUES (2, NULL, NULL);  -- 成功

普通索引

-- 创建普通索引
CREATE INDEX idx_username ON users(username);

-- 或者在建表时
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    INDEX idx_username (username)
);

前缀索引

对于长字符串列,可以只索引前N个字符:

-- 创建前缀索引
CREATE INDEX idx_email_prefix ON users(email(10));

-- 计算合适的前缀长度
SELECT 
    COUNT(DISTINCT email) / COUNT(*) AS full_selectivity,
    COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS prefix_5,
    COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS prefix_10,
    COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) AS prefix_15
FROM users;

-- 选择接近full_selectivity的最小前缀长度

联合索引(复合索引)

-- 创建联合索引
CREATE INDEX idx_age_city ON users(age, city);

-- 联合索引的结构
-- 先按age排序,age一样时按city排序
联合索引 idx_age_city(age, city) 的B+树结构:

                    ┌─────────────────┐
                    │    (25, 北京)    │
                    │    (35, 上海)    │
                    └────────┬────────┘
           ┌─────────────────┼─────────────────┐
           ▼                 ▼                 ▼
    ┌────────────┐    ┌────────────┐    ┌────────────┐
    │(18,北京)   │    │(25,广州)   │    │(35,深圳)   │
    │(18,上海)   │    │(25,深圳)   │    │(40,北京)   │
    │(20,广州)   │    │(30,北京)   │    │(45,上海)   │
    └────────────┘    └────────────┘    └────────────┘

最左前缀原则

联合索引遵循最左前缀原则:查询条件必须从索引的最左列开始,且不能跳过中间列。

-- 假设有联合索引 idx_a_b_c(a, b, c)

-- ✅ 可以使用索引
SELECT * FROM t WHERE a = 1;                    -- 使用a
SELECT * FROM t WHERE a = 1 AND b = 2;          -- 使用a, b
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3; -- 使用a, b, c
SELECT * FROM t WHERE a = 1 AND c = 3;          -- 只使用a(c无法使用)
SELECT * FROM t WHERE b = 2 AND a = 1;          -- 使用a, b(优化器会调整顺序)

-- ❌ 无法使用索引
SELECT * FROM t WHERE b = 2;                    -- 不满足最左前缀
SELECT * FROM t WHERE c = 3;                    -- 不满足最左前缀
SELECT * FROM t WHERE b = 2 AND c = 3;          -- 不满足最左前缀

范围查询对索引的影响

-- 假设有联合索引 idx_a_b_c(a, b, c)

SELECT * FROM t WHERE a = 1 AND b > 2 AND c = 3;
-- a使用索引,b使用索引(范围查询),c无法使用索引

SELECT * FROM t WHERE a = 1 AND b LIKE 'abc%' AND c = 3;
-- a使用索引,b使用索引(前缀匹配),c无法使用索引

-- 优化:调整索引顺序
CREATE INDEX idx_a_c_b ON t(a, c, b);
SELECT * FROM t WHERE a = 1 AND c = 3 AND b > 2;
-- a, c, b都可以使用索引

索引失效场景

1. 对索引列使用函数

-- ❌ 索引失效
SELECT * FROM users WHERE YEAR(created_at) = 2024;
SELECT * FROM users WHERE LEFT(username, 3) = 'abc';

-- ✅ 优化写法
SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
SELECT * FROM users WHERE username LIKE 'abc%';

2. 对索引列进行运算

-- ❌ 索引失效
SELECT * FROM users WHERE id + 1 = 10;
SELECT * FROM users WHERE age * 2 = 40;

-- ✅ 优化写法
SELECT * FROM users WHERE id = 9;
SELECT * FROM users WHERE age = 20;

3. 隐式类型转换

-- 假设phone字段是VARCHAR类型

-- ❌ 索引失效(字符串与数字比较,phone被转换为数字)
SELECT * FROM users WHERE phone = 13800138000;

-- ✅ 正确写法
SELECT * FROM users WHERE phone = '13800138000';

4. LIKE以通配符开头

-- ❌ 索引失效
SELECT * FROM users WHERE username LIKE '%abc';
SELECT * FROM users WHERE username LIKE '%abc%';

-- ✅ 可以使用索引
SELECT * FROM users WHERE username LIKE 'abc%';

5. OR条件使用不当

-- ❌ 如果or两边的列不都有索引,索引失效
SELECT * FROM users WHERE username = 'Alice' OR age = 25;
-- 假设只有username有索引,age没有索引

-- ✅ 优化方案1:给age也加索引
CREATE INDEX idx_age ON users(age);

-- ✅ 优化方案2:使用UNION
SELECT * FROM users WHERE username = 'Alice'
UNION
SELECT * FROM users WHERE age = 25;

6. NOT IN 和 NOT EXISTS

-- ❌ 可能导致索引失效
SELECT * FROM users WHERE id NOT IN (1, 2, 3);

-- ✅ 优化写法(如果数据量大)
SELECT * FROM users WHERE id NOT IN (1, 2, 3) AND id > 0;
-- 或者使用LEFT JOIN
SELECT u.* FROM users u 
LEFT JOIN (SELECT 1 AS id UNION SELECT 2 UNION SELECT 3) t ON u.id = t.id
WHERE t.id IS NULL;

7. IS NULL 和 IS NOT NULL

-- 索引列有大量NULL值时,IS NOT NULL可能不走索引
SELECT * FROM users WHERE email IS NOT NULL;

-- 优化:设计时尽量避免NULL,使用默认值
ALTER TABLE users MODIFY email VARCHAR(100) NOT NULL DEFAULT '';

8. 不等于条件

-- ❌ != 和 <> 可能导致索引失效
SELECT * FROM users WHERE status != 1;

-- ✅ 如果status只有几个值,改用IN
SELECT * FROM users WHERE status IN (0, 2, 3);

索引设计原则

1. 选择区分度高的列

-- 计算列的区分度(选择性)
SELECT 
    COUNT(DISTINCT username) / COUNT(*) AS username_selectivity,
    COUNT(DISTINCT city) / COUNT(*) AS city_selectivity,
    COUNT(DISTINCT age) / COUNT(*) AS age_selectivity
FROM users;

-- 区分度越接近1越好,提议 > 0.1

2. 联合索引列顺序

-- 原则:
-- 1. 区分度高的列放前面
-- 2. 查询频率高的列放前面
-- 3. 范围查询的列放后面

-- 示例:查询条件常常是 city = ? AND age > ?
-- 推荐:INDEX(city, age),而不是INDEX(age, city)

3. 避免冗余索引

-- 冗余索引示例
CREATE INDEX idx_a ON t(a);
CREATE INDEX idx_a_b ON t(a, b);  -- idx_a是冗余的

-- 查找冗余索引
SELECT * FROM sys.schema_redundant_indexes;

4. 控制索引数量

-- 查看表的索引
SHOW INDEX FROM users;

-- 提议:
-- 单表索引数量不超过5-6个
-- 单个索引列数不超过5个

5. 使用覆盖索引

-- 高频查询场景,设计覆盖索引
-- 假设常常执行:SELECT id, username, email FROM users WHERE username = ?

CREATE INDEX idx_username_email ON users(username, email);
-- 这样查询就不需要回表

EXPLAIN执行计划

EXPLAIN是分析SQL性能的重大工具。

EXPLAIN SELECT * FROM users WHERE username = 'Alice';

关键字段解读

字段

说明

id

查询序号

select_type

查询类型

table

访问的表

type

访问类型(重大)

possible_keys

可能使用的索引

key

实际使用的索引

key_len

索引使用的字节数

ref

索引比较的列或常量

rows

预估扫描行数

filtered

过滤比例

Extra

额外信息(重大)

type访问类型(从优到差)

-- 1. system: 表只有一行
EXPLAIN SELECT * FROM (SELECT 1) t;

-- 2. const: 主键或唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE id = 1;

-- 3. eq_ref: 多表JOIN时,使用主键或唯一索引
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;

-- 4. ref: 非唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE username = 'Alice';

-- 5. range: 索引范围查询
EXPLAIN SELECT * FROM users WHERE age > 25;
EXPLAIN SELECT * FROM users WHERE id IN (1, 2, 3);

-- 6. index: 全索引扫描
EXPLAIN SELECT username FROM users;

-- 7. ALL: 全表扫描(需要优化)
EXPLAIN SELECT * FROM users WHERE email LIKE '%@gmail.com';

Extra常见值

-- Using index: 覆盖索引,无需回表
EXPLAIN SELECT username FROM users WHERE username = 'Alice';

-- Using where: 使用WHERE过滤
EXPLAIN SELECT * FROM users WHERE age > 25;

-- Using index condition: 索引条件下推(ICP)
EXPLAIN SELECT * FROM users WHERE username LIKE 'A%' AND username LIKE '%e';

-- Using temporary: 使用临时表(需要优化)
EXPLAIN SELECT DISTINCT city FROM users;

-- Using filesort: 文件排序(需要优化)
EXPLAIN SELECT * FROM users ORDER BY email;

-- Using join buffer: JOIN时使用缓冲区
EXPLAIN SELECT * FROM users u, orders o WHERE u.id = o.user_id;

key_len计算

-- key_len表明索引使用的字节数,可以判断联合索引使用了几列

-- 计算规则:
-- INT: 4字节
-- BIGINT: 8字节
-- VARCHAR(n): 3n + 2 (UTF8MB4)  n + 2 (Latin1)
-- CHAR(n): 3n (UTF8MB4)
-- 允许NULL: +1字节

-- 示例:idx_age_city(age INT, city VARCHAR(50))
-- age: 4 + 1 = 5 (允许NULL)
-- city: 50 * 3 + 2 + 1 = 153 (VARCHAR + 允许NULL)
-- 全部使用: 5 + 153 = 158

索引优化实战

案例1:慢查询优化

-- 原始慢查询
SELECT * FROM orders 
WHERE user_id = 12345 
AND status = 1 
AND created_at > '2024-01-01'
ORDER BY created_at DESC
LIMIT 10;

-- 分析
EXPLAIN SELECT * FROM orders ...;
-- type: ALL, 全表扫描

-- 优化:创建联合索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);

-- 优化后
EXPLAIN SELECT * FROM orders ...;
-- type: range, 使用索引范围查询

案例2:分页查询优化

-- 原始分页查询(深分页性能差)
SELECT * FROM users ORDER BY id LIMIT 1000000, 10;

-- 优化方案1:延迟关联
SELECT u.* FROM users u
INNER JOIN (
    SELECT id FROM users ORDER BY id LIMIT 1000000, 10
) t ON u.id = t.id;

-- 优化方案2:游标分页(记住上次的位置)
SELECT * FROM users WHERE id > 1000000 ORDER BY id LIMIT 10;

案例3:ORDER BY优化

-- 原始查询(Using filesort)
SELECT * FROM users WHERE city = '北京' ORDER BY age;

-- 优化:创建联合索引
CREATE INDEX idx_city_age ON users(city, age);

-- 优化后:Using index(索引排序)
EXPLAIN SELECT * FROM users WHERE city = '北京' ORDER BY age;

案例4:GROUP BY优化

-- 原始查询(Using temporary; Using filesort)
SELECT city, COUNT(*) FROM users GROUP BY city;

-- 优化:创建索引
CREATE INDEX idx_city ON users(city);

-- 优化后:Using index
EXPLAIN SELECT city, COUNT(*) FROM users GROUP BY city;

索引监控与维护

查看索引使用情况

-- 查看索引使用统计
SELECT 
    object_schema,
    object_name,
    index_name,
    count_star,
    count_read,
    count_write
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'your_database'
ORDER BY count_star DESC;

-- 查找未使用的索引
SELECT 
    object_schema,
    object_name,
    index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema NOT IN ('mysql', 'performance_schema');

索引碎片整理

-- 查看表的碎片情况
SELECT 
    TABLE_NAME,
    DATA_LENGTH,
    INDEX_LENGTH,
    DATA_FREE,
    (DATA_FREE / (DATA_LENGTH + INDEX_LENGTH)) * 100 AS fragmentation_ratio
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database'
AND DATA_FREE > 0;

-- 整理碎片
OPTIMIZE TABLE users;
-- 或者
ALTER TABLE users ENGINE=InnoDB;

在线DDL

-- MySQL 8.0支持在线添加索引
ALTER TABLE users ADD INDEX idx_email(email), ALGORITHM=INPLACE, LOCK=NONE;

-- ALGORITHM选项:
-- INPLACE: 原地修改,不复制表
-- COPY: 复制表(旧方式)

-- LOCK选项:
-- NONE: 不加锁
-- SHARED: 共享锁(允许读)
-- EXCLUSIVE: 排他锁

本章小结

本文深入讲解了MySQL索引的核心知识:

  1. B+树原理:非叶子节点存索引,叶子节点存数据并形成有序链表
  2. 聚簇索引vs二级索引:聚簇索引存完整数据,二级索引存索引值+主键
  3. 回表与覆盖索引:覆盖索引可以避免回表,提升查询性能
  4. 最左前缀原则:联合索引必须从最左列开始使用
  5. 索引失效场景:函数、运算、类型转换、LIKE %开头等
  6. EXPLAIN分析:type和Extra是关键字段
  7. 索引设计原则:高区分度、合理顺序、避免冗余

下一篇预告:《深入学习MySQL(三):SQL优化与执行计划分析》,将详细讲解慢查询分析、SQL优化技巧和执行计划的深度解读。

深入学习MySQL(二):索引原理与优化实战

© 版权声明

相关文章

暂无评论

none
暂无评论...