一、直接答案
不是,联合索引不是越多越好。过多的联合索引会带来:
- 写入性能下降:每次INSERT/UPDATE/DELETE都要维护所有索引
- 存储空间浪费:每个索引都占用磁盘空间
- 内存压力增大:Buffer Pool需要缓存更多索引页
- 优化器困惑:索引太多可能导致选择失误
- 维护成本增加:索引越多,管理和优化越复杂
二、联合索引的代价
1. 写入性能影响
代价分析
-- 假设表有3个联合索引
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_user_time(user_id, create_time),
INDEX idx_status_time(status, create_time)
);
单次INSERT操作的成本:
1. 写入主键索引(聚簇索引)
2. 写入 idx_user_status
3. 写入 idx_user_time
4. 写入 idx_status_time
5. 可能触发索引页分裂
6. 写入 undo log
7. 写入 redo log
总成本 = 4次B+树写入 + 日志开销
性能测试
-- 测试表:无额外索引
CREATE TABLE test1 (
id INT PRIMARY KEY AUTO_INCREMENT,
a INT, b INT, c INT, d INT, data VARCHAR(100)
);
-- 测试表:有5个联合索引
CREATE TABLE test2 (
id INT PRIMARY KEY AUTO_INCREMENT,
a INT, b INT, c INT, d INT, data VARCHAR(100),
INDEX idx_ab(a, b),
INDEX idx_ac(a, c),
INDEX idx_ad(a, d),
INDEX idx_bc(b, c),
INDEX idx_cd(c, d)
);
-- 插入测试(10万行)
-- test1: 1.2秒
-- test2: 5.8秒(慢约5倍)
-- UPDATE测试(更新1万行)
-- test1: 0.3秒
-- test2: 2.1秒(慢约7倍)
2. 存储空间占用
空间计算
-- 表数据:1000万行,每行约200字节
-- 表大小:10,000,000 × 200字节 ≈ 2GB
-- 单个联合索引大小(平均)
-- 索引 idx_user_status_time (BIGINT + TINYINT + DATETIME + 主键)
-- 每行索引记录:8 + 1 + 8 + 8 = 25字节
-- 索引大小:10,000,000 × 25字节 ≈ 250MB
-- 如果有8个联合索引:
-- 总索引大小:250MB × 8 ≈ 2GB
-- 总占用:表(2GB) + 索引(2GB) = 4GB
实际案例
-- 查看表和索引大小
SELECT
table_name,
ROUND(data_length/1024/1024, 2) AS data_mb,
ROUND(index_length/1024/1024, 2) AS index_mb,
ROUND((data_length + index_length)/1024/1024, 2) AS total_mb,
ROUND(index_length/data_length*100, 2) AS index_ratio
FROM information_schema.TABLES
WHERE table_schema='your_db' AND table_name='orders';
-- 典型结果:
-- data_mb: 2000
-- index_mb: 1800 ← 索引占了90%的空间
-- index_ratio: 90%
3. 内存压力
Buffer Pool 竞争
InnoDB Buffer Pool 分配:
- 数据页缓存:60-70%
- 索引页缓存:20-30%
- 其他(自适应哈希等):10%
索引越多 → 索引页越多 → Buffer Pool 命中率下降
案例分析
-- 配置:innodb_buffer_pool_size = 8GB
-- 表数据:5GB
-- 索引总量:10GB(索引过多)
-- 结果:
-- 1. Buffer Pool 无法完全缓存索引
-- 2. 频繁换页,导致磁盘IO增加
-- 3. 查询性能下降
-- 监控命令:
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- Innodb_buffer_pool_read_requests: 10000000(内存读)
-- Innodb_buffer_pool_reads: 1000000(磁盘读)
-- 命中率:90%(理想应 > 99%)
4. 优化器选择困难
问题描述
-- 表有10个联合索引
CREATE TABLE orders (
id BIGINT,
user_id BIGINT,
merchant_id BIGINT,
status TINYINT,
create_time DATETIME,
update_time DATETIME,
INDEX idx_user_status(user_id, status),
INDEX idx_user_time(user_id, create_time),
INDEX idx_user_update(user_id, update_time),
INDEX idx_merchant_status(merchant_id, status),
INDEX idx_merchant_time(merchant_id, create_time),
INDEX idx_status_time(status, create_time),
INDEX idx_status_update(status, update_time),
INDEX idx_time_status(create_time, status),
INDEX idx_update_status(update_time, status),
INDEX idx_user_merchant(user_id, merchant_id)
);
-- 查询
SELECT * FROM orders
WHERE user_id=? AND status=? AND create_time >= ?;
优化器困境:
可选索引:
1. idx_user_status(用user_id和status,时间范围过滤)
2. idx_user_time(用user_id和create_time,status过滤)
3. idx_status_time(用status和create_time,user_id过滤)
优化器需要:
- 评估每个索引的成本
- 评估是否使用索引合并
- 统计信息可能不准确
- 可能做出错误选择
结果:
- 优化器耗时增加
- 可能选择次优索引
- 执行计划不稳定
错误选择案例
-- 期望走 idx_user_time(user_id区分度高)
-- 实际走 idx_status_time(统计信息误导)
EXPLAIN SELECT * FROM orders
WHERE user_id=12345 AND status=1 AND create_time >= '2025-01-01';
-- 结果:
key: idx_status_time -- 错误选择
rows: 500000 -- 扫描了50万行
-- 强制使用正确索引:
EXPLAIN SELECT * FROM orders FORCE INDEX(idx_user_time)
WHERE user_id=12345 AND status=1 AND create_time >= '2025-01-01';
-- 结果:
key: idx_user_time -- 正确选择
rows: 100 -- 只扫描100行
三、合理数量建议
1. 经验数值
单表联合索引建议:
- 小表(<1万行):0-2个
- 中表(1万-100万行):2-5个
- 大表(>100万行):3-8个
- 超大表(>1亿行):根据查询模式,不超过10个
总索引数(包括单列索引):
- 建议:5-10个
- 警戒线:15个
- 危险线:20个以上
2. 判断标准
标准1:覆盖核心查询
-- 分析慢查询日志
SELECT
sql_text,
count_star,
avg_timer_wait / 1000000000000 AS avg_seconds
FROM sys.x$statements_with_runtimes_in_95th_percentile
ORDER BY avg_timer_wait DESC
LIMIT 10;
-- 为Top 10慢查询设计索引
-- 不要为所有可能的查询都建索引
标准2:查询频率权衡
-- 查询频率分析
-- 高频查询(QPS > 100):必须有优化索引
-- 中频查询(QPS 10-100):建议有索引
-- 低频查询(QPS < 10):可以容忍慢查询,不建索引
标准3:写入比例
读写比例判断:
- 读多写少(10:1以上):可以多建索引(6-10个)
- 读写均衡(3:1左右):适度建索引(4-6个)
- 写多读少(1:1以下):少建索引(2-4个)
四、优化策略
策略1:索引合并
原理
-- 不好:为所有组合建索引
INDEX(a, b)
INDEX(a, c)
INDEX(b, c)
INDEX(a, b, c)
-- 好:建少量高效索引
INDEX(a, b, c) -- 覆盖大部分查询
INDEX(b) -- 单独查b时使用
INDEX(c) -- 单独查c时使用
合并规则
合并原则:
1. 最左前缀:INDEX(a,b,c) 可以替代 INDEX(a) 和 INDEX(a,b)
2. 高频覆盖:优先保留高频查询的索引
3. 去除冗余:删除完全被包含的索引
示例:
已有 INDEX(user_id, status, create_time)
可删除:
- INDEX(user_id)
- INDEX(user_id, status)
保留(如果需要):
- INDEX(status, create_time) -- 不同查询模式
策略2:查询改写
方案A:统一查询模式
-- 原查询(需要多个索引)
-- 查询1:WHERE user_id=?
-- 查询2:WHERE user_id=? AND status=?
-- 查询3:WHERE user_id=? AND status=? AND create_time>=?
-- 原索引方案(3个索引)
INDEX(user_id)
INDEX(user_id, status)
INDEX(user_id, status, create_time)
-- 优化:统一使用一个索引
INDEX(user_id, status, create_time)
-- 查询1改写(可选)
WHERE user_id=? AND status IS NOT NULL -- 仍走索引
方案B:OR改UNION
-- 原查询(需要多个索引支持OR)
SELECT * FROM orders
WHERE user_id=? OR merchant_id=?;
-- 需要:INDEX(user_id), INDEX(merchant_id) + 索引合并
-- 改写为UNION
SELECT * FROM orders WHERE user_id=?
UNION
SELECT * FROM orders WHERE merchant_id=?;
-- 同样的索引,但执行计划更可控
策略3:分表分库
垂直分表
-- 原表:字段多,索引多
CREATE TABLE orders (
id BIGINT,
user_id BIGINT,
merchant_id BIGINT,
status TINYINT,
create_time DATETIME,
-- 20个其他字段...
-- 15个索引...
);
-- 拆分:核心表 + 扩展表
CREATE TABLE orders_core (
id BIGINT PRIMARY KEY,
user_id BIGINT,
merchant_id BIGINT,
status TINYINT,
create_time DATETIME,
INDEX idx_user_status_time(user_id, status, create_time),
INDEX idx_merchant_time(merchant_id, create_time)
-- 只保留高频查询的索引
);
CREATE TABLE orders_ext (
order_id BIGINT PRIMARY KEY,
-- 其他20个字段
-- 2-3个低频查询索引
);
水平分表
-- 按时间分表
orders_2025_01
orders_2025_02
...
-- 好处:
-- 1. 每个分表索引更小
-- 2. 历史表可以减少索引(只读场景)
-- 3. 查询只命中相关分表
策略4:定期审计
监控脚本
-- 1. 查找未使用的索引
SELECT
t.table_schema,
t.table_name,
s.index_name,
s.cardinality
FROM information_schema.statistics s
JOIN information_schema.tables t
ON s.table_name = t.table_name
LEFT JOIN sys.schema_unused_indexes u
ON u.object_schema = t.table_schema
AND u.object_name = t.table_name
AND u.index_name = s.index_name
WHERE t.table_schema = 'your_db'
AND u.index_name IS NOT NULL;
-- 2. 查找冗余索引
SELECT * FROM sys.schema_redundant_indexes
WHERE table_schema='your_db';
-- 3. 分析索引大小
SELECT
table_name,
index_name,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS size_mb
FROM mysql.innodb_index_stats
WHERE database_name='your_db' AND stat_name='size'
ORDER BY stat_value DESC;
五、实战案例
案例1:电商订单表优化
优化前
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
merchant_id BIGINT,
status TINYINT,
create_time DATETIME,
update_time DATETIME,
total_amount DECIMAL(10,2),
-- 12个索引(过多)
INDEX idx_user(user_id),
INDEX idx_user_status(user_id, status),
INDEX idx_user_time(user_id, create_time),
INDEX idx_user_status_time(user_id, status, create_time),
INDEX idx_merchant(merchant_id),
INDEX idx_merchant_status(merchant_id, status),
INDEX idx_merchant_time(merchant_id, create_time),
INDEX idx_status(status),
INDEX idx_status_time(status, create_time),
INDEX idx_time(create_time),
INDEX idx_update(update_time),
INDEX idx_amount(total_amount)
);
-- 问题:
-- 1. 写入性能差(13个B+树维护)
-- 2. 占用空间大(索引占表的120%)
-- 3. 优化器经常选错
优化后
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
merchant_id BIGINT,
status TINYINT,
create_time DATETIME,
update_time DATETIME,
total_amount DECIMAL(10,2),
-- 5个索引(精简)
INDEX idx_user_status_time(user_id, status, create_time),
INDEX idx_merchant_status_time(merchant_id, status, create_time),
INDEX idx_status_time(status, create_time),
INDEX idx_time_status(create_time, status),
INDEX idx_update(update_time)
);
-- 改进:
-- 1. 写入性能提升 70%
-- 2. 空间占用减少 60%
-- 3. 查询性能不降反升(优化器选择更准确)
查询覆盖分析
-- 高频查询1(50%):用户查自己订单
WHERE user_id=? ORDER BY create_time DESC
→ 使用 idx_user_status_time(最左前缀)
-- 高频查询2(30%):商家查订单
WHERE merchant_id=? AND status=?
→ 使用 idx_merchant_status_time
-- 高频查询3(15%):管理员按状态查
WHERE status=? AND create_time>=?
→ 使用 idx_status_time
-- 高频查询4(5%):按时间范围查
WHERE create_time BETWEEN ? AND ?
→ 使用 idx_time_status
-- 覆盖率:100%
-- 索引数:从12个减少到5个
案例2:社交媒体帖子表
分析
-- 业务特点:
-- 1. 写入非常频繁(用户发帖)
-- 2. 读取也很频繁(浏览帖子)
-- 3. 读写比约 5:1
-- 索引策略:
-- 由于写入频繁,索引要精简
INDEX(author_id, publish_time) -- 用户时间线
INDEX(publish_time, like_count) -- 热门内容(覆盖索引)
INDEX(topic_id, publish_time) -- 话题浏览
-- 只保留3个高频索引,写入性能优先
六、监控和告警
1. 关键指标
-- 监控指标1:索引使用率
SELECT
index_name,
rows_selected,
rows_inserted,
rows_updated,
rows_deleted,
ROUND(rows_selected / (rows_inserted + rows_updated + rows_deleted + 1), 2)
AS read_write_ratio
FROM sys.schema_index_statistics
WHERE table_name='orders'
ORDER BY rows_selected DESC;
-- 监控指标2:写入性能
SHOW STATUS LIKE 'Handler_write';
SHOW STATUS LIKE 'Innodb_rows_inserted';
-- 监控指标3:索引大小占比
-- (如前述查询)
2. 告警规则
告警阈值:
1. 索引总大小 > 表大小:警告
2. 索引总大小 > 表大小 * 1.5:严重
3. 单表索引数 > 15:警告
4. 单表索引数 > 20:严重
5. 写入TPS下降 > 30%:警告(可能索引过多)
6. Buffer Pool命中率 < 95%:警告
七、面试总结
联合索引不是越多越好,需要权衡以下因素:
过多索引的代价:
- 写入性能:每个索引都需要维护,写入放大N倍
- 存储空间:索引可能占表大小的50%-100%
- 内存压力:Buffer Pool无法完全缓存,命中率下降
- 优化器困惑:索引太多可能导致选择失误
- 维护成本:管理和优化复杂度增加
合理数量:
- 中小表:3-5个联合索引
- 大表:5-8个联合索引
- 总索引数(包括单列):不超过15个
优化策略:
- 索引合并:利用最左前缀,减少冗余
- 查询改写:统一查询模式,减少索引需求
- 定期审计:删除未使用和冗余索引
- 读写权衡:读多写少可以多索引,反之少索引
判断标准:
- 覆盖核心高频查询(80%以上)
- 考虑写入性能影响
- 监控索引使用情况
- 实测性能对比
关键是找到查询性能和写入性能的平衡点,而不是盲目追求更多索引。
这道题考查对索引成本的全面理解和实战经验,能体现候选人的综合能力。