深蓝黑背景品牌封面:左侧大标题「建索引之前,先想清楚」,副标题「选择性、读写比与联合索引顺序」;右侧原创装饰——一枚放大镜悬在数据表格上方,镜筒引出三个标签:查得多吗、重复多吗、写得勤吗,示意建索引前的三问;左上标注「经典题解析 · 数据库」,右上标注「Day 06 · 10-04」 图 1:本文封面,点明建索引前的三个关键问题。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「创建数据库索引应该怎么考虑」——这道题不问原理,问的是决策。很多人背得出 B+ 树结构,却在生产表上乱建索引:查询没快多少,写入先慢了一截。

先把成本摆上桌面,因为索引从来不是免费的。**每多一个索引,每次写入都要多维护一棵索引树;表多占一份磁盘;优化器还多一个可能选错的对象。**所以建索引的正确姿势不是「怎么建」,而是「该不该建、建在哪、建成什么样」。下面四问,按顺序走完再动手。

第一问:这列会出现在查询条件里吗?

这是门槛问题。不出现在 WHERE、JOIN、ORDER BY、GROUP BY 里的列,建了索引也没人用,纯属给写入上税。判断依据不是「我觉得以后会查」,而是慢查询日志和真实的业务 SQL。索引跟着查询走,不跟着直觉走。

第二问:这列的选择性高吗?

选择性 = 不重复值个数 ÷ 总行数,越接近 1 越好。手机号、订单号接近唯一,一次索引定位精准命中一行;性别、状态标志只有几个值,索引过滤后还剩半张表——优化器一看扫描比例太高,干脆放弃索引走全表扫,这个索引就白建了。MySQL 的 SHOW INDEX 输出里就有 Cardinality(基数)字段,它是不重复值的估算,基数越高的索引越可能被优化器相中。

实测:选择性差距有多夸张。

模拟一张 10 万行的用户表,算性别和手机号两列的选择性与过滤效果,Node.js 22.15.0 直接运行(node index-selectivity.js):

'use strict';

// 模拟一张 10 万行的用户表,算两列的选择性与过滤效果
const ROWS = 100000;

// 可复现的伪随机数
let seed = 42;
const rand = () => (seed = (seed * 1103515245 + 12345) % 2 ** 31) / 2 ** 31;

const genders = ['男', '女'];
const rows = [];
for (let i = 0; i < ROWS; i++) {
  rows.push({
    gender: genders[Math.floor(rand() * 2)],
    phone: `138${String(10000000 + i)}`, // 接近唯一
  });
}

// 选择性 = 不重复值个数 / 总行数,越接近 1 越值得建索引
const distinct = (column) => new Set(rows.map((r) => r[column])).size;
for (const column of ['gender', 'phone']) {
  const cardinality = distinct(column);
  const selectivity = cardinality / ROWS;
  const matched = rows.filter((r) => r[column] === rows[0][column]).length;
  console.log(
    `${column}:基数 ${cardinality},选择性 ${selectivity},查一行平均过滤后约剩 ${matched} 行`,
  );
}

// 结论对照:同样是「查一个值」,低选择性列几乎过滤不掉行
console.log('gender 索引要扫的比例:', (rows.filter((r) => r.gender === '男').length / ROWS * 100).toFixed(1) + '%');
console.log('phone 索引要扫的比例:', (rows.filter((r) => r.phone === rows[0].phone).length / ROWS * 100).toFixed(5) + '%');

实测输出:

gender:基数 2,选择性 0.00002,查一行平均过滤后约剩 48789 行
phone:基数 100000,选择性 1,查一行平均过滤后约剩 1 行
gender 索引要扫的比例: 51.2%
phone 索引要扫的比例: 0.00100%

同样是「按一个值查」,性别列过滤后还剩五万行,手机号列只剩一行。数字面前,「性别列要不要建索引」根本不用争论。

第三问:这张表的读写比如何?

索引的收益在查询侧,成本在写入侧。读多写少的配置表、字典表,可以放心建;写多读少的流水表、日志表,每加一个索引都在拖慢本就吃紧的写入——这种表要克制,只保留真正服务查询的最小索引集,甚至考虑以批处理、覆盖扫描等替代方案。

第四问:单列还是联合,列的顺序怎么排?

多列经常一起出现在查询条件里,就该考虑联合索引而不是一堆单列索引——一个设计正确的联合索引可以服务多种查询组合。排顺序的经验法则:等值查询、选择性高的列放前面,范围查询的列放后面。背后的原理是最左前缀:索引按定义顺序排序,查询条件从左边第一列开始连续命中才能用上索引;范围条件之后的列就只能用来过滤、用不上有序性了。

决策图:给一张表建索引前按四问走决策路径——第一问「这列会出现在查询条件里吗」,不出现就不建;第二问「列的选择性高吗」,性别这类只有两个值的列过滤不掉多少行,不值得建,手机号这类接近唯一的列值得建;第三问「表的读写比如何」,写多读少的表每加一个索引都拖慢写入,要克制;第四问「单列还是联合」,多列常一起查时建联合索引,把选择性高、等值查询的列放前面,范围查询的列放后面。右侧给出值得建与不值得建的清单对比:值得——高频查询条件、高选择性、关联键、排序分组列;不值得——低选择性列、频繁更新的列、几乎不查的列、小表全表扫更快。底部提示:索引的成本是写入变慢、空间变大、优化器选错索引的风险,收益必须盖过成本才建 图 2:四问决策路径与「值得建 / 不值得建」清单。

一张对照清单收拢四问。

值得建:高频查询条件列、高选择性列、JOIN 关联键、排序与分组列。不值得建:低选择性列(性别、布尔标志)、频繁更新的列(索引维护成本随更新次数放大)、几乎不出现在查询里的列、以及小表——几百行的表全表扫比走索引还快,优化器自己就会这么选。

易错点清单。

一,「索引越多查询越快」——写入放大和优化器选错索引的代价会教你做人。二,给低选择性列建单列索引,还奇怪为什么 EXPLAIN 里用不上。三,联合索引列序随手排,把范围列放最前,后面的等值列全部用不上有序性。四,建完不看执行计划——索引是假设,EXPLAIN 才是验证;线上跑两周,慢查询日志会告诉你假设对不对。

回到面试现场,建议的答题骨架:先立成本观(索引拿写入和空间换查询),再按「查不查、选择性、读写比、单列还是联合」四问展开,最后补一句「建完用 EXPLAIN 和慢查询日志验证」。AI 业务表增长快、查询模式变化也快,这套「按查询模式与列选择性做决策」的习惯,比背任何索引口诀都耐用。

你给线上表加索引时踩过什么坑?是写入变慢还是优化器不领情?欢迎分享。