Skip to content

MySQL SQL 执行全过程原理

很多人学 MySQL 时只知道:

text
SQL 先解析,再优化,再执行。

这句话太粗。真正要能学懂、面试能讲清楚、线上能排查,需要把一条 SQL 从客户端发出到 InnoDB 读页返回结果的每一步都拆开。

这一页解决这些问题:

  1. 客户端连接到 MySQL 后发生了什么。
  2. Server 层解析、预处理、优化、执行分别做什么。
  3. 优化器为什么可能不用你建的索引。
  4. 执行器怎么调用 InnoDB。
  5. InnoDB 怎么从 Buffer Pool 或磁盘读页。
  6. 为什么 limit 20 不代表只扫描 20 行。
  7. 一条慢 SQL 到底可能慢在哪个环节。

总流程

mermaid
flowchart TD
    A["客户端发送 SQL"] --> B["连接层"]
    B --> C["权限和连接状态检查"]
    C --> D["解析器"]
    D --> E["预处理器"]
    E --> F["优化器"]
    F --> G["执行器"]
    G --> H["存储引擎接口"]
    H --> I["InnoDB 索引和数据页"]
    I --> J["返回行给执行器"]
    J --> K["返回结果给客户端"]

一句话理解:

Server 层决定 SQL 怎么执行,InnoDB 负责真正按页和索引读取数据。

第一步:连接层做什么

客户端连接 MySQL 通常基于 TCP。

mermaid
flowchart TD
    A["客户端发起连接"] --> B["MySQL 接收连接"]
    B --> C["认证用户名和密码"]
    C --> D["检查账号权限"]
    D --> E["分配连接线程或线程资源"]
    E --> F["连接进入可执行 SQL 状态"]

连接层负责:

事情说明
建立连接客户端和 MySQL Server 建立通信
认证校验用户名、密码、来源主机
权限判断账号能访问哪些库、表、列
连接状态保存当前数据库、事务状态、隔离级别等

如果连接层出问题,可能表现为:

  1. 连接数满。
  2. 应用连接池耗尽。
  3. 权限不足。
  4. 认证失败。
  5. 网络超时。

这类问题不是 SQL 索引能解决的。

第二步:解析器做什么

解析器把 SQL 文本变成 MySQL 能理解的语法结构。

例如:

sql
select id, username
from users
where status = 1
order by created_at desc
limit 20;

解析器会识别:

  1. 这是 select
  2. 查询字段是 idusername
  3. 来源表是 users
  4. 过滤条件是 status = 1
  5. 排序是 created_at desc
  6. 限制返回 20 行。

如果语法错,会在这一层报错:

sql
select from users;

解析器只判断语法,不判断这条 SQL 是否高效。

第三步:预处理器做什么

预处理器会检查语义:

检查示例
表是否存在users 表有没有
列是否存在username 字段有没有
字段是否歧义多表 Join 中同名字段是否明确
权限是否足够当前用户能不能查这些字段

例如:

sql
select not_exist_column
from users;

语法可能没问题,但预处理会发现字段不存在。

第四步:优化器做什么

优化器不是简单地“有索引就用索引”。它会生成多个候选计划,并估算成本。

mermaid
flowchart TD
    A["SQL 语义明确"] --> B["找可用索引"]
    B --> C["估算每种访问路径成本"]
    C --> D["估算扫描行数和回表成本"]
    D --> E["考虑排序和临时表成本"]
    E --> F["选择成本最低的执行计划"]

优化器会考虑:

  1. 全表扫描。
  2. 走哪个索引。
  3. 是否回表。
  4. Join 顺序。
  5. 是否需要 filesort。
  6. 是否使用临时表。
  7. 扫描行数预计是多少。

为什么有索引也不用

假设表:

sql
create table orders (
  id bigint primary key auto_increment,
  user_id bigint not null,
  status tinyint not null,
  amount decimal(10, 2) not null,
  created_at datetime not null,
  key idx_status (status)
) engine = InnoDB;

