简单分析一下:一条查询SQL在数据库中执行的过程


简单分析一下:一条查询SQL在数据库中执行的过程

查询语句eg:

SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20;

这条 SQL 看似简单,但 MySQL 真正执行它的过程,和你写 SQL 的顺序完全不同,也和很多人想象的"先查全表再排序再截断"完全不同。

下面按 MySQL 真实执行机制 给你拆解,结合游标分页的场景讲清楚为什么它快。


一、先纠正一个认知:SQL 的书写顺序 ≠ 执行顺序

你写的顺序是:

SELECT *           -- ① 书写第1个
FROM sys_user      -- ② 书写第2个
WHERE id > 100000  -- ③ 书写第3个
ORDER BY id        -- ④ 书写第4个
LIMIT 20;          -- ⑤ 书写第5个

MySQL 逻辑上的执行顺序是:

FROM → WHERE → SELECT → ORDER BY → LIMIT

但这是逻辑顺序。

在物理执行层面,如果走了索引,MySQL 会做大量优化,实际过程比这高效得多。


二、假设前提(影响执行计划的关键)

为了讲清楚,先明确两个前提(这也是游标分页能生效的基础):

条件说明
id 是 主键InnoDB 中主键就是聚簇索引,B+ 树叶子节点存的是完整行数据
表引擎是 InnoDB默认引擎,数据按主键聚簇存储

如果 id 不是主键只是普通索引,执行过程会有所不同(需要回表),我后面会提。


三、MySQL 真实物理执行全过程(一步一步来)

阶段 1:客户端 → MySQL 服务层

1. 客户端发送 SQL 到 MySQL Server
2. 连接器:校验用户名密码、权限
3. 查询缓存:MySQL 8.0 已移除,跳过
4. 分析器:词法分析(识别 SELECT、FROM、WHERE 等关键字)→ 语法分析(生成解析树)
5. 优化器:生成执行计划(决定用哪个索引、 JOIN 顺序等)

阶段 2:优化器决策(非常关键)

优化器看到这条 SQL 后,会做如下判断:

WHERE id > 100000 AND ORDER BY id
  • id 是主键,有主键索引(聚簇索引)
  • ORDER BY id 和 WHERE 条件用的是同一个索引字段
  • 主键索引的叶子节点天然按 id 有序

→ 优化器决定:走主键索引,且不需要额外排序(Using index / No filesort)

可以用 EXPLAIN 验证:

EXPLAIN SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20;

典型结果:

type: range
key: PRIMARY
rows: 20        -- 注意这里,优化器估算只扫 20 行!
Extra: Using where; Using index

⚠️ rows: 20 说明优化器知道只需要扫 20 行就够了吗?不完全是,这只是估算。但 LIMIT 20 让优化器知道"不需要扫太多"。


阶段 3:执行器 + InnoDB 存储引擎(核心执行)

这是真正干活的阶段,按执行操作的时间线来讲:

第 1 步:定位起点(B+ 树查找)

执行器调用 InnoDB 接口:找 id > 100000 的第一条记录
    ↓
InnoDB 从主键索引(B+ 树)的根节点开始:
    - 根节点 → 非叶子节点 → ... → 定位到叶子节点中 id = 100001 的位置
    - 时间复杂度:O(log N),通常 3~4 次 IO(千万级数据也只要 3~4 层 B+ 树)

第 2 步:顺序扫描(沿着叶子节点链表)

B+ 树的叶子节点之间是**双向链表**连接的
    ↓
InnoDB 从 id = 100001 这个位置开始,沿着链表**向右顺序读**:
    - 读第 1 条:id = 100001 ✓ → 返回给执行器
    - 读第 2 条:id = 100002 ✓ → 返回给执行器
    - ...
    - 读第 20 条:id = 100020 ✓ → 返回给执行器

第 3 步:LIMIT 触发提前终止(Early Termination)

执行器数了数:已经拿到 20 条了
    ↓
LIMIT 20 条件满足 → **立即停止扫描**
    ↓
不需要再往下读了,直接返回结果给客户端

整个过程只读了 20 行数据,只做了 1 次 B+ 树定位 + 20 次顺序 IO。


四、用一张图理解 B+ 树的执行路径

[根节点]
                   /    |    \
              [非叶子] [非叶子] [非叶子]
                 |       |       |
                 v       v       v
┌─────────────────────────────────────────────────┐
│ 叶子节点链表(按 id 有序存储完整行数据)          │
│  ... | 99999 | 100000 | 100001 | 100002 | ...  │
│                          ↑                      │
│                    定位起点,从这里开始           │
│                    向右顺序读 20 条就停           │
└─────────────────────────────────────────────────┘

五、对比:如果是 LIMIT 100000, 20 会怎样?

-- 深分页写法
SELECT * FROM sys_user ORDER BY id LIMIT 100000, 20;

执行过程变成:

1. 从 B+ 树最左叶子节点开始顺序扫描
2. 读第 1 条 → 丢弃(LIMIT 说要跳过前 100000 条)
3. 读第 2 条 → 丢弃
...
100001. 读第 100001 条 → 保留(终于轮到我要的了)
...
100020. 读第 100020 条 → 保留

结果:扫描并丢弃了 100000 条,实际只返回 20 条。IO 和 CPU 白白浪费在丢弃的数据上。

对比项id > 100000 LIMIT 20LIMIT 100000, 20
扫描行数20 行100020 行
B+ 树定位直接定位到起点从最左端开始
丢弃数据0 行100000 行
时间复杂度O(log N + 20)O(N)
深分页性能√ 恒定快× 越深越慢

六、如果 id 不是主键,是普通索引呢?

如果 id 只是普通索引(非聚簇索引),执行过程会多一步:

1. 走 id 的普通索引 B+ 树 → 找到 20 个 id 值(id > 100000 的前 20 个)
2. **回表**:用这 20 个 id 去主键索引里查完整行数据
3. 返回结果

这就是所谓的 Using index condition / Using where; Using index 和 回表 的区别。

但即使回表,也只回表 20 次,性能依然远好于 LIMIT 100000, 20(那个要回表 100020 次)。


七、EXPLAIN 怎么看这条 SQL 的执行计划

EXPLAIN SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20;

关键字段解读:

字段理想值含义
typerange范围扫描,比 ALL(全表)好
keyPRIMARY实际使用的索引
rows接近 20估算扫描行数
ExtraUsing where; Using index索引覆盖 + 条件过滤,无 filesort

如果看到 Using filesort,说明 ORDER BY 没用上索引,需要优化(比如加联合索引)。


八、一句话总结

SELECT \* FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20 的执行本质是:

  1. 利用主键索引 B+ 树的有序性,直接定位到 id > 100000 的起点
  2. 沿着叶子节点链表顺序读 20 行
  3. LIMIT 20 触发提前终止,不扫多余数据
  4. 全程 O(log N + 20),与页码深浅无关

这就是为什么游标分页方案能彻底解决深分页问题——它不是"优化"了 OFFSET,而是从根本上消灭了 OFFSET。


JAVA-技能点
知识点
Mysql