资讯动态

SQL执行全链路解析:连接、解析、优化、执行与存储

发布时间:2026/9/17 3:16:08 来源:尧图企业网站定制
先抛一个很多后端同学都问过的问题你往数据库里敲下一条 SQL比如SELECT name FROM users WHERE age 20按下回车屏幕上很快返回几十行数据。看起来很简单但这条 SQL 究竟经历了什么是谁在解析它谁决定走哪个索引最终又是谁把磁盘上的数据读出来、算完、返回给你如果你只是写业务代码可能觉得“能跑就行”。可一旦线上出慢查询、接口超时、数据库 CPU 飙高你必然要面对“为什么这条 SQL 慢”这种灵魂拷问。而答案不在业务代码里全在这条 SQL 的执行链路中。这篇文章会把一条 SQL 从发出到返回结果的完整旅程拆开揉碎讲清楚。我尽量用操作过的真实经验说话不堆概念适合刚入行的后端开发、运维也适合想系统梳理一遍的 DBA。你可以把它当成一份“数据库内部原理导读”看完再去调慢 SQL、看执行计划会顺畅很多。1. 先看全景一次SQL请求的完整路线1.1 一条SQL的一生先记住一句话SQL 是一段描述“你想要什么结果”的声明式语言它不告诉你“怎么去取”。决定“怎么去取”的是数据库内部的优化器和执行引擎。我在排查问题时习惯把整条链路画成下面这五个节点任何一个节点出问题表现都是“SQL 慢”或“SQL 报错”但原因可能天差地别客户端连接请求发送 SQL 文本连接器负责建立连接、认证、维持会话解析器做词法分析、语法分析、语义检查优化器根据统计信息计算成本生成执行计划执行器按照执行计划调用存储引擎接口读写数据返回结果以 MySQL 为例一条完整 SQL 的执行顺序是客户端 → 连接器 → 解析器 → 预处理器 → 优化器 → 执行器 → 存储引擎。市面上主流数据库比如 SQL Server、Oracle、PostgreSQL核心思想都一样只是组件命名和细节有差异。SQL Server 里叫 Relational Engine关系引擎和 Storage Engine存储引擎职责边界非常清晰。1.2 一个点餐的类比我把这个过程比作去餐厅点餐。你坐在餐桌前服务员过来记录你要什么菜这是连接器后厨根据菜单把菜名拆成具体食材这是解析器厨师长根据现有食材库存和灶台情况决定先炒哪个菜、用哪个锅这是优化器最后帮厨真的去洗菜、切菜、下锅这是执行器和存储引擎。这个类比能解释一个关键问题为什么同一条 SQL有时候快有时候慢因为厨师长优化器的决定会受食材库存统计信息、灶台繁忙程度系统负载影响。数据库也一样同样的 SQL数据量变了、索引变了、统计信息过期了执行计划可能完全不一样。1.3 理解这条链路到底有什么用说实话刚工作那会儿我也觉得“懂原理不如会写 SQL”。后来线上出过一次事故一条报表 SQL 在数据量从百万涨到千万之后执行计划从走索引变成了全表扫描直接拖垮库。那时候才意识到不了解执行链路你连“优化器为什么选错”这个问题的方向都找不到。理解这条链路的价值有三块排查慢 SQL 时能快速定位问题出在解析阶段、优化阶段还是执行阶段设计索引时知道索引是怎么被优化器选中的就不容易拍脑袋建一堆无用索引阅读执行计划时能看懂每个字段背后的含义而不是只会看有没有用索引2. 第一步连接建立与请求发送2.1 谁来接住你的连接请求不管是 MySQL、SQL Server 还是 PostgreSQL第一步都要先建立客户端和服务端的连接。这个环节负责的是连接器。以 MySQL 为例连接器做的事情有三件认证身份、校验权限、维护会话状态。show processlist里你能看到一堆连接处于Sleep状态的就是已经建立但当前没有查询的连接。SQL Server 里对应的概念是会话SessionDBA 常用sys.dm_exec_sessions和sys.dm_exec_connections来查连接情况。这里有个实操经验很多生产环境报“Too many connections”并不是真的并发太高而是连接池配置没做好或者有大量连接处于 Sleep 状态长期不释放。MySQL 默认最大连接数是 151具体版本有差异SQL Server 默认最大连接数是 32767但实际能撑住的并发连接数取决于内存和线程资源。还有一个小坑连接建立时要走 TCP 三次握手本地回环可能用 Unix Socket远程访问走 TCP。如果是跨机房跨网络访问数据库连接建立的那一下如果慢整体链路都会变慢。排查时先 ping 一下延迟别一上来就盯着 SQL 本身看。2.2 通信协议与数据包限制连接建立后SQL 文本要通过网络协议传给服务端。这里容易被忽略的是“包大小限制”。MySQL 有一个参数叫max_allowed_packet默认值在不同版本不一样常见的是 4MB 或 64MB。如果一条 SQL 文本特别长或者结果集特别大超过了这个限制会直接报 “Packet too large” 错误。我做过的实际案例业务方把几千条 insert 拼成一条 SQL结果在测试环境跑得好好的生产环境报错。查了半天发现是生产环境的max_allowed_packet设置比测试环境小。这类问题不涉及 SQL 本身纯粹是通信层限制。SQL Server 这边对应的概念是 TDS 协议默认端口 1433。SQL Server 也有网络包大小设置叫network packet size默认 4096 字节。一般情况下不需要特意调大但如果你传输大字段比如存储了很大的 JSON 或二进制数据可以考虑适当调整。2.3 认证与权限检查的时机连接建立后服务端会验证用户名和密码同时把账号对应的权限读取到会话上下文中。但这个权限是“会话建立时快照”的状态。有一个非常经典的坑你用账号 A 连接数据库此时账号 A 还没有某张表的 SELECT 权限然后管理员给账号 A 授予了权限但如果你的连接一直没断开旧连接里可能仍拿不到新权限。这在 MySQL 和部分数据库里都会遇到。遇到“明明授权了还是没权限”的诡异问题先重连一下。权限校验通常不是在执行链路的第一步一次性查完。MySQL 里打开表的时候会再次做表级权限校验执行器的每个操作节点也会做一些行级、列级权限判断。也就是说语法解析阶段先检查一次真正读数据阶段还会再检查一次。3. 第二步解析阶段的拆解3.1 词法分析SQL文本是怎么被切碎的连接器把 SQL 文本交给解析器之后第一步是词法分析。它做的事情非常机械把字符串按空格、标点、关键字切分成一个个 token。以这条 SQL 为例SELECT u.name, u.age FROM users u WHERE u.age 20;词法分析会把整条字符串切成类似这样的 token 序列SELECT | u | . | name | , | u | . | age | FROM | users | u | WHERE | u | . | age | | 20 | ;其中SELECT、FROM、WHERE是关键字u、name、age是标识符20是数字常量是操作符。这里的难点在于词法分析要识别“什么叫一个字”比如字符串里的空格要不要去掉、注释要不要跳过、字符串字面量里有空格怎么办。你可能觉得这些是数据库内核开发才需要关心的事。但理解这一点对写 SQL 有帮助SQL 写的规范不规范会影响解析效率吗说实话解析器的处理速度非常快几千字符的 SQL 解析耗时都在微秒到毫秒级绝大部分慢 SQL 的瓶颈根本不在解析。但有些写法会误导解析器比如字符串拼接 SQL 时把参数值和 SQL 结构混在一起这属于另一类问题SQL 注入风险后面再说。3.2 语法分析生成语法树词法分析得到 token 流之后语法分析器会按照数据库定义的语法规则把这些 token 组装成一棵抽象语法树AST。比如SELECT u.name FROM users u WHERE u.age 20会变成类似这样的一棵树SelectStatement ├── selectList │ └── ColumnRef(u.name) ├── fromClause │ └── TableRef(users AS u) └── whereClause └── BinaryExpr() ├── ColumnRef(u.age) └── Literal(20)语法分析阶段只关心“你写的这句话符不符合 SQL 语法”不关心表存不存在、列存不存在。比如SELECT FROM WHERE这种明显缺东西的写法在这里就会报语法错误。很多数据库的报错信息比如MySQLYou have an error in your SQL syntaxSQL ServerIncorrect syntax near ...都是语法分析阶段的报错。看到这类错误先看近括号、引号、逗号是不是配对了。我排查过无数这种问题绝大多数是括号不匹配或者少写了一个逗号。3.3 语义检查表和列真的存在吗语法树生成后接下来是语义检查。这个阶段会把语法树和系统表中的元数据表结构、列类型、约束条件比对。比如执行SELECT nmae FROM usersnmae这个列在users表里不存在就会报“Unknown column nmae in field list”。如果表不存在会报“Table xxx doesnt exist”。这里有个细节有些数据库把权限校验也放在这个阶段有些放在执行阶段。MySQL 里表存在期间会做权限检查但更细的列权限、行权限可能在执行阶段才校验。不同版本的行为也不完全一样。遇到权限相关报错别只看报错信息还要结合你使用的账号和连接方式一起排查。3.4 常见解析错误的排查思路我从实际运维中总结了一个小小的排查优先级遇到解析报错可以按这个顺序看先看报错位置附近 20 个字符语法错误的报错位置通常很准确检查括号、引号、逗号是否配对检查表名、列名是否真的存在注意大小写和别名检查关键字是否被当成了列名或表名比如某列名叫orderorder是 SQL Server 里的关键字必须加中括号[order]SQL Server 里对关键字的处理尤其严格因为很多系统函数、关键字如果被用作列名必须用方括号转义。MySQL 里用的是反引号。这也是为什么我一直建议建表时尽量避免使用关键字作为字段名这是从源头规避问题的好习惯。4. 第三步优化器怎么选执行计划4.1 为什么说执行计划是数据库的“灵魂”解析器确认 SQL 语义没问题之后真正的重头戏来了优化器要决定怎么执行这条 SQL。同一个结果可以有无数种执行方式。比如查users表年龄大于 20 的用户既可以全表扫描一行一行过滤也可以走 age 字段上的索引定位到起点后顺序扫描。如果还要 join 另一张表驱动表选择、连接方式选择都会影响性能。优化器的任务就是在这些可能性里挑一个“成本最低”的。这里我说一下 MySQL 和 SQL Server 的区别。MySQL 用的是基于成本的优化器CBO核心思想是估算每种执行计划的代价选代价最小的。SQL Server 同样也是 CBO而且它的优化器会根据代价分三个阶段简单计划、完整优化、并行计划复杂度上比 MySQL 更精细一些。4.2 逻辑优化改写不等于瞎改优化器做的第一类工作是逻辑优化也叫等价改写。它不会改变 SQL 的语义但会把 SQL 变成更优的形式。常见的逻辑优化手段子查询扁平化把WHERE id IN (SELECT user_id FROM orders)改写成半连接semi join减少执行次数谓词下推把WHERE条件下推到 join 之前的表扫描阶段提前过滤行数常量传递WHERE a 1 AND b a会被改写成WHERE a 1 AND b 1冗余条件消除去掉恒真或恒假的过滤条件这些改写对用户是透明的。但有个重要结论不要以为优化器能帮你把所有烂 SQL 都改好。有些写法优化器确实做不了等价改写比如过分复杂的嵌套子查询、带有用户自定义函数的条件、无法推导传递性的关联条件。这就是为什么“看似相同的 SQL 性能差异巨大”的根源之一。4.3 物理优化走哪个索引、怎么连接表逻辑优化后是物理优化。这一步要确定三件事单表访问路径全表扫描还是某个索引扫描多表连接顺序先连哪张表再连哪张表具体连接算法嵌套循环连接、哈希连接还是排序合并连接先说单表访问。假设users表有两百万行age列和city列都有索引。你要查年龄大于 30 且城市为北京的用户优化器需要估算“走 age 索引过滤后再回表过滤 city”和“走 city 索引过滤后再回表过滤 age”哪个成本低。这个估算基于统计信息具体来说就是每个条件下有多少行符合。再说连接算法。MySQL 8.0.18 之前只支持嵌套循环连接Nested Loop Join及其变体8.0.18 之后引入哈希连接Hash Join。SQL Server 支持嵌套循环、哈希连接、合并连接三种优化器根据数据量级和是否有可用索引选择。你可以简单理解小表驱动大表时嵌套循环很高效大表和大表等值连接时哈希连接通常更快两边都有序时合并连接有优势。优化器并不是傻傻地套公式它基于统计信息选最划算的策略。4.4 统计信息与直方图优化器为什么会“看走眼”这里重点讲优化器失败的根本原因成本估算依赖的是统计信息不是真实数据。MySQL 的 InnoDB 引擎通过采样随机索引页来估算行数所以统计信息本身就有误差。如果表的增删改非常频繁统计信息还会过期。走索引的条件原本应该过滤掉大部分行但优化器拿到的统计信息显示“这列大概有 50% 的重复值”可能就直接选全表扫描了。SQL Server 也有类似问题。你需要定期更新统计信息否则优化器可能选出一个灾难性的执行计划。MySQL 8.0 之前用ANALYZE TABLE更新统计信息MySQL 8.0 开始支持直方图可以更精确地描述列值分布SQL Server 则是UPDATE STATISTICS。DBA 定期维护统计信息是比调索引优先级还高的一项工作。我踩过一次真实的坑一张订单表每天新增几十万行某天业务突然反馈查询变慢。查执行计划发现优化器选了全表扫描但explain里的预估行数明显偏少。跑了ANALYZE TABLE之后执行计划立刻切回索引扫描查询从 8 秒降到 50 毫秒。这个案例让我养成了一个习惯遇到数据量级巨变导致的慢查询先更新统计信息再调 SQL。4.5 实操用EXPLAIN看透执行计划不管是 MySQL 还是 SQL Server都有查看执行计划的手段。MySQL 最常用的是EXPLAINEXPLAIN SELECT u.name, u.age, o.order_no FROM users u JOIN orders o ON u.id o.user_id WHERE u.age 20 AND o.amount 100;看执行计划时我最关注的字段是这几个字段关键含义排查优先级type访问类型从好到坏依次是 system const eq_ref ref range index ALL出现 ALL 说明全表扫描重点排查key实际用到的索引NULL 表示没用索引rows预估扫描行数与实际行数偏差过大说明统计信息有问题Extra附加信息比如 Using filesort、Using temporary看到这两个关键字基本可以断定 SQL 需要优化如果type是ALL说明这条路基本不可取。如果Extra里有Using filesort说明排序没有走索引数据量大时就是个性能炸弹。SQL Server 这边最常用的是“显示估计的执行计划”或者用 DMV 查缓存计划SELECT * FROM sys.dm_exec_query_stats;很多人只喜欢看“有没有用索引”这一个点这是不够的。我用执行计划的标准动作是先看rows预估值是否离谱再看Extra里的额外操作最后看type和key。顺序错了容易漏掉真正的问题。5. 第四步执行器与存储引擎的配合5.1 执行器的迭代模型一条条取数据优化器生成执行计划后执行器开始干活。执行计划是树形结构最底层是表扫描或索引扫描节点往上依次是过滤、计算、连接、排序、聚合等节点。现代数据库几乎都采用迭代模型也叫火山模型每个算子对外提供next()接口上层算子每调一次next()下层算子就返回一行数据。这个设计让执行器非常灵活查询、更新、删除都可以用同一套执行框架。MySQL 的执行器会通过 handler 接口调用存储引擎。执行流程大概是执行器调用 InnoDB 的接口取第一行判断 WHERE 条件是否满足满足就放入结果集然后继续取下一行直到取完。这里有个重要机制索引条件下推Index Condition PushdownICP。如果 WHERE 条件里的字段是索引的一部分存储引擎层就可以先过滤减少回表次数。MySQL 5.6 开始默认开启 ICP这也是为什么有时候我在执行计划里看到Using index condition反而说明它已经做了优化。5.2 InnoDB的B树与行存储执行器要读数据必须通过存储引擎。InnoDB 是 MySQL 默认的存储引擎底层数据结构是 B 树。为什么选 B 树而不是哈希表或者二叉树B 树有两个关键特性非叶子节点只存索引键不存数据所以一页能容纳更多索引项树的高度可控。三层 B 树大概能存千万到上亿行数据叶子节点用双向链表串起来非常适合范围查询。age 20这种范围条件只需要定位到起点然后顺序往后扫SQL Server 的聚集索引和非聚集索引底层也是 B 树原理相同。Oracle 的索引结构略有不同但也基于 B 树思想。这里有个概念容易混淆InnoDB 的聚簇索引主键索引的叶子节点存的是整行数据二级索引的叶子节点存的是主键值。也就是说用二级索引查询时如果需要返回不在索引里的列就必须拿主键值回到聚簇索引里再查一次这个动作叫“回表”。5.3 回表与覆盖索引这个细节最影响性能回表最直接的影响是产生大量随机 IO。聚簇索引的叶子节点按主键顺序排列在磁盘页里二级索引查到的主键值是随机的回表时大概率去不同的数据页读每一页都要经历一次磁盘 IO。这就是很多慢查询慢的原因。举个具体例子。一张用户表有id主键、age、name三个字段你建了一个索引idx_age(age)然后执行SELECT name FROM users WHERE age 20;执行过程走idx_age索引找到所有符合条件的二级索引记录拿到对应的主键 id然后回表到聚簇索引查name字段最后返回。如果符合条件的有 10 万行就要回表 10 万次。反过来如果你执行SELECT age FROM users WHERE age 20;age就在idx_age索引里不需要回表直接返回即可这叫覆盖索引。如果你有高频查询合理设计覆盖索引能省掉大量回表 IO。但注意覆盖索引不是建得越多越好因为索引本身也要占空间、影响写入性能要把频繁查询且命中率高的场景优先覆盖。5.4 事务与日志机制查询背后还有一笔“账”执行过程中不只是读数据那么简单。如果 SQL 是更新操作InnoDB 会涉及三种日志redo log物理日志记录页的修改。采用 WALWrite-Ahead Logging机制先写日志再写数据。崩溃重启后靠它恢复数据undo log逻辑日志记录数据的旧版本。事务回滚和 MVCC多版本并发控制都靠它binlog服务层日志记录逻辑操作。主从复制依赖它。MySQL 8.0 里redo log 和 binlog 采用两阶段提交保证一致性这也是为什么一条 update 语句的执行链路上除了执行器、存储引擎还会有日志系统参与。如果你遇到“更新很慢”不要只盯着索引也可能是因为日志刷盘策略太保守。比如 MySQL 里innodb_flush_log_at_trx_commit设置为 1 时每次事务提交都要把 redo log 刷到磁盘安全性最高但最慢设置为 2 时每秒刷一次性能提升但崩溃时可能丢最多 1 秒的数据。5.5 SQL Server的执行架构差异前面主要以 MySQL 为主讲但热搜里 SQL Server 相关的内容也很多这里稍微对比一下。SQL Server 的执行架构分为 Relational Engine 和 Storage Engine 两层。Relational Engine 负责解析、优化、执行Storage Engine 负责数据存取、锁和日志。从逻辑架构上SQL Server 和 MySQL 是高度相似的。不同点在于SQL Server 对执行计划有非常细的缓存管理叫 Plan Cache。每次执行相同 SQL 时如果参数一样或者参数化后一样可以复用计划跳过硬解析和硬优化。MySQL 8.0 也有查询缓存但注意MySQL 8.0 已经移除了查询缓存这个功能别混为一谈。如果你在用 SQL Server可以查sys.dm_exec_query_stats找到 CPU 消耗最高的前几条 SQL也可以查sys.dm_exec_query_plan看具体执行计划。排查慢 SQL 的思路和 MySQL 一脉相承先找执行计划再看扫了多少行、有没有回表、有没有排序。6. 慢SQL优化从原理到实践6.1 从执行链路定位慢的原因我把慢 SQL 按执行链路拆成四类每类的表现和排查方向都不一样慢的原因表现特征排查方向网络或连接问题所有 SQL 都慢或特定客户端慢检查网络延迟、连接池配置、包大小限制解析瓶颈极复杂的动态 SQL几千个字符检查是否硬解析频繁考虑参数化查询优化器选错计划同一条 SQL 时快时慢数据量变化后变慢更新统计信息重写复杂 SQL加提示如索引提示执行瓶颈执行计划没问题但扫描行数大、回表多优化索引设计调整 SQL 结构避免深分页如果你遇到慢查询第一件事不是去优化 SQL而是先看执行计划。只有知道瓶颈在哪一层才能对症下药。6.2 几个最常见的慢SQL场景我在实际工作中反复撞见的慢 SQL 场景基本就这几类第一类隐式类型转换导致索引失效SELECT * FROM orders WHERE user_id 123;如果user_id是数值型但传入的是字符串MySQL 会做隐式类型转换导致索引失效。SQL Server 也有类似行为。排查时留意表结构字段类型和 SQL 传入参数类型是否一致即可。第二类函数包裹列导致索引失效SELECT * FROM orders WHERE YEAR(create_time) 2025;这种写法即使create_time有索引也没办法走索引因为索引树里存的是原始时间值不是计算后的年份值。改成范围查询就能用上索引SELECT * FROM orders WHERE create_time 2025-01-01 AND create_time 2026-01-01;第三类深分页SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;MySQL 这个写法的执行逻辑是扫描前 1000020 行然后丢弃前 1000000 行只返回最后 20 行。扫描量是 100 万行能不慢吗深分页优化常用手段是用上次查询的最大 ID 代替 OFFSETSELECT * FROM orders WHERE id 1000000 ORDER BY id LIMIT 20;第四类Join关联字段类型或字符集不一致两边字段类型不一致或者字符集不一致会导致优化器无法使用索引做关联只能在内存里做额外的转换和过滤。联表查询变慢先看两张表关联字段的类型、字符集、排序规则是否统一。第五类窗口函数排序和临时表SQL 窗口函数写起来很爽比如ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)但它在执行计划里通常需要额外排序甚至产生临时表。数据量一大很容易触发“Using temporary; Using filesort”。排查时不要只看窗口函数的业务逻辑更要看执行计划里的排序和临时表消耗。6.3 一些被验证过的优化习惯慢 SQL 优化其实没有多神秘关键是方法论要正确。我自己沉淀下来的习惯先看执行计划再改 SQL。没有执行计划的优化都是瞎猜先更新统计信息再谈其他。统计信息过期导致的计划错误很常见能用索引覆盖就别回表能避免排序就别用会产生排序的写法合理使用 EXPLAIN ANALYZE 或 SQL Server 的 SET STATISTICS TIME ON拿到真实的执行时间和扫描行数不要试图在一个地方解决所有问题。有些慢查询是 SQL 写法的问题有些是索引问题有些是服务端配置问题逐一排查我还想提醒一点SQL 注入风险其实也和“一条 SQL 如何执行”强相关。如果你用拼接字符串的方式动态生成 SQL用户的输入可能被当成 SQL 结构的一部分从而改变语法树。这也是为什么我一直强调写 SQL 时要用参数化查询而不是字符串拼接。写在最后一次真实案例送给你的提醒前两年我接手过一个查询优化任务。业务方说某个列表页接口一到晚上就超时他们自己排查了很久怀疑是服务器性能不行。我上去先看了执行计划发现一条SELECT语句的key是 NULLtype是 ALLExtra里还带着Using filesort。表才两万多行照理说不该慢。但执行计划里rows预估是 1.9 万行实际扫描了 180 万行。一查才发现这是一张宽表每行有十几个大字段全表扫描的代价被统计信息严重低估了。后续事情很简单把 WHERE 条件里的字段整理成复合索引把需要排序的字段也加进索引再把 SQL 里的SELECT *改成只查需要的字段。执行时间从 6 秒降到了 80 毫秒。这个案例给我最大的启发不是索引有多重要而是只要你愿意沿着“连接 → 解析 → 优化 → 执行 → 存储”这条链路去排查慢 SQL 大概率是能找到明确原因的。相反如果你不了解这条链路看到慢查询第一反应是“加内存、换机器”那你永远都在治标不治本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价