查询:

sql
select *
from orders
where status = 1;

如果 status = 1 占全表 90%,优化器可能不用 idx_status

为什么?

mermaid
flowchart TD
    A["走 status 索引"] --> B["扫描大量二级索引记录"]
    B --> C["每条记录回表取整行"]
    C --> D["大量随机访问聚簇索引"]
    E["全表扫描"] --> F["顺序扫描聚簇索引页"]
    D --> G{"哪个成本低"}
    F --> G

如果返回大部分数据,走二级索引再大量回表,可能比全表扫描更慢。

所以“有索引不用”不是 MySQL 傻,而是它估算走索引不划算。

第五步:执行器做什么

优化器选好计划后,执行器按计划执行。

执行器不直接操作磁盘,它通过存储引擎接口调用 InnoDB。

mermaid
flowchart TD
    A["执行器拿到执行计划"] --> B["调用 InnoDB 接口"]
    B --> C["读取第一条符合条件的记录"]
    C --> D["Server 层判断剩余条件"]
    D --> E{"是否满足"}
    E -- "是" --> F["放入结果集"]
    E -- "否" --> G["丢弃"]
    F --> H{"是否满足 limit"}
    G --> I["读取下一条"]
    H -- "否" --> I
    H -- "是" --> J["返回客户端"]

注意:有些条件可以下推给 InnoDB,有些需要 Server 层继续判断。

第六步:InnoDB 怎么读数据

InnoDB 以页为单位读写,默认页大小通常是 16KB。

主键查询:

sql
select *
from users
where id = 1001;

过程:

mermaid
flowchart TD
    A["执行器请求 id=1001"] --> B["InnoDB 从聚簇索引根页开始"]
    B --> C["定位内部页"]
    C --> D["定位叶子页"]
    D --> E{"叶子页在 Buffer Pool 吗"}
    E -- "是" --> F["直接读内存页"]
    E -- "否" --> G["从磁盘读页到 Buffer Pool"]
    G --> F
    F --> H["从页中找到完整行"]

关键点:

  1. InnoDB 不是一行一行从磁盘读。
  2. 它先读页。
  3. 页在 Buffer Pool 中就读内存。
  4. 页不在 Buffer Pool 中才读磁盘。

二级索引查询为什么可能回表

查询:

sql
select id, username, age
from users
where username = 'tom';

如果有索引:

sql
create index idx_username on users(username);

二级索引叶子节点保存的是:

text
username + 主键 id

如果查询字段 age 不在二级索引里,就要回表。

mermaid
flowchart TD
    A["查 idx_username"] --> B["找到 username=tom"]
    B --> C["拿到主键 id"]
    C --> D["回到聚簇索引查 id"]
    D --> E["读取完整行"]
    E --> F["返回 id / username / age"]

回表少量没问题,大量回表就可能慢。

limit 20 为什么不代表只扫描 20 行

查询:

sql
select id, order_no
from orders
where status = 1
order by created_at desc
limit 20;

如果没有合适索引,MySQL 可能要:

  1. 找出大量 status = 1 的行。
  2. created_at 排序。
  3. 取前 20 行。
mermaid
flowchart TD
    A["where status=1"] --> B["扫描大量候选行"]
    B --> C["按 created_at 排序"]
    C --> D["取前 20 行"]

这就是为什么 limit 写得小,也可能很慢。

如果有联合索引:

sql
create index idx_status_created
on orders(status, created_at);

MySQL 可以更接近按索引顺序取数据。

filesort 是什么

Using filesort 不一定表示一定落磁盘,它表示 MySQL 需要额外排序,而不是直接利用索引顺序。

排序可能发生在内存,也可能因为数据太大使用磁盘临时文件。

mermaid
flowchart TD
    A["候选行"] --> B{"索引顺序能满足 order by 吗"}
    B -- "能" --> C["按索引顺序返回"]
    B -- "不能" --> D["额外排序 filesort"]
    D --> E{"内存够吗"}
    E -- "够" --> F["内存排序"]
    E -- "不够" --> G["磁盘临时文件辅助排序"]

