📊 SQL 入门 Day 18:索引原理

深入理解索引的底层数据结构、分类与使用策略,学会用 EXPLAIN 分析查询性能。

一、为什么需要索引

场景: 一个 100 万行的用户表,执行 SELECT * FROM users WHERE email = 'test@example.com'。没有索引时,数据库必须逐行扫描全表(全表扫描),耗时数秒甚至更久。加了索引后,毫秒级返回。

本质: 索引是数据库维护的额外的数据结构 ,提供快速定位数据的能力,以写入性能换读取性能

sql 复制代码
-- 创建索引
CREATE INDEX idx_email ON users(email);

-- 复合索引
CREATE INDEX idx_name_age ON users(name, age);

二、索引数据结构 ------ B+ 树

2.1 B+ 树核心特性

特性 说明
多路平衡树 非二叉,每个节点可存大量键值对
非叶子节点存键 仅用于导航,不存数据,一个节点可包含数百个键
叶子节点存数据/指针 所有实际数据(或指向数据的指针)都在叶子节点
叶子节点链表连接 叶子节点通过双向链表串联,支持范围查询极速遍历
树高通常 3-4 层 百万级数据也只需 3-4 次磁盘 IO

B+ 树为何适合数据库?

  • 矮胖结构 → 减少磁盘 IO(树的高度 ≈ 磁盘寻道次数)
  • 叶子链表 → 范围查询不用回溯(BETWEEN>ORDER BY 效率高)
  • 非叶子只存键 → 一页能装更多键,进一步降低树高

2.2 B+ 树 vs B 树 vs 哈希索引

类型 范围查询 等值查询 排序 适用场景
B+ 树 ✅ 极快 ✅ 快 ✅ 天然有序 大多数场景(MySQL InnoDB 默认)
B 树 ❌ 慢 ✅ 快 已淘汰
哈希索引 ❌ 不可用 ✅ 最快 O(1) 精确等值查找(MEMORY 引擎)

三、聚簇索引 vs 非聚簇索引

3.1 聚簇索引(Clustered Index)

  • InnoDB 存储引擎的默认主键索引
  • 叶子节点直接存储整行数据
  • 一张表只能有一个聚簇索引
  • 主键即聚簇索引(不设主键时,MySQL 会选唯一索引或隐式生成 row_id)
sql 复制代码
-- 主键就是聚簇索引
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 聚簇索引
    name VARCHAR(50),
    email VARCHAR(100)
);

3.2 非聚簇索引(Secondary Index / 二级索引)

  • 叶子节点存储的是主键值,而非整行数据
  • 一张表可有多个非聚簇索引
  • 查询时需回表(先查二级索引拿到主键 ID,再回聚簇索引拿整行数据)
sql 复制代码
-- 二级索引
CREATE INDEX idx_email ON users(email);
-- 查询:SELECT * FROM users WHERE email = '...';
-- 流程:二级索引 → 拿到 id → 聚簇索引 → 拿到整行

3.3 覆盖索引(Covering Index)

  • 要查询的列全部在二级索引中,无需回表
  • 性能接近聚簇索引,是常见的优化手段
sql 复制代码
-- 如果只需查询 id 和 email
-- 二级索引 idx_email 已包含(email, id),无需回表
SELECT id, email FROM users WHERE email = 'test@example.com';

经验原则: 设计索引时尽量包含 SELECT 的字段,减少回表次数。


四、复合索引与最左前缀原则

4.1 最左前缀原则

创建复合索引 (a, b, c) 时,实际生成的是以下组合的索引:

查询条件 是否命中索引
WHERE a = 1 ✅ 命中
WHERE a = 1 AND b = 2 ✅ 命中
WHERE a = 1 AND b = 2 AND c = 3 ✅ 命中
WHERE b = 2 未命中(跳过了 a)
WHERE a = 1 AND c = 3 ⚠️ 部分命中(a 生效,c 不生效)
WHERE b = 2 AND c = 3 ❌ 未命中(跳过了 a)

关键口诀: 带头大哥不能死,中间兄弟不能断。

sql 复制代码
-- 创建复合索引
CREATE INDEX idx_status_created ON orders(status, created_at);

-- ✅ 命中:status 在最左
SELECT * FROM orders WHERE status = 'paid';

-- ✅ 完全命中
SELECT * FROM orders WHERE status = 'paid' AND created_at > '2024-01-01';

-- ❌ 未命中:跳过了 status
SELECT * FROM orders WHERE created_at > '2024-01-01';

