一、核心原则概览
MySQL索引设计遵循以下核心原则:
- 选择性原则:优先索引高区分度字段
- 最左前缀原则:合理设计联合索引顺序
- 覆盖索引原则:减少回表操作
- 适度原则:避免过度索引
- 业务优先原则:根据实际查询模式设计
二、详细原则解析
原则1:为高频查询条件建索引
核心要点
✅ 应该建索引:
- WHERE 子句中的过滤字段
- JOIN ON 的关联字段
- ORDER BY / GROUP BY 的排序字段
- SELECT DISTINCT 的字段
❌ 不需要建索引:
- 很少在查询中使用的字段
- 总是 SELECT * 但从不过滤的字段
实战示例
-- 分析业务查询模式
-- 高频查询1(80%):按用户ID查订单
SELECT * FROM orders WHERE user_id=? ORDER BY create_time DESC;
-- 高频查询2(15%):按状态和时间查订单
SELECT * FROM orders WHERE status=? AND create_time >= ?;
-- 低频查询(5%):按商品名称查
SELECT * FROM orders WHERE product_name LIKE ?;
-- 索引设计:
INDEX(user_id, create_time) -- 覆盖查询1
INDEX(status, create_time) -- 覆盖查询2
-- 不为 product_name 建索引(频率低,LIKE查询效果差)
原则2:选择高区分度(选择性)字段
区分度计算
-- 计算字段选择性
SELECT
COUNT(DISTINCT column) / COUNT(*) AS selectivity
FROM table_name;
-- 选择性范围:0-1
-- 接近1:区分度高(如:用户ID、订单号)
-- 接近0:区分度低(如:性别、状态)
实战判断
-- 表有100万行数据
-- 字段1:user_id
SELECT COUNT(DISTINCT user_id) FROM orders; -- 结果:500,000
-- 选择性:500,000 / 1,000,000 = 0.5 ✅ 适合建索引
-- 字段2:gender
SELECT COUNT(DISTINCT gender) FROM users; -- 结果:2
-- 选择性:2 / 1,000,000 = 0.000002 ❌ 不适合单独建索引
-- 字段3:status
SELECT COUNT(DISTINCT status) FROM orders; -- 结果:5
-- 选择性:5 / 1,000,000 = 0.000005 ⚠️ 看查询条件决定
低区分度字段的处理
-- 策略1:作为联合索引的后续列
INDEX(user_id, status) -- status区分度低,但组合后有用
-- 策略2:利用索引跳跃扫描(MySQL 8.0+)
INDEX(status, create_time) -- status值少,可跳跃扫描
-- 策略3:使用复合条件
WHERE status='active' AND create_time >= '2025-01-01'
-- status快速定位少量数据,再用时间精确过滤
原则3:合理设计联合索引顺序
排序规则
规则1:高频等值查询列优先
-- 查询:WHERE a=? AND b=? AND c>?
-- a、b是等值,c是范围
-- 索引:INDEX(a, b, c) 或 INDEX(b, a, c)
规则2:区分度高的列优先
-- a区分度:0.8(高)
-- b区分度:0.1(低)
-- 索引:INDEX(a, b) ✅ 优于 INDEX(b, a)
规则3:范围查询列放后面
-- 查询:WHERE a=? AND b>? AND c=?
-- 索引:INDEX(a, b, c)
-- 注意:b是范围查询,c无法使用索引
-- 优化:如果c过滤效果好,考虑 INDEX(a, c, b)
典型场景
-- 场景:电商订单查询
-- 字段:user_id(区分度0.5)、status(区分度0.00001)、create_time
-- 查询模式1(70%):用户查自己的订单
WHERE user_id=? ORDER BY create_time DESC
→ INDEX(user_id, create_time)
-- 查询模式2(20%):管理员按状态查订单
WHERE status=? AND create_time >= ?
→ INDEX(status, create_time)
-- 查询模式3(10%):用户查特定状态订单
WHERE user_id=? AND status=? ORDER BY create_time DESC
→ INDEX(user_id, status, create_time) -- 或复用索引1
原则4:充分利用最左前缀原则
减少冗余索引
-- ❌ 冗余设计
INDEX(a)
INDEX(a, b)
INDEX(a, b, c)
-- ✅ 优化设计
INDEX(a, b, c) -- 一个索引覆盖 (a), (a,b), (a,b,c) 三种查询
-- 必要时再加 INDEX(b) 或 INDEX(c)(看查询频率)
合理补充索引
-- 已有:INDEX(a, b, c)
-- 如果有高频查询:WHERE b=? AND c=?
-- 需要补充:INDEX(b, c)
-- 如果有高频查询:WHERE c=?
-- 需要补充:INDEX(c)
原则5:利用覆盖索引
什么是覆盖索引
-- 索引:INDEX(user_id, status, create_time)
-- 覆盖索引查询(无需回表)
SELECT user_id, status, create_time
FROM orders
WHERE user_id=?;
-- Extra: Using index
-- 非覆盖查询(需要回表)
SELECT user_id, status, create_time, total_amount
FROM orders
WHERE user_id=?;
-- Extra: Using index condition
设计技巧
-- 技巧1:把SELECT常用列包含在索引中
-- 高频查询:SELECT id, name, age FROM users WHERE city=?
INDEX(city, name, age) -- 覆盖索引
-- 技巧2:延迟关联(大表分页)
-- 不好:
SELECT * FROM large_table
WHERE condition
ORDER BY create_time
LIMIT 10000, 10; -- 需要扫描10010行并回表
-- 好:使用覆盖索引 + 延迟关联
SELECT t.* FROM large_table t
INNER JOIN (
SELECT id FROM large_table
WHERE condition
ORDER BY create_time
LIMIT 10000, 10
) tmp ON t.id = tmp.id; -- 子查询走覆盖索引
原则6:避免过度索引
索引的代价
写入成本:
- 每个索引在INSERT/UPDATE/DELETE时都需要维护
- N个索引 → 写入放大N倍
存储成本:
- 每个索引占用磁盘空间
- 索引越多,空间越大
内存成本:
- 索引需要缓存在Buffer Pool中
- 过多索引导致缓存命中率下降
合理数量
经验值:
- 单表索引数量:5-8个为宜
- 最多不超过15个
- 联合索引列数:2-4列为宜
- 最多不超过5列
识别冗余索引
-- 冗余场景1:完全包含
INDEX(a, b, c)
INDEX(a, b) -- ❌ 冗余,删除
-- 冗余场景2:索引前缀相同
INDEX(a, b, c)
INDEX(a, b, d) -- ⚠️ 可能冗余,看查询模式
-- 冗余场景3:主键包含
PRIMARY KEY(id)
INDEX(id) -- ❌ 冗余,删除
-- 查询冗余索引(MySQL 8.0+)
SELECT * FROM sys.schema_redundant_indexes;
原则7:字符串索引优化
前缀索引
-- 长字符串使用前缀索引
-- 原始:INDEX(email) -- email平均50字符
-- 优化:INDEX(email(10)) -- 只索引前10个字符
-- 确定合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS sel_5,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS sel_10,
COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) AS sel_15,
COUNT(DISTINCT email) / COUNT(*) AS sel_full
FROM users;
-- 选择选择性接近完整列的最短长度
哈希索引模拟
-- 对于很长的字符串或TEXT类型
ALTER TABLE articles ADD COLUMN title_hash INT UNSIGNED;
CREATE INDEX idx_title_hash ON articles(title_hash);
-- 应用层计算哈希
INSERT INTO articles (title, title_hash)
VALUES ('...', CRC32('...'));
-- 查询时使用哈希
SELECT * FROM articles
WHERE title_hash=CRC32('keyword') AND title='keyword';
-- 注意:必须同时检查原始值(防止哈希碰撞)
原则8:特殊字段处理
NULL值处理
-- NULL值可以使用索引,但有限制
INDEX(column)
SELECT * FROM table WHERE column IS NULL; -- ✅ 走索引
-- 但对于联合索引
INDEX(a, b)
SELECT * FROM table WHERE a IS NULL AND b=?; -- ⚠️ 可能不走索引
-- 建议:重要字段设置 NOT NULL + DEFAULT
枚举类型
-- 使用ENUM或TINYINT存储固定状态
CREATE TABLE orders (
status ENUM('pending','paid','shipped','completed','cancelled'),
INDEX idx_status(status)
);
-- 优点:
-- 1. 存储空间小(1-2字节)
-- 2. 索引空间小
-- 3. 比较速度快
三、索引设计流程
Step 1:分析查询模式
-- 收集慢查询日志
SELECT
query_time,
sql_text,
rows_examined,
rows_sent
FROM mysql.slow_log
WHERE query_time > 1
ORDER BY query_time DESC;
-- 分析查询频率
-- 工具:pt-query-digest (Percona Toolkit)
Step 2:评估字段特征
-- 检查字段选择性
SELECT
'user_id' AS field,
COUNT(DISTINCT user_id) / COUNT(*) AS selectivity
FROM orders
UNION ALL
SELECT
'status',
COUNT(DISTINCT status) / COUNT(*)
FROM orders;
Step 3:设计索引方案
-- 根据查询模式设计
-- 模式1:单条件高频查询 → 单列索引
-- 模式2:组合条件高频查询 → 联合索引
-- 模式3:覆盖查询 → 包含SELECT列的索引
Step 4:验证与优化
-- 创建索引
CREATE INDEX idx_xxx ON table(...);
-- 验证执行计划
EXPLAIN SELECT ...;
-- 对比性能
-- 创建索引前后的查询时间对比
-- 监控索引使用
SELECT * FROM sys.schema_index_statistics
WHERE table_name='your_table';
四、实战案例
案例1:电商订单表
-- 表结构(简化)
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
total_amount DECIMAL(10,2),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
-- 查询分析
-- Q1(40%):用户查自己订单,按时间倒序
SELECT * FROM orders
WHERE user_id=?
ORDER BY create_time DESC
LIMIT 10;
-- Q2(30%):用户查特定状态订单
SELECT * FROM orders
WHERE user_id=? AND status=?
ORDER BY create_time DESC;
-- Q3(20%):管理员查特定状态订单
SELECT * FROM orders
WHERE status=? AND create_time >= ?;
-- Q4(10%):统计用户订单金额
SELECT user_id, SUM(total_amount)
FROM orders
WHERE create_time >= ?
GROUP BY user_id;
-- 索引方案:
INDEX(user_id, create_time) -- 覆盖Q1
INDEX(user_id, status, create_time) -- 覆盖Q1+Q2
INDEX(status, create_time) -- 覆盖Q3
INDEX(create_time, user_id, total_amount) -- 覆盖Q4(覆盖索引)
-- 最终优化(减少冗余):
INDEX(user_id, status, create_time) -- 主索引
INDEX(status, create_time) -- 管理员查询
INDEX(create_time) -- 时间范围查询
案例2:社交媒体帖子表
CREATE TABLE posts (
id BIGINT PRIMARY KEY,
user_id BIGINT,
content TEXT,
is_deleted TINYINT DEFAULT 0,
publish_time DATETIME,
like_count INT DEFAULT 0,
INDEX idx_user_time(user_id, publish_time),
INDEX idx_time_like(publish_time, like_count),
INDEX idx_del_time(is_deleted, publish_time)
);
-- 设计考虑:
-- 1. user_id+publish_time:用户时间线查询
-- 2. publish_time+like_count:热门内容查询(覆盖索引)
-- 3. is_deleted+publish_time:支持软删除过滤
五、常见错误
错误1:为所有字段建索引
-- ❌ 错误做法
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
email VARCHAR(100),
phone VARCHAR(20),
city VARCHAR(50),
INDEX(name),
INDEX(age),
INDEX(email),
INDEX(phone),
INDEX(city) -- 索引过多
);
错误2:忽略联合索引顺序
-- 查询:WHERE status=? AND user_id=?
-- ❌ INDEX(user_id, status) -- user_id区分度高但不常单独查
-- ✅ INDEX(status, user_id) -- status常单独查,放前面
错误3:过度使用前缀索引
-- ❌ 前缀太短
INDEX(email(3)) -- 'abc@...'、'abc@...' 前缀重复太多
-- ✅ 合理长度
INDEX(email(15)) -- 保证足够的区分度
六、面试总结
MySQL索引设计的核心原则:
- 针对性:为高频查询条件建索引
- 选择性:优先高区分度字段
- 顺序性:联合索引遵循最左前缀,高频/等值/高区分度列优先
- 覆盖性:尽量实现覆盖索引,减少回表
- 适度性:避免过度索引,权衡查询与写入性能
- 前缀性:长字符串使用前缀索引
- 非空性:重要字段设置NOT NULL
- 监控性:定期分析索引使用情况,删除无效索引
设计流程:
- 分析查询模式(慢查询日志)
- 评估字段特征(选择性、数据类型)
- 设计索引方案(单列/联合/覆盖)
- 验证与优化(EXPLAIN分析)
关键权衡:
- 查询性能 vs 写入性能
- 索引数量 vs 索引效果
- 完整索引 vs 前缀索引
- 单列索引 vs 联合索引
在面试中能系统地阐述这些原则,并结合实际案例说明,体现了扎实的索引设计能力。