一条查询 SQL 的完整例子

表:

sql
create table orders (
  id bigint primary key auto_increment,
  order_no varchar(64) not null,
  user_id bigint not null,
  status tinyint not null,
  amount decimal(10, 2) not null,
  created_at datetime not null,
  unique key uk_order_no(order_no),
  key idx_user_created(user_id, created_at)
) engine = InnoDB default charset = utf8mb4;

SQL:

sql
select id, order_no, amount, created_at
from orders
where user_id = 1001
order by created_at desc
limit 20;

完整过程:

mermaid
flowchart TD
    A["客户端发送 SQL"] --> B["连接层检查连接和权限"]
    B --> C["解析 SQL 结构"]
    C --> D["预处理检查 orders 和字段"]
    D --> E["优化器选择 idx_user_created"]
    E --> F["执行器调用 InnoDB"]
    F --> G["InnoDB 定位 user_id=1001 的索引范围"]
    G --> H["按 created_at 顺序扫描叶子节点"]
    H --> I["需要 amount,可能回表读取完整行"]
    I --> J["收集 20 行"]
    J --> K["返回客户端"]

为什么索引 (user_id, created_at) 合适:

  1. user_id 是等值条件,可以先定位用户范围。
  2. created_at 用于排序,可以利用索引顺序。
  3. limit 20 可以尽快停止扫描。

慢 SQL 可能慢在哪

环节问题现象
连接层连接池满、连接数满应用拿不到连接
解析预处理SQL 复杂、权限错误报错或解析耗时
优化器统计信息不准、选错索引执行计划不合理
执行器filesort、temporaryExtra 出现对应信息
InnoDB扫描页多、回表多rows 高、IO 高
等待其他事务单独执行快,并发慢
网络和应用返回数据太多DB 执行不慢但接口慢

排查顺序

mermaid
flowchart TD
    A["发现 SQL 慢"] --> B["拿到原 SQL 和参数"]
    B --> C["EXPLAIN 看计划"]
    C --> D["看 type / key / rows / Extra"]
    D --> E{"扫描是否过多"}
    E -- "是" --> F["检查索引和 SQL 写法"]
    E -- "否" --> G{"是否排序或临时表"}
    G -- "是" --> H["优化 order by / group by / 索引"]
    G -- "否" --> I{"是否锁等待"}
    I -- "是" --> J["查事务和锁"]
    I -- "否" --> K["查网络 / 应用 / 返回行数"]

商业场景:订单列表为什么忽然变慢

背景:

sql
select id, order_no, amount, created_at
from orders
where user_id = 1001
order by created_at desc
limit 20;

如果没有 (user_id, created_at),只有 user_id 索引,可能发生:

  1. user_id 找到该用户所有订单。
  2. 回表读取大量订单。
  3. 再按 created_at 排序。
  4. 取前 20。

用户订单少时不慢,用户订单多后突然变慢。

优化:

sql
create index idx_user_created
on orders(user_id, created_at);

优化后:

  1. 直接定位用户订单范围。
  2. 按时间顺序扫描。
  3. 扫够 20 条就停止。

面试标准回答

text
一条 MySQL 查询会先经过连接层做连接、认证和权限检查,然后 Server 层解析 SQL、预处理检查表和字段,再由优化器根据统计信息和成本选择执行计划,执行器按计划调用存储引擎接口。InnoDB 会根据索引从 B+Tree 根页定位到叶子页,优先从 Buffer Pool 读页,页不在内存才读磁盘。二级索引叶子保存索引列和主键值,如果查询字段不在索引中,需要回到聚簇索引查完整行,也就是回表。limit 不代表只扫描这么多行,如果没有合适索引,MySQL 可能先扫描、过滤、排序大量数据,再取少量返回。排查慢 SQL 要看 EXPLAIN 的 type、key、rows、Extra,并结合锁等待、返回行数和应用侧耗时分析。