4.2 复合索引设计建议

  • 选择性高的列放在最左边(如 status 只有 3 种值 → 选择性低;email 唯一 → 选择性高)
  • 经常作为范围查询的列放右边(因为范围条件后无法使用后续列)
  • 不要建太多单列索引,尽量合并成复合索引

五、索引失效的常见场景

sql 复制代码
-- ❌ 场景1:对索引列使用函数
WHERE YEAR(created_at) = 2024;    -- 索引失效
WHERE created_at >= '2024-01-01'  -- 索引生效(改为范围查询)

-- ❌ 场景2:隐式类型转换
WHERE phone = 13800138000;  -- phone 是 VARCHAR,传入数字 → 索引失效
WHERE phone = '13800138000';  -- 正确

-- ❌ 场景3:LIKE 以通配符开头
WHERE name LIKE '%张三';   -- 索引失效
WHERE name LIKE '张三%';   -- 索引生效(前缀匹配)

-- ❌ 场景4:OR 条件中有一方无索引
WHERE email = 'a@b.com' OR phone = '123456';  -- phone 无索引则全表扫描

-- ❌ 场景5:NOT IN / != / IS NOT NULL
WHERE status != 'deleted';    -- 可能不走索引(取决于数据分布)
WHERE status = 'active';      -- 走索引

-- ❌ 场景6:复合索引违反最左前缀
-- 复合索引 idx(a,b,c),以下查询 不生效:
WHERE b = 1 AND c = 2;  -- 跳过了 a

六、EXPLAIN 查看执行计划

6.1 基础用法

sql 复制代码
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

6.2 关键字段解读

字段 含义
type 访问类型 const > ref > range > index > ALL
possible_keys 可能使用的索引 非空 -
key 实际使用的索引 非空 NULL(未使用索引)
rows 扫描行数估计 越小越好
Extra 额外信息 Using index(覆盖索引) Using filesortUsing temporary

6.3 实际分析示例

sql 复制代码
-- 创建一个测试表
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    status VARCHAR(20),
    amount DECIMAL(10,2),
    created_at DATETIME,
    INDEX idx_status (status),
    INDEX idx_user_status (user_id, status)
);

-- 分析查询
EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 1001 AND status = 'paid'\G

输出解读:

  • type: ref --- 使用非唯一索引查找
  • key: idx_user_status --- 使用了复合索引
  • rows: 1 --- 预估只扫描 1 行
  • Extra: NULL --- 仍需回表查询 amount 字段
sql 复制代码
-- 优化:改为覆盖索引
DROP INDEX idx_user_status ON orders;
CREATE INDEX idx_user_status_amount ON orders(user_id, status, amount);

EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 1001 AND status = 'paid'\G
-- Extra: Using index  ← 覆盖索引,无需回表!

今日小结

  • B+ 树 是 InnoDB 默认索引结构,矮胖 + 叶子链表使其同时擅长等值和范围查询
  • 聚簇索引 存整行数据,一张表只有一个;二级索引 存主键值,需要回表
  • 覆盖索引 是性能利器 --- 索引包含所有查询列,避免回表
  • 最左前缀原则 是复合索引的核心,设计时把高选择性的列放左边
  • 避免函数、隐式转换、LIKE 前缀通配符等导致索引失效的操作
  • EXPLAIN 是 SQL 优化入门必修课,关注 typekeyrowsExtra 四个字段
相关推荐
Csvn2 小时前
🐍 Day3 : Python 容器精讲 — list、dict、set、tuple 底层实现与高级操作
后端·python
DBA小马哥2 小时前
关系型数据库核心概念手册:SQL、事务与存储引擎的技术脉络
数据库·sql
用户938515635072 小时前
ESLint 代码规范完全指南——从 AST 原理到 flat config 逐行解析
javascript·后端·代码规范
用户938515635072 小时前
深入理解 Next.js:从 SPA 的 SEO 问题到约定式路由与 SSR 原理
前端·后端·全栈
灯澜忆梦2 小时前
【MySQL12】进阶篇 | SQL优化
数据库·sql·mysql·性能优化
码事漫谈4 小时前
Deepseek涨价了,前后对比,竟然差这么多
后端
东风破_5 小时前
大前端手里的 Next.js:从 #root 到 SEO,一份 HTML 的两种命运
前端·后端
IT_陈寒5 小时前
Vue的v-for不听话?我被这个Key的坑整懵了
前端·人工智能·后端
众人皆醒我独醉5 小时前
训练加速实战:Flash Attention、Gradient Checkpointing 与数据流水线
后端·面试·gpu