一、答案概要
不一定没用。区分度不高的字段建索引是否有效,取决于以下因素:
- 数据分布:实际过滤的数据量
- 查询模式:是否与其他条件组合
- 表规模:表的总行数
- 业务场景:查询频率和性能要求
二、核心概念
1. 什么是区分度(选择性)
选择性 = COUNT(DISTINCT column) / COUNT(*)
区分度分类:
- 高区分度(0.8-1.0):用户ID、订单号、邮箱
- 中区分度(0.1-0.8):城市、年龄、职业
- 低区分度(0-0.1):性别、状态、布尔值
2. 低区分度字段示例
-- 表:1000万行数据
-- gender:只有 'M'/'F' 两个值
SELECT COUNT(DISTINCT gender) FROM users; -- 结果:2
-- 选择性:2 / 10,000,000 = 0.0000002
-- status:有 5 个状态值
SELECT COUNT(DISTINCT status) FROM orders; -- 结果:5
-- 选择性:5 / 10,000,000 = 0.0000005
-- is_deleted:软删除标记 0/1
SELECT COUNT(DISTINCT is_deleted) FROM posts; -- 结果:2
-- 选择性:2 / 10,000,000 = 0.0000002
三、适用场景分析
场景1:数据分布极度不均衡
典型案例
-- 订单表:1000万行
-- 状态分布:
-- cancelled: 9,900,000 行 (99%)
-- active: 100,000 行 (1%)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
status VARCHAR(20),
INDEX idx_status(status)
);
-- 查询:查找活跃订单
SELECT * FROM orders WHERE status='active';
分析
不建索引:
- 扫描行数:10,000,000
- 返回行数:100,000
- 扫描率:100%
建立索引:
- 扫描行数:100,000(通过索引直接定位)
- 返回行数:100,000
- 扫描率:1%
- 性能提升:100倍
关键点
✅ 有效场景:
- 数据分布严重倾斜
- 查询条件集中在少数值
- 过滤效果达到 90%+
❌ 无效场景:
- 查询条件平均分布
- 过滤后仍有大量数据(如50%)
场景2:作为联合索引的一部分
典型案例
-- 订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status TINYINT, -- 只有5个值,区分度低
create_time DATETIME,
-- 联合索引
INDEX idx_user_status_time(user_id, status, create_time)
);
-- 高频查询
SELECT * FROM orders
WHERE user_id=? AND status=?
ORDER BY create_time DESC;
分析
为什么有效?
1. 虽然 status 区分度低,但在联合索引中起到进一步过滤作用
2. user_id 先过滤到用户级别(假设1000行)
3. status 再过滤到特定状态(假设200行)
4. create_time 用于排序
单独 status 索引:无效(区分度太低)
联合索引中的 status:有效(组合后区分度提升)
组合后的选择性
-- 单独字段选择性
SELECT COUNT(DISTINCT status) / COUNT(*) FROM orders;
-- 结果:0.0000005(几乎无用)
-- 组合字段选择性
SELECT COUNT(DISTINCT user_id, status) / COUNT(*) FROM orders;
-- 结果:0.05(明显提升)
-- 三列组合选择性
SELECT COUNT(DISTINCT user_id, status, DATE(create_time)) / COUNT(*)
FROM orders;
-- 结果:0.8(高区分度)
场景3:覆盖索引优化
典型案例
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
gender ENUM('M', 'F'),
city VARCHAR(50),
INDEX idx_city_gender_name(city, gender, name)
);
-- 查询:只需要索引中的列
SELECT name FROM users
WHERE city='Beijing' AND gender='F';
分析
为什么包含 gender?
1. 虽然 gender 区分度低,但可以实现覆盖索引
2. 避免回表操作
3. gender 在索引中只占 1 字节,空间开销小
执行计划:
type: ref
key: idx_city_gender_name
Extra: Using where; Using index ← 覆盖索引
场景4:小表场景
典型案例
-- 配置表:只有100行数据
CREATE TABLE config (
id INT PRIMARY KEY,
category VARCHAR(20), -- 只有3个类别
config_key VARCHAR(50),
config_value TEXT,
INDEX idx_category(category)
);
分析
小表是否需要索引?
数据量 < 1000行:
- 全表扫描非常快(几毫秒)
- 索引维护开销 > 查询优化收益
- 结论:通常不需要索引
数据量 1000-10000行:
- 看查询频率和并发量
- 高并发场景建议建索引
数据量 > 10000行:
- 建议建索引(即使区分度低)
场景5:与其他高区分度字段组合查询
典型案例
-- 文章表
CREATE TABLE articles (
id BIGINT PRIMARY KEY,
author_id BIGINT,
is_published TINYINT, -- 0/1,区分度很低
publish_time DATETIME,
INDEX idx_author_pub_time(author_id, is_published, publish_time)
);
-- 查询:查看某作者的已发布文章
SELECT * FROM articles
WHERE author_id=? AND is_published=1
ORDER BY publish_time DESC;
分析
执行过程:
1. author_id 定位到作者的所有文章(假设100篇)
2. is_published 过滤未发布的(假设剩50篇)
3. publish_time 排序
虽然 is_published 区分度低,但:
- 在已过滤的小范围内(100篇)进一步过滤
- 过滤效果显著(从100篇到50篇)
- 有效利用索引排序
四、优化器的选择逻辑
1. 成本估算
-- MySQL 优化器评估使用索引的成本
全表扫描成本 = 表的数据页数 × IO成本
索引扫描成本 =
+ 索引扫描页数 × IO成本
+ 回表次数 × IO成本
+ CPU处理成本
-- 当:索引扫描成本 < 全表扫描成本 时,使用索引
2. 区分度阈值
经验值:
- 选择性 > 0.1:通常会使用索引
- 选择性 0.01-0.1:根据数据量和分布决定
- 选择性 < 0.01:通常不使用索引(除非数据分布极度倾斜)
但这不是绝对的,MySQL 会动态评估。
3. 实际测试
-- 测试表:1000万行
CREATE TABLE test (
id INT PRIMARY KEY AUTO_INCREMENT,
status TINYINT, -- 1,2,3,4,5 五个值,均匀分布
data VARCHAR(100),
INDEX idx_status(status)
);
-- 测试1:查询20%的数据
EXPLAIN SELECT * FROM test WHERE status IN (1, 2);
-- 结果:type: ALL(全表扫描)
-- 原因:过滤后还有400万行,回表成本太高
-- 测试2:查询1%的数据
EXPLAIN SELECT * FROM test WHERE status=1 AND id > 9900000;
-- 结果:type: range
-- 原因:只需查10万行,索引有效
-- 测试3:覆盖索引
EXPLAIN SELECT COUNT(*) FROM test WHERE status=1;
-- 结果:type: index, Extra: Using index
-- 原因:覆盖索引,无需回表,索引有效
五、具体优化策略
策略1:利用数据分布倾斜
-- 场景:软删除标记
-- 分布:is_deleted=0 (99%), is_deleted=1 (1%)
CREATE TABLE posts (
id BIGINT,
content TEXT,
is_deleted TINYINT DEFAULT 0,
INDEX idx_not_deleted(is_deleted, publish_time)
);
-- 查询未删除的(主要查询)
SELECT * FROM posts
WHERE is_deleted=0 AND publish_time >= ?;
-- ✅ 索引有效
-- 查询已删除的(次要查询)
SELECT * FROM posts WHERE is_deleted=1;
-- ✅ 索引仍有效(只有1%的数据)
策略2:创建联合索引
-- 不好:单独的低区分度索引
INDEX(status)
-- 好:联合索引
INDEX(status, create_time, user_id)
-- 查询:
WHERE status=? AND create_time >= ?
-- 充分利用联合索引
策略3:使用部分索引(MySQL 8.0.13+)
-- 仅为特定值建索引
-- 注意:MySQL暂不直接支持,但可以通过函数索引模拟
-- PostgreSQL 的部分索引示例(参考)
CREATE INDEX idx_active_orders
ON orders(create_time)
WHERE status='active';
策略4:条件下推
-- 利用索引下推(Index Condition Pushdown)
CREATE INDEX idx_status_time ON orders(status, create_time);
SELECT * FROM orders
WHERE status='active'
AND create_time >= '2025-01-01'
AND total_amount > 100; -- 虽然不在索引中,但可以下推
-- Extra: Using index condition
-- total_amount 的过滤在存储引擎层完成,减少回表
六、性能对比实测
测试场景
-- 表:10,000,000 行
-- status: 'A'/'B'/'C' 三个值,均匀分布(各333万行)
-- 测试1:无索引
SELECT COUNT(*) FROM orders WHERE status='A';
-- 执行时间:2.5秒(全表扫描)
-- 测试2:有索引 + 覆盖查询
CREATE INDEX idx_status ON orders(status);
SELECT COUNT(*) FROM orders WHERE status='A';
-- 执行时间:0.3秒(索引扫描,无回表)
-- 测试3:有索引 + 需要回表
SELECT * FROM orders WHERE status='A';
-- 执行时间:8秒(索引扫描 + 333万次回表)
-- 结论:优化器可能选择全表扫描(2.5秒更快)
-- 测试4:联合索引 + 进一步过滤
CREATE INDEX idx_status_time ON orders(status, create_time);
SELECT * FROM orders
WHERE status='A' AND create_time >= '2025-11-01';
-- 执行时间:0.05秒(只需回表1000行)
七、面试陷阱问题
陷阱1:绝对化判断
错误回答:”区分度低的字段建索引没用”
正确回答:
- 需要考虑数据分布、查询模式、表规模
- 数据倾斜场景下很有用
- 联合索引中可以发挥作用
- 覆盖索引场景下有效
陷阱2:只看选择性数值
错误回答:”选择性0.0001,所以不建索引”
正确回答:
- 数值只是参考,不是绝对标准
- 需要结合实际过滤比例
- 查询
WHERE status='rare_value'可能只返回0.1%的数据 - 此时索引非常有效
陷阱3:忽略组合效果
错误回答:”status区分度低,不用加到联合索引中”
正确回答:
- 联合索引中,每增加一列都能提升组合区分度
- status在user_id之后,可以进一步缩小范围
- 空间开销小(status通常1-2字节)
八、实战建议
1. 评估是否建索引
决策流程:
1. 计算选择性
2. 分析数据分布(是否倾斜)
3. 评估查询模式(单独查询 vs 组合查询)
4. 考虑表规模(行数)
5. 实测对比(EXPLAIN + 实际执行时间)
2. 监控索引使用
-- 查看索引使用情况
SELECT
table_name,
index_name,
rows_selected,
rows_inserted,
rows_updated,
rows_deleted
FROM sys.schema_index_statistics
WHERE table_name='orders';
-- 如果 rows_selected=0,考虑删除索引
3. 定期分析
-- 更新统计信息
ANALYZE TABLE orders;
-- 检查索引效率
EXPLAIN SELECT * FROM orders WHERE status='xxx';
-- 对比不同索引方案
EXPLAIN FORMAT=JSON SELECT ...;
九、面试总结
区分度不高的字段建索引不一定没用,关键看场景:
有效场景:
- 数据分布倾斜:查询集中在少数值(如查询1%的活跃订单)
- 联合索引一部分:与高区分度字段组合,提升整体效果
- 覆盖索引:无需回表,索引扫描成本低
- 大表 + 高频查询:即使区分度低,也比全表扫描快
无效场景:
- 数据均匀分布且需要大量回表
- 小表(<1000行)
- 查询过滤后仍有大量数据(>30%)
优化建议:
- 利用数据倾斜特性
- 创建联合索引而非单列索引
- 尽量实现覆盖索引
- 使用EXPLAIN验证实际效果
- 监控索引使用情况
关键原则:不要教条化,要根据实际数据和查询模式灵活决策。
能从多个角度分析这个问题,体现了对MySQL索引机制的深刻理解和实战经验。