Theory · MySQL · InnoDB
16KB 页上的有序结构 —— 树高 3 层撑起 2000 万行,索引的代价与收益全在"页"里
非叶只存键+指针、叶子存记录并双向成链——范围扫描、最左前缀、免排序,全部从结构直接推出
聚簇叶存整行即"表本体";二级叶存索引列+主键,查整行要回表——覆盖索引与 ICP 都是省回表的手段
type 从 const 到 ALL 的阶梯、key_len 判断用到哪一列、Extra 三兄弟定位回表/下推/排序
Why Index Matters
-- orders 表 800 万行,每行约 1KB,没建任何索引 SELECT * FROM orders WHERE user_id = 10086 AND created_at > '2026-08-01'; -- 无索引:从第 1 行开始,一行一行比对 800 万次 -- 全表 ≈ 800万 × 1KB = 8 GB -- 按 16KB 一页读 ≈ 53 万次页访问 -- 顺序读 200MB/s ≈ 40 秒 -- 而且:并发 10 个这样的查询 → 整库被拖垮 -- 加一个 INDEX(user_id, created_at) 之后: -- 沿树下降 3 层定位到 user_id=10086 的起点 = 3 次页访问 -- 沿叶子链表向右扫,扫到 created_at 越界为止 = M 次页访问 -- 命中 200 行 → 总共约 3 + 14 ≈ 17 次页访问 -- 耗时 ≈ 3 毫秒
把整张表的页从头到尾读一遍,每读一行就比对一次 user_id=10086。这叫全表扫描(Full Table Scan)。表多大就要读多少,跟你要几行完全无关——查 1 行和查 100 万行,耗时一样。
它换掉的是IO 次数,代价是空间与写入速度:数据库额外维护一份"按 user_id 排好序的目录",这份目录自己也要占磁盘、每次 INSERT/UPDATE 都要同步更新。所以索引是典型的用空间和写性能换读性能——这也是"索引不是越多越好"的根源(第 16 页)。
① 为什么这个目录恰好是 B+ 树——哈希、B 树、红黑树为什么不行(第 4 页);
② 目录里到底存了什么——决定了哪些查询能用上、哪些用不上(第 7–11 页);
③ 怎么验证用没用上——EXPLAIN 怎么读(第 14–15 页)。
Prerequisites & Glossary
| 术语 | 一句话理解(先记住这个,细节后面展开) |
|---|---|
| 页(Page) | InnoDB 读写磁盘的最小单位,默认 16KB;一次 IO 就是搬一页,不是搬一行 |
| B+ 树 | 一种"矮而宽"的多叉搜索树:只有叶子层存数据,非叶层纯粹指路;叶子之间还串成双向链表 |
| 扇出(Fanout) | 一个节点能指向多少个孩子。扇出越大树越矮 → 需要的 IO 越少 |
| 聚簇索引 | 按主键建的 B+ 树,叶子存整行数据——它就是表本身 |
| 二级索引 | 按别的列建的 B+ 树,叶子只存索引列 + 主键值 |
| 回表 | 二级索引只告诉你主键是多少,要拿整行得再查一次聚簇索引——这第二次查找就叫回表 |
| 覆盖索引 | 要查的列恰好全在索引里,不用回表,Extra 显示 Using index |
| 最左前缀 | 联合索引 (a,b,c) 是按字典序排的一棵树,必须从 a 开始才能用它定位 |
| 选择性 | 一列能区分多少不同的值:count(distinct col)/count(*) 越接近 1,索引越值钱 |
| EXPLAIN | 在 SQL 前加这个词,MySQL 会打印执行计划而不是执行它——看索引用没用上的工具 |
平衡树(数据结构系列)→ B+ 树作为"平衡多叉搜索树"的通用原理、与红黑树/AVL 的分工
LRU 缓存(手撕)→ buffer pool 用什么策略淘汰页(理解"根页常驻内存"为什么成立)
为了不纠结名词,后面统一把索引说成一棵按索引键排好序的树。你只需要盯住一个量:这次查询要读多少页。能用上索引 = 从根下降 3 层直奔目标;用不上 = 把整张表的页过一遍。
Why B+ Tree
开场那条 SQL 之所以能从 53 万次页访问降到 17 次,前提是这份"目录"本身也能被快速查找。所以问题变成:用什么数据结构组织它?
| 结构 | 等值查询 | 范围 / 排序 | 磁盘友好度 | 典型场景 |
|---|---|---|---|---|
| 哈希表 | O(1),最快 | 不支持(无序,前缀匹配也不行) | 一次随机 IO 即定位,但冲突链坏局部性 | MEMORY 引擎;InnoDB 自适应哈希(AHI)只是热点索引页上的缓存加速层 |
| B 树 | O(logN) | 可中序遍历,但跨层往返 | 每个节点都存整行 → 节点少、扇出小、树更高 | 少用;是理解 B+ 树的对照品 |
| B+ 树 | O(logN),稳定(都走到叶) | 原生支持:叶子有序 + 双向链表,O(logN + M) | 非叶只存键+指针 → 扇出大、树矮、IO 少 | InnoDB / MyISAM 索引的统一形态 |
| 跳表 | O(logN),期望 | 支持(底层有序链表) | 节点高度随机、指针散,对磁盘页局部性差 | 内存态为主:Redis ZSet、LSM 的 memtable |
§10.3.9:哈希索引只在 = / <=> / IN() 这类等值比较中占优;优化器无法用哈希索引加速 ORDER BY,也查不了两值之间的范围。MySQL 默认索引用 B-tree——哈希只在显式 ENGINE=MEMORY 或 AHI 里出现。
二叉树扇出=2,2000 万行高约 24 层 = 24 次页 IO;B+ 树把扇出做到上千,3 层就够。数据库要的是让树高 ≈ IO 次数,这决定了"宽而矮"是唯一方向。
Structure · §17.6.2.2
Back-of-envelope
// 数量级推演(bigint 主键 · 单行约 1KB) // 非叶页:每项 = 8B 键 + 6B 页指针 ≈ 14B 扇出 fanout ≈ 16KB / 14B ≈ 1170 // 叶子页:手册 17.6.2.2 —— 顺序插入约 15/16 满 每页行数 ≈ 16KB × 15/16 / 1KB ≈ 15 行 树高 1:1170 行 树高 2:1170 × 15 ≈ 1.8 万 树高 3:1170 × 1170 × 15 ≈ 2053 万 树高 4:再乘 1170 ≈ 240 亿
| 追问点 | 口径 |
|---|---|
| 这是精确值吗? | 数量级推演,非精确:行宽、键长、填充率都影响结果;面试先给公式再给结论 |
| 扇出取决于什么? | 索引键长度——键越长每页放的项越少、扇出越小、树越高(长主键连带抬升所有二级索引,见第 7 页) |
| 2000 万行几次 IO? | 逻辑上 3 次页访问;根页 + 非叶页常驻 buffer pool,冷查询真实磁盘 IO 常只有 1 次(叶子页)→ 缓存淘汰的底层结构见 LRU 缓存 deck(手撕) |
| 为什么按 15/16 算? | 手册:InnoDB 留 1/16 空闲,顺序插入的页约 15/16 满;随机插入只有 1/2~15/16 满 |
Clustered vs Secondary · §17.6.2.1
Primary Key Design
新行总落在叶子链表最右侧的页:手册 §17.6.2.2——顺序插入的页"about 15/16 full",且只在末端偶发分裂,整树几乎无碎空间;主键紧凑(bigint 8B),二级索引也瘦。
随机键落点不可预测:手册同页——随机插入的页只有"from 1/2 to 15/16 full"。目标页常不在缓存 → 每次插入都可能产生随机读 + 页分裂 + 写回;缓存命中率与填充率双双下降,空间浪费可达 1/3 以上。
| 维度 | 自增(AUTO_INCREMENT / 雪花) | UUID v4 等随机键 |
|---|---|---|
| 页填充率 | ≈ 15/16,紧凑 | 1/2 ~ 15/16,碎片多 |
| 页分裂 | 仅末端,代价小 | 任意位置,分裂 + 上层指针更新频繁 |
| buffer pool 命中 | 热点集中在最右页 | 写点分散,冷页挤占缓存 |
| 索引体积 | 8B(bigint)× 每个二级索引 × 每行 | 36 字符(或 16B 二进制)主键膨胀所有二级索引 |
| 代价 | 自增锁(8.0 默认 mode=2 基本无碍,见 lock-internals);单机序列,分布式需改雪花/号段 | 换来全局唯一、客户端可预生成——分布式场景的正当代价 |
Split & Merge · §17.6.2.2
插入的目标页已满 → 新建一页,原页记录按中间点分成两页,上层加一项指针;极端时逐层向上连锁分裂。顺序插入触发"近似顺序分裂"(新页只承接新记录,原页几乎不搬);随机插入触发"中间分裂",原页约一半数据搬家——代价最高的形态。
删除使页占用低于 MERGE_THRESHOLD(默认 50%)时,InnoDB 尝试把它与相邻页合并,合并不动就尽量做数据搬运腾空。随机删除会留下"半空页",直到合并或 purge 后空间被新插入复用。
| 场景 | 机制与影响 |
|---|---|
| 乱序插入批量写入 | 分裂频繁 + 半满页 → 表"变胖"、空间比数据多;重建表(OPTIMIZE TABLE / 空表 ALTER)可重整到紧凑状态 |
| 二级索引大量 UPDATE/DELETE | 二级索引更新 = delete-mark + 插入新记录(不原地更新)→ 页内先膨胀后留垃圾,等 purge 物理清理;机制见 mvcc · 二级索引的 MVCC |
| 自增主键 + 批量删除 | 最右页持续分裂/合并,左侧老页稳定不受扰——所以顺序键对分裂合并都友好 |
| 监控抓手 | SHOW TABLE STATUS 的 Data_length/Index_length 与 Data_free、information_schema.INNODB_SYS_INDEXES;碎片高时考虑重建 |
Composite Index · §10.3.6
| WHERE / ORDER BY 条件 | 索引使用情况 | 原因(回到树的排列) |
|---|---|---|
a=1 | a ✓ | 树按 a 有序 |
a=1 AND b=2 | a ✓ b ✓ | a 等值把 b "钉"进有序段 |
a=1 AND b=2 AND c=3 | a ✓ b ✓ c ✓ | 逐级钉住 |
b=2 / c=3 | ✗(无最左) | 树按 a 排,跳过 a 无序可循(8.0.13+ 少量场景可 Index Skip Scan 兜底,见 §10.2.1.3) |
a=1 AND c=3 | a ✓,c ✗ | 中间缺 b,c 在页内无序;c 条件可走 ICP(见第 11 页) |
a>1 AND b=2 | a ✓(range),b ✗ | 范围列截断:a>1 之后 b 又乱了——"等值续、范围断" |
a=1 ORDER BY b, c | 免排序 | 等值条件钉住 a 后,(b,c) 在页内天然有序 |
ORDER BY a, b(无 WHERE) | 免排序 | 整体字典序直接可用;ORDER BY b 则 filesort |
INDEX(status, created_at) 支撑 WHERE status=? AND created_at>?;若把范围列放中间,后面的列全部截断。范围列截断的本质:B+ 树里只有"前缀固定"的部分才有序。Covering Index & ICP · §10.2.1.6
SELECT 的列全部包含在某棵索引树里 → 在二级索引就地返回,Extra 显示 Using index。天然条件:二级索引叶子已带主键,所以 SELECT id, name 走 idx_name 即覆盖;SELECT * 几乎永远无法覆盖。代价:把列加进索引 = 多一份冗余 = 写放大。
WHERE 中只用索引列就能判定的部分,server 层下推给 InnoDB 在索引条目上先过滤,不满足的不回表。手册:用于 range/ref/eq_ref/ref_or_null 且需读整行的场景;InnoDB 仅对二级索引生效;EXPLAIN 显示 Using index condition;开关 optimizer_switch='index_condition_pushdown=on'(默认开)。
-- INDEX(zipcode, lastname, firstname) · 手册 §10.2.1.6 原例 SELECT * FROM people WHERE zipcode='95054' AND lastname LIKE '%etrunia%' AND address LIKE '%Main Street%'; // 无 ICP:取回所有 zipcode=95054 的整行,server 再过滤 lastname // 有 ICP:lastname LIKE 条件在索引条目上先判 —— 前导 % 用不了索引定位,但能在索引上过滤! // 只有"索引判定通过"的行才回表 → 回表次数大幅下降
ORDER BY · §10.8.2
| 情形(INDEX(a, b)) | 结果 | 说明 |
|---|---|---|
WHERE a=1 ORDER BY b | ✓ 索引序,免排序 | a 等值钉住前缀,b 在有序段内 |
ORDER BY a, b / ORDER BY a | ✓ 索引序 | 直接沿树字典序读 |
ORDER BY b | Using filesort | 无最左前缀 |
ORDER BY a ASC, b DESC(8.0 前建索引) | Using filesort | 5.7 及更早解析并忽略 DESC——索引只按升序存;8.0 起有真降序索引(第 16 页) |
SELECT * ... WHERE a>0 ORDER BY b | 可能仍 filesort | a 是 range,b 无序;优化器也可能弃用索引选全表 + 排序 |
手册 §10.8.2:Using filesort = "MySQL must do an extra pass to find out how to retrieve the rows in sorted order"。不等于落盘:先在会话级排序缓冲里排,放不下才用临时文件做外排归并——小结果集代价有限,大结果集才是真痛点。
同一批 Using temporary(多为 GROUP BY 与 ORDER BY 列不一致、或 DISTINCT/子查询物化)——手册建议"你要查询尽可能快"时优先盯住这两个值。两者都提示"结果集被物化/重排了",往往意味着缺索引或需要改写。
When Indexes Fail
| 场景 | 示例 | 根因 / 补救 |
|---|---|---|
| ① 对列做函数 / 运算 | WHERE DATE(t)='2026-01-01'、WHERE id+1=10 | 对列加工后树序失效;改写成范围条件 t>=… AND t<…,或用 8.0.13+ 函数索引 |
| ② 隐式类型转换(字符串列 vs 数字) | str_col = 1(str_col 是索引字符串列) | 手册 §12.3 原话:"For comparisons of a string column with a number, MySQL cannot use an index on the column";等价于对列套 CAST。反例注意:int_col = '1' 常量转数字,索引仍可用 |
| ③ 隐式字符集/排序规则转换 | JOIN 两表列 utf8mb4_general_ci vs utf8mb4_0900_ai_ci(8.0 默认) | 对一侧列隐式 CONVERT,索引失效;统一 collation 或显式转换到列上 |
| ④ 前导 % 的 LIKE | LIKE '%abc' | 后缀无序,不能定位;例外:查询列全在索引内时可走全索引扫描(type=index)+ ICP 过滤 |
| ⑤ OR 两侧有"无索引侧" | a=1 OR unindexed_col=2 | 任一侧需全表即全表;两索引侧可用 index_merge。补救:UNION ALL 拆分 |
| ⑥ 不等于 / NOT IN | a <> 1 | 语法上可转两个 range(<1 或 >1),不是必然失效——选择性差时优化器按代价弃用;别说死"不走索引" |
| ⑦ 联合索引非最左前缀 | INDEX(a,b) 下 WHERE b=2 | 树的几何结构决定;见第 10 页 |
| ⑧ 优化器主动放弃 | 统计信息过期、选择性差、回表行数过多 | 代价估算判定全表更快;ANALYZE TABLE 刷新统计,或上覆盖索引降低回表成本 |
EXPLAIN 1/2 · §10.8.2
| type(好→坏) | 含义(手册口径) |
|---|---|
| const / system | 最多一行命中:主键/唯一索引全列等值(WHERE pk=1),启动时读入 |
| eq_ref | JOIN 时按主键/非空唯一索引逐行精确匹配——"最好的关联类型" |
| ref | 按非唯一索引(或最左前缀)取所有匹配行 |
| range | 索引范围:= <> > >= < <= BETWEEN IN LIKE |
| index | "same as ALL, except that the index tree is scanned"——全索引扫描(可能顺手覆盖) |
| ALL | 全表扫描,手册:"usually very bad" |
完整阶梯含 fulltext / ref_or_null / index_merge / unique_subquery / index_subquery;日常达标线:核心查询 ≥ range,JOIN 被驱动表 ≥ ref
| key_len 速算(utf8mb4) |
|---|
INT NOT NULL → 4;INT NULL → 4+1=5(NULL 标志 1B) |
BIGINT → 8 |
VARCHAR(64) NOT NULL → 64×4+2(长度前缀)=258 |
VARCHAR(64) NULL → 259 |
CHAR(1)(utf8mb4)→ 4 |
filtered 估算过滤比例。EXPLAIN 2/2 · §10.8.2
-- id select_type table type key key_len rows Extra 1 SIMPLE users ref idx_name 258 1 Using index condition -- 例 1:SELECT id,name WHERE name='Alice' -- → ref + Using index (覆盖,免回表) -- 例 2:SELECT * WHERE name='Alice' AND name LIKE 'A%' -- → ref + Using index condition(ICP:索引上过滤,命中才回表) -- 例 3:SELECT * FROM users WHERE name LIKE '%li%' ORDER BY age -- → index/ALL + Using where; Using filesort -- 例 4:GROUP BY 与 ORDER BY 列不一致 -- → Using temporary; Using filesort
| Extra 值 | 含义(手册口径) |
|---|---|
| Using index | 覆盖索引:只读索引树即得列信息,"without having to do an additional seek" |
| Using index condition | ICP:先按索引条目判条件,通过才读整行 |
| Using where | server 层 WHERE 过滤(注意:type=index/ALL 而无 Using where 时手册提醒可能有错) |
| Using filesort | 额外排序一遍才能按序返回 |
| Using temporary | 建临时表存中间结果(GROUP BY/ORDER BY 不一致、DISTINCT 等) |
Trade-offs · 8.0 Features
| 手段 | 解决什么 | 代价 / 边界 |
|---|---|---|
前缀索引 col(N) | 长字符串列太胖 → 只索引前 N 字符;选择性 = count(distinct left(col,N))/count(*) 逼近 1 即够用 | 索引里没有完整列 → 不能覆盖、不能免 ORDER BY;COUNT 列在索引中不存在 |
| 降序索引(§10.3.13) | INDEX(a ASC, b DESC) 真按降序存键,支撑 ORDER BY a, b DESC 混向排序 | 8.0 起才生效(旧版忽略 DESC);优化器按扫描方向自动选升/降读 |
| 隐藏索引(§10.3.12) | ALTER TABLE … ALTER INDEX idx INVISIBLE——优化器无视但索引仍维护,用于安全下线索引 / 灰度验证 | 仍占空间与写放大;主键不可隐藏;use_invisible_indexes=on 可临时试开 |
| 函数索引(§13.1.15,8.0.13+) | INDEX((DATE(t))) 直接索引表达式——解决"对列做函数必失效" | 等价于"隐藏生成列 + 索引"的语法糖;表达式必须确定性的 |
| 控制索引数量 | 每索引一棵独立 B+ 树,DML 要同步维护所有二级索引 + 写 redo | 写放大 = 行变更 × 索引数;失效索引先 INVISIBLE 观察再 DROP |
Cheat Sheet · 1/2
| 页大小 | 默认 16KB(innodb_page_size),一次 IO = 搬一页 |
| 非叶扇出 | 16KB ÷(8B 键 + 6B 指针 ≈ 14B)≈ 1170 |
| 3 层容量 | 1170 × 1170 × 15 行 ≈ 2000 万行(叶子按 15/16 填充) |
| 等值查询 IO | 逻辑 3 次页访问;根页/非叶页常驻 buffer pool,冷查真实磁盘 IO 常为 1 次 |
| 叶子存什么 | 聚簇 = 整行;二级 = 索引列 + 主键 |
a=1 AND b=2 | ✓✓ 等值逐级"钉住"前缀 |
b=2 | ✗ 无最左,无序可循(8.0.13+ 少量 Skip Scan 兜底) |
a=1 AND c=3 | a ✓,c ✗(但 c 可走 ICP 下推过滤) |
a>1 AND b=2 | a ✓,b ✗ —— 等值续、范围断 |
a=1 ORDER BY b | 免排序(a 钉住后 b 天然有序) |
| 结构性失效(必然) | 对列做函数/运算、字符串列 vs 数字(隐式 CAST)、collation 不一致、前导 % 的 LIKE、非最左前缀 |
| 代价性放弃(看数据分布) | != / NOT IN(其实可转两个 range)、统计信息过期、选择性差、回表代价过高 |
| 隐式转换方向性 | 字符串列 = 1 → 失效(列被 CAST);数字列 = '1' → 不失效(常量被转) |
| filesort 怎么判 | = 需额外排序一步,不等于落盘:小结果集内存排完即走,大结果集才外排归并 |
Cheat Sheet · 2/2
| type(好→坏) | const → eq_ref → ref → range → index → ALL;达标线:核心查询 ≥ range、JOIN 被驱动表 ≥ ref。index ≠ 好(它是全索引扫描) |
| key + key_len | 验证"用没用、用到第几列"。key_len 速算:INT=4、NULL 列 +1、VARCHAR(n) utf8mb4 = 4n+2 |
| rows | 预估扫描行数(估算值,配合 filtered 看过滤比例) |
| Extra | Using index=覆盖(零回表)|Using index condition=ICP(过滤后回表)|filesort/temporary=成本信号 |
| 主键 | 用自增/单调(顺序写 → 15/16 填充、分裂仅末端);UUIDv4 随机写 → 1/2~15/16 填充 + 频繁中间分裂。分布式用单调变体(雪花 / UUIDv7) |
| 主键要短 | 主键被每个二级索引复制一份 → 长主键让所有二级索引变胖、扇出变小 |
| 联合索引顺序 | 等值列在前、范围列收尾(如 INDEX(status, created_at)) |
| 能不能覆盖 | 把高频查询的返回列并进索引 → Using index;代价是写放大 |
| 索引数量 | 每索引一棵树,DML 同步维护 + 写 redo + 从库重放 → 写放大 = 行变更 × 索引数;下线先 INVISIBLE 观察 |
Interview QA · 1/2
先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回第 17 / 18 页速查表。
哈希等值 O(1) 但无序,不支持范围与排序;B 树非叶也存数据,扇出小树高;红黑树二叉太高(2000 万行约 24 层)。B+ 树非叶只导航 → 扇出上千、3 层覆盖千万行,叶子有序链表天然支持范围与排序(§10.3.9)。
非叶每项 8B 键 + 6B 指针 ≈ 14B,16KB/14B ≈ 1170;叶子按 15/16 填充放约 15 行;1170²×15 ≈ 2000 万。强调是数量级推演,扇出对键长敏感。
逻辑 3 次:根 → 非叶 → 叶。根页与非叶页几乎常驻 buffer pool,冷数据真实磁盘 IO 常为 1 次(叶子页)。追问延伸:缓存与淘汰见 LRU 缓存 deck。
聚簇叶存整行(表即索引组织表);二级叶存索引列 + 主键值。按二级索引查整行需拿主键再走聚簇索引 = 回表。主键长则所有二级索引变胖——"short primary key"(§17.6.2.1)。
自增 = 永远写在叶子链最右页:页 15/16 满、分裂仅末端、缓存命中集中。UUID 随机落点:页 1/2~15/16 满、频繁中间分裂、随机 IO。分布式必须预生成时选单调变体(雪花/UUIDv7)。
查询列全在某棵索引树内,二级索引就地返回,省一次 B+ 树查找与随机 IO。二级叶天然带主键,所以 SELECT id,name + idx_name 即覆盖;SELECT * 几乎不可能覆盖。代价是冗余与写放大。
联合索引按 (a,b,c) 字典序成树,只有前缀固定时后续列才有序;范围列后的列在页内无序 → 截断。设计口诀:等值列在前、范围列收尾,如 INDEX(status, created_at)。
ICP 把"仅用索引列可判定"的 WHERE 下推到 InnoDB 在索引条目上先过滤,通过才回表;仅二级索引生效(§10.2.1.6)。覆盖索引消除回表,ICP 减少回表;方向相同、层次不同。
Interview QA · 2/2
同样建议先自答。这一页每题都要能给出"结论 + 一句边界"——只答结论会被追问(如第 10 题的方向性、第 14 题的分层判断)。
前导 % 无法用于定位,但若查询列全在索引内,可用全索引扫描(type=index)+ ICP 过滤——比全表扫仍省。LIKE 'abc%' 则正常走 range。
字符串列 vs 数字:每行都把列转数字再比,等价于对列套函数 → 索引失效(手册 §12.3 原话)。反向 int_col='1' 是常量转数字,索引照常可用——方向不同结果不同。
不是。a<>1 可转 (<1) ∪ (>1) 两个 range;选择性好的时候照样走。选择性差时优化器按代价放弃——归入"代价性放弃"而非"结构性失效"。
先 type 定位访问方式,再 key+key_len 验证"用没用、用到第几列",rows 估扫描量,Extra 找回表/排序/临时表信号。核心查询 ≥ range、JOIN 被驱动表 ≥ ref 是常用达标线;index 不等于好。
INDEX(a INT NULL, b INT NOT NULL):a 占 4+1=5B,b 占 4B。key_len=5 → 只用了 a;key_len=9 → a、b 都用上。答题时把"列宽×字符集×NULL 标志"现场算一遍,比背结论有说服力。
filesort = 需额外排序,不代表落盘:小结果集内存排完即走,大结果集才外排归并。LIMIT 小值 + 索引可部分缓解;若出现在大分页深翻页场景(ORDER BY … LIMIT 100000,10)应改游标分页。
每索引一棵树:UPDATE 触发聚簇 + N 个二级索引变更(delete-mark + 插入),全部记 redo/binlog 并在从库重放;还拖慢批量导入与空间利用率。下线用 INVISIBLE 观察,确认无用再 DROP。
8.0 加/删二级索引默认 ALGORITHM=INPLACE:不重建表、允许并发 DML(细节见 lock-internals 的 Online DDL 表);MDL 短暂持有,长事务会卡 DDL——这也是"先查长事务再变更"的原因。
Related & References
mvcc.html · 二级索引无隐藏列 / delete-mark / 覆盖索引失效的 MVCC 根源
lock-internals.html · 锁加在索引记录上:无索引更新锁全表 / Online DDL
LRU 缓存(手撕实现) · 哈希 + 双链表结构与 O(1) get/put
log-redo-undo-binlog.html · 索引变更的 redo 落盘链
transaction-basics.html · ACID 与事务语义全景
balanced-tree(数据结构系列) · B+ 树结构原理与"为什么不是红黑/哈希"
16KB 页 / 扇出 1170 → 3 层 ≈ 2000 万行
聚簇存整行 · 二级存列+PK → 回表 → 覆盖索引 / ICP
顺序键 → 15/16 填充、末端分裂 → 自增主键
树序被破坏 = 失效 → EXPLAIN type / key_len / Extra 验证
References
| dev.mysql.com/doc/refman/8.0/en/innodb-physical-structure.html | §17.6.2.2:B-tree 结构、索引页默认 16KB、留 1/16 空闲、顺序 15/16 满 vs 随机 1/2~15/16、MERGE_THRESHOLD |
| dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html | §17.6.2.1:聚簇索引定义、二级记录含主键列、短主键原则、无逻辑主键时用自增 |
| dev.mysql.com/doc/refman/8.0/en/mysql-indexes.html | §10.3:How MySQL Uses Indexes / Multiple-Column Indexes(10.3.6) / B-Tree vs Hash(10.3.9) / Invisible(10.3.12) / Descending(10.3.13) |
| dev.mysql.com/doc/refman/8.0/en/index-condition-pushdown-optimization.html | §10.2.1.6:ICP 定义、InnoDB 仅二级索引、适用访问方式、Using index condition |
| dev.mysql.com/doc/refman/8.0/en/explain-output.html | §10.8.2:type 值 best→worst 清单、key_len 语义、Using index / index condition / filesort / temporary 定义 |
| dev.mysql.com/doc/refman/8.0/en/type-conversion.html | §12.3:字符串列与数字比较无法使用索引(隐式转换失效的官方出处) |
| dev.mysql.com/doc/refman/8.0/en/create-index.html | §13.1.15:Functional Key Parts(8.0.13+) |
| dev.mysql.com/doc/internals/en/innodb-page-structure.html | MySQL Internals Manual:FIL Header 的 prev/next 页指针(叶子双向链表的物理依据) |
https:// 后面直达官方页面。版本敏感结论务必回到手册核对——本 deck 全部按 MySQL 8.0 撰写,5.7 在降序索引、隐藏索引、函数索引、Skip Scan 上与 8.0 行为不同。