索引选择性是判断一个索引是否值得创建的关键指标,它反映的是索引列中不同值的分布情况。很多看似合理的索引在实际查询中效果很差,根源往往在于选择性评估不准确。要准确评估选择性,必须从字段基数分析入手。

一、索引选择性与字段基数的核心概念
索引选择性(Index Selectivity)指的是索引列中不同值的数量与表总行数的比值。例如一张订单表有100万行,状态列只有待支付、已支付、已取消3个不同值,那么该列的选择性就是3/1000000,约为0.000003,这个值非常低。相反,订单号列每个值都唯一,选择性为1,属于最高选择性。选择性越高,索引过滤数据的能力越强,优化器也越倾向于使用该索引。
字段基数(Column Cardinality)通常指某列中不同值的个数。但是需要注意,基数并不是一个固定不变的绝对指标,它必须与表行数结合才有意义。一个字段基数绝对值为50万,如果表行数是100万,选择性是0.5,已经比较理想;如果表行数是5000万,选择性只有0.01,这时单独在该字段上建索引的收益会大幅下降。因此评估时一定要把基数转化为选择性,再结合查询条件来判断。
在MySQL的InnoDB存储引擎中,SHOW INDEX命令展示的Cardinality列是一种估算值,它并不精确,通常基于采样计算。很多开发者看到Cardinality接近行数就认为选择性很好,但实际数据分布可能严重倾斜,例如某个值占了90%的行。此时即使基数较高,针对高频值的查询走索引也未必比全表扫描快。所以理解基数的统计口径和分布情况,是避免误判的关键。
二、字段基数分析方法:从SQL计算到统计信息
评估字段基数和选择性最直接的方法是自己执行SQL计算。以下语句可以计算单列的选择性、不同值数量以及总行数:
-- 计算指定列的选择性
SELECT
COUNT(DISTINCT column_name) / COUNT(*) AS selectivity,
COUNT(DISTINCT column_name) AS distinct_count,
COUNT(*) AS total_rows
FROM table_name;
这个查询的结果中,selectivity的值在0到1之间,越接近1表示选择性越高。例如商品表的category_id列不同值有200个,总行数是100000,那么选择性是0.002,单独在category_id上建索引效果通常很差。如果列是user_id,不同值有98000个,选择性是0.98,这种列就非常适合创建索引。需要说明的是,COUNT(DISTINCT)在大表上执行可能比较慢,生产环境可以使用采样或依赖数据库统计信息来降低开销。
各个数据库都维护了统计信息,通过系统视图可以查看基数和分布情况。在MySQL中,可以通过下面的命令快速查看一个索引的基数估算值:
-- 查看orders表上所有索引的基数 SHOW INDEX FROM orders;
在PostgreSQL中,可以查询pg_stats视图获取列的不同值数量、最常见值比例、直方图边界等信息,例如SELECT attname, n_distinct, most_common_vals FROM pg_stats WHERE tablename = 'orders';。SQL Server中则可以通过sys.dm_db_stats_histogram查看更细的分布。依赖统计信息分析时要注意,统计信息可能过期,尤其在频繁更新或大量数据变更之后,应该及时执行ANALYZE或更新统计信息命令,否则优化器会因为错误的基数估算而选择错误的执行计划。
除了单列选择性,组合索引的选择性分析同样重要。通常需要评估前导列或组合列的整体选择性。例如查询条件包含WHERE status = 'PAID' AND create_time >= '2025-01-01',单独看status选择性很低,但status与create_time的组合能大幅缩小扫描范围。组合选择性可以用下面的SQL计算:
-- 组合列选择性计算
SELECT
COUNT(DISTINCT CONCAT(status, '|', DATE(create_time))) / COUNT(*) AS combined_selectivity
FROM orders
WHERE status IS NOT NULL
AND create_time IS NOT NULL;
这段代码用CONCAT将两个字段合并后计算不同值数量,再除以总行数得到组合选择性。不过要注意,组合选择性高并不一定意味着必须按这个顺序建立组合索引,还要考虑查询的前缀匹配条件。如果所有查询都包含status和create_time,组合索引的收益会非常明显;如果有些查询只过滤status,那么把status放在前导列可以兼顾更多场景。
三、低选择性字段的优化策略与索引设计
遇到低选择性字段时,直接放弃建索引并不总是正确的。例如状态字段只有3个值,单列索引效果确实很差,但如果业务上90%的查询都带有状态过滤,且返回数据量很小,可以通过组合索引来提升性能。将高选择性字段放在低选择性字段之后往往能取得更好的过滤效果。比如订单查询经常同时使用user_id和status,其中user_id选择性接近1,而status只有5个值,应该建立(user_id, status)索引,而不是(status, user_id)。因为前者能借助前导列快速定位到某个用户的订单,再按状态过滤;后者先按状态过滤仍需扫描大量用户订单。
对于字符串类型的字段,如果完整字段基数很高但前缀重复度大,可以使用前缀索引降低索引体积,同时保持可接受的选择性。例如邮箱地址的前缀长度选择需要评估不同前缀长度的选择性:
-- 评估不同前缀长度的选择性
SELECT
COUNT(DISTINCT LEFT(email, 6)) / COUNT(*) AS prefix6,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS prefix10,
COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) AS prefix15
FROM users;
如果前缀6的选择性已经达到0.9以上,就可以考虑INDEX idx_email_prefix (email(6)),从而减少索引存储空间。但前缀索引无法用于排序和覆盖索引,因此需要根据查询是否需要ORDER BY email来权衡。对于长文本字段,还可以考虑生成哈希列并建立索引,但哈希列无法处理范围查询,只适合等值匹配。
另一种策略是覆盖索引。即使某个字段选择性不高,但如果把查询需要的其他列一起放入索引,形成覆盖索引,优化器仍然可能选择该索引来避免回表。例如SELECT order_no, status FROM orders WHERE status = 'PAID'中,status选择性低,但如果建立(status, order_no)覆盖索引,查询只需要扫描索引叶子节点即可返回结果,避免了访问聚簇索引,这在数据页缓存不足时会有明显收益。判断一个索引是否为覆盖索引,可以使用EXPLAIN查看Using index提示。
四、常见评估误区与持续监控方法
第一个常见误区是只看不同值数量,忽略数据分布倾斜。假设某列有100个不同值,表行数100万,选择性为0.0001,看起来很低,但如果99%的查询都只针对其中的1个值,而该值恰好只有1000行,那么这个索引对该高频查询仍然有效。相反,如果一个值占了80万行,索引访问该值几乎等于全表扫描,优化器往往会放弃索引。因此分析选择性时最好结合histogram或分桶查询观察值分布。
第二个误区是使用函数或隐式类型转换导致选择性评估失真。例如字段create_time是datetime类型,查询写成WHERE DATE(create_time) = '2025-03-30',即使create_time上有索引,优化器也无法使用,因为索引存储的是原始值,函数会破坏索引匹配。类似地,字符串字段与数值比较可能触发隐式转换,使索引失效。这类问题与选择性评估无关,但会在执行计划中表现为全表扫描或索引扫描范围扩大,排查时可以结合EXPLAIN和SHOW WARNINGS查看改写后的条件。
索引选择性不是一次性评估,随着数据增长和分布变化,原本高效的索引可能逐渐劣化。建议定期采集关键表的行数、基数、选择性指标,并对比历史趋势。例如可以每周执行一次ANALYZE TABLE或UPDATE STATISTICS,同时记录SHOW INDEX中Cardinality的变化。如果某个索引的基数增长缓慢而行数增长迅速,说明选择性下降,需要结合慢查询日志判断是否调整索引设计。借助数据库自带的性能视图,如MySQL的sys.schema_unused_indexes和sys.schema_index_statistics,还能识别出从未使用或已冗余的索引,及时清理可以降低写操作开销。