从元组演算到执行计划:数据库查询优化与慢SQL排查实战

📅 发布时间:2026/10/9 8:41:47
从元组演算到执行计划:数据库查询优化与慢SQL排查实战
1. 为什么考完试就忘干净了元组演算和域演算的真实用处我见过太多数据库系统工程师考生把元组演算、域演算背得滚瓜烂熟考试一过就彻底抛到脑后。这个现象本身不奇怪——考试要的是公式推导和符号变换而日常开发面对的是SQL语句和慢查询日志两者看起来完全不是一个世界的东西。但我想说的是这些理论概念恰恰是理解数据库底层逻辑的钥匙尤其是当你开始研究查询优化器、分析执行计划、排查慢SQL的时候你当年背诵的那些公式会以另一种方式重新出现在你面前。先把这个话题摊开来说。元组演算和域演算诞生于上世纪七十年代是关系模型创始人E.F.Codd在研究如何用数学语言描述数据库查询时提出的。它们和关系代数一样是关系数据库查询语言的理论根基。SQL虽然看起来像是英文句子但它的语义基础其实是元组演算的变体——SELECT ... FROM ... WHERE ...这种结构本质上就是在做元组变量的声明和条件判断。这块内容的核心价值在哪里我总结下来有三点。第一它是理解查询优化器推理过程的理论前提。优化器要把SQL重写成执行计划需要一套严密的等价变换规则这些规则的来源就是关系代数和演算的恒等变换。不懂这套底层逻辑看执行计划永远只能看个皮毛。第二它是判断一条SQL是否可优化、如何优化的思维工具。比如一个子查询能不能改写成JOIN一个EXISTS能不能换成IN这些问题的答案其实都隐藏在关系代数和演算的等价性里。第三它是你作为数据库系统工程师区别于普通CRUD开发者的知识壁垒。面试聊到索引、执行计划、查询重写的时候深度立刻见分晓。这篇文章我就打算从元组演算和域演算的基本概念讲起一路聊到它们和SQL的对应关系再深入到查询优化器的工作机制最后落到真实的执行计划分析场景里。如果你正在备考数据库系统工程师或者工作中经常被慢查询折磨这篇内容应该能帮上忙。2. 元组演算和域演算到底在说什么从集合论到查询公式2.1 先搞清楚研究对象元组、域和关系要理解元组演算和域演算先得把三个基础概念厘清元组、域、关系。关系数据库里的“关系”在数学上就是一组元组的集合。元组可以理解成一张表中的一行它是一组值的有序排列。域则更简单——它是元组中某个分量可能取值的集合。举个例子一张学生表里有学号、姓名、年龄三个列那么每一行就是一个元组(2024001, 张三, 20)这个三元组就是元组的具体表现形式。而“学号”这一列所有可能的取值的集合就是这个属性对应的域。理解了这个基础元组演算和域演算的区别就清晰了元组演算以“行”为基本单位做变量声明和条件判断域演算以“列的值”为基本单位。用大白话说元组演算是“我一整行一整行地看”域演算是“我按每个单元格的值来判断”。这个区别看似简单但它决定了两种演算在表达能力上的差异也决定了它们在后来数据库实现中的命运。实际关系数据库管理系统里SQL的语义实现更靠近元组演算而域演算则更多地出现在一些形式化验证和逻辑推导的场景中。所以很多教材会偏重元组演算这是有工程原因的。2.2 元组演算的基本形式存在量词和全称量词元组演算的基本表达式长得像这样{t | P(t)}这个式子的意思是所有满足条件P的元组t的集合。理解这个表达式是理解整个元组演算的关键。它其实用了一阶谓词逻辑的语言来定义集合——你给我一个条件我从整个关系集合里把所有符合条件的行挑出来。这里最让初学者头疼的是两个量词存在量词∃和全称量词∀。存在量词好理解意思是“存在这样一行”全称量词麻烦一点意思是“对所有行都成立”。举一个特别典型的例子。给定学生表S学号, 姓名, 年龄, 系别和选课表SC学号, 课程号, 成绩要查“选修了全部课程的学生姓名”。这个需求用自然语言说很简单但用元组演算写就稍微绕一点{t[姓名] | S(t) ∧ (∀u)(C(u) → (∃v)(SC(v) ∧ v[学号]t[学号] ∧ v[课程号]u[课程号]))}这个式子的逻辑是我先找到学生元组t然后对所有课程元组u只要这门课程存在就必须存在一条选课记录v把t和u关联起来。外层是对所有学生遍历内层是对所有课程推导。如果某门课没有对应的选课记录这个学生的元组就会被排除。这个例子考研、软考、数据库系统工程师考试都爱出。很多人考试时靠死记硬背这个模板通过但如果你真的理解了它的推理过程你会发现在“对所有课程都选了”这种业务需求的SQL实现中你会有更清晰的改写思路——用NOT EXISTS还是用COUNT比较背后的逻辑基础就在这里。2.3 域演算按值域说话的另一种视角域演算的表达式长这样{x1, x2, ..., xn | P(x1, x2, ..., xn)}它声明的是若干个域变量每个变量取各自域中的值然后通过条件P来约束这些变量的组合。不用声明整行元组作为变量而是直接针对列的值做操作。域演算的一个经典例子是“查年龄大于18岁的学生姓名”{姓名 | (∃学号)(∃年龄)(∃系别)(学生(学号, 姓名, 年龄, 系别) ∧ 年龄 18)}这里的变量是学号、年龄、系别这些具体的值而不是一整行。它更像是用逻辑公式描述一张虚拟的结果表这一列放姓名那几列是约束条件涉及的值大家组合起来。从查询表达力的角度看域演算和元组演算是等价的——能用一个表达出来的查询另一个也能表达。但认知方式很不一样元组演算比较接近“行扫描”的直觉域演算更接近“列投影”加“筛选”的认知。有意思的是SQL的执行计划中投影操作和过滤操作确实是分开的域演算的这种拆分方式反而和物理执行过程有着某种呼应。给备考的朋友一个建议考试中域演算的题目不用抠太深掌握基本转换规则即可。但元组演算请务必理解透因为SQL的嵌套子查询语义和它高度同构。2.4 安全性问题别让查询结果无穷大学演算还有一个绕不开的话题——安全性。由于演算用的是谓词逻辑如果不加限制你完全可能写出一个公式它的结果集合是无限的。举个例子{t | ¬S(t)}的意思是“所有不在学生表S里的元组的集合”。如果元组的定义域是无限的自然数集合这个查询的结果也是无限的这在工程上毫无意义也不可能执行。所以数据库提供了“安全表达式”的概念一个演算表达式是安全的当且仅当它的所有可能结果都来自某个有限的域集合。实际操作中这个有限域集合通常来自关系实例中实际出现的值以及查询自身用到的常量。SQL之所以是可执行的查询语言根源就在于它隐含了这样的安全性约束。这事听着抽象但它在SQL中的影子很常见——为什么SQL的SELECT结果必须是有限的行数为什么子查询里的NOT IN在遇到NULL时会有诡异的行为这些工程问题追溯到底层都和安全语义有关。理解这些数学边角能帮你更淡定地面对那些“玄学”SQL问题。3. 演算到SQL的桥梁SEL/SELECT-WHERE结构与查询树的推演3.1 SQL的语义核心是“谓词约束元组投影”现在把视角从数学世界拉回工程世界。SQL的SELECT ... FROM ... WHERE ...结构和元组演算的{t | P(t)}存在清晰的对应关系FROM子句提供了元组遍历的范围WHERE子句扮演了谓词P的角色SELECT子句则决定了最终保留哪些分量。这个对应关系不是巧合SQL在设计时就是受了元组演算的启发。SQL的创始人Donald Chamberlin和Raymond Boyce在1974年发表的论文里明确提到了关系演算对SEQUEL语言设计的影响。所以当你写一条SQL时其实你已经在用元组演算的思维了只是平时没人提醒你这一点。工程上的一个特定实现是SELselect算子和它的参数化形式。其实不一定要停留在某个特定数据库产品的层面我们可以从通用角度理解一条SQL在执行计划层面会被拆成一系列逻辑算子包括扫描算子、过滤算子、投影算子、连接算子等。这些算子的排列组合形成了查询树而查询树是优化器做逻辑等价变换的基础。3.2 查询树的等价变换把公式变成可执行的方案我举个例子来说明查询树推演的过程。假设有两个表——订单表O订单号, 客户号, 金额, 状态和客户表C客户号, 姓名, 城市。现在要查“北京的客户在2024年下的金额大于1000元的订单”。SQL写出来很直观SELECT O.订单号, O.金额, C.姓名 FROM 订单 O JOIN 客户 C ON O.客户号 C.客户号 WHERE C.城市 北京 AND O.金额 1000 AND O.下单时间 BETWEEN 2024-01-01 AND 2024-12-31;在关系代数层面这条SQL对应的查询树是先做订单表扫描再做客户表扫描然后做连接操作最后做选择和投影。但问题是这个执行顺序是不是最优的优化器会做一件关键的事——把可以提前做的过滤操作尽量下推。比如可以在扫描订单表的时候就把金额1000和下单时间范围的过滤条件直接套在表扫描之上只把符合条件的订单行送入连接操作客户表也一样先把城市北京的客户过滤出来。这样进入连接操作的数据量会大幅减少连接代价也随之降低。在关系代数里这个操作的理论依据是选择和连接的可交换性σ_条件(连接(A, B)) 等价于 连接(σ_条件A(A), σ_条件B(B))只要过滤条件只涉及A或只涉及B就可以安全地下推。3.3 一个反直觉的改写案例EXISTS的演算本质关于等价变换我分享一个特别有意思的真实案例。有一次我在优化一条SQL业务场景是查“有订单的客户”。第一版SQL用的是IN子查询SELECT C.客户号, C.姓名 FROM 客户 C WHERE C.客户号 IN (SELECT O.客户号 FROM 订单 O WHERE O.状态 已完成);这条SQL在客户表数据量十万、订单表数据量百万的情况下执行计划出现了很糟糕的选择——优化器选择了先全量扫描订单子查询再对客户表做逐行探测。虽然订单上建有客户号索引但子查询需要先过滤状态已完成索引使用效率不高导致执行时间飙到十几秒。我当时的处理是把IN改写为EXISTSSELECT C.客户号, C.姓名 FROM 客户 C WHERE EXISTS (SELECT 1 FROM 订单 O WHERE O.客户号 C.客户号 AND O.状态 已完成);改写后优化器可以更灵活地选择执行策略把客户表作为驱动表订单表通过客户号索引进行关联探测执行时间从十几秒降到了几百毫秒。那么用演算怎么解释这个改写的正确性IN子查询的语义是“客户号在子查询结果集合中”EXISTS的语义是“存在一条订单记录满足条件”。用元组演算的语言来说IN对应的是外层元组的值是否属于一个已确定的集合EXISTS对应的是一个存在量词推导。这两个表达式的逻辑等价性正是谓词逻辑中“元素属于集合”和“存在量词断言”之间的同义变换。这个案例给我们的启示是优化器虽然能做很多自动重写但它依赖统计信息、索引结构、代价模型并不总是智能到能识别所有等价的写法。作为工程师理解底层演算语义能在关键时刻用人工改写的方式帮优化器一把。4. 优化器内部视角从逻辑树到物理计划的完整推理链路4.1 逻辑优化和物理优化其实是两层不同的决策深入查询优化器内部你会发现它做的事情远不止“等价重写”这么简单。一个完整的查询优化流程可以拆成两个层面逻辑优化和物理优化。逻辑优化是在不改变查询语义的前提下对查询树做结构性的等价变换。常见的手法包括谓词下推把过滤尽可能提前、子查询展开把子查询改写成连接、连接重排序决定多表连接的先后顺序、视图合并把视图的定义并入主查询。这些变换的理论基础就是前面说的关系代数和演算的恒等性。物理优化则是在逻辑计划确定之后决定每一具体操作的实现方式表的访问路径是走全表扫描还是索引扫描连接算法是用嵌套循环、哈希连接还是排序合并是否需要额外的排序操作并发执行的程度如何设定这一层依赖的是数据库系统内部的代价模型、统计信息和物理存储结构。打个比喻逻辑优化像是你在规划旅行路线时决定“先去北京再去上海还是先去上海再去北京”物理优化则像是决定“这两座城市之间坐高铁还是飞机”。前者关注的是步骤的先后和内容后者关注的是每一步的执行方式。4.2 代价模型的两个核心组件基数估计和成本公式理解了这两层优化之后我想带你看看优化器做决策的最关键依据——代价模型。绝大多数现代数据库的代价模型都包含两个核心组件基数估计和成本公式。基数估计是估算某个中间结果集有多少行成本公式则是根据基数估算、数据分布、物理存储特性来算一个操作的代价数值。优化器会枚举多个可能的计划用代价模型估算每个计划的总代价然后选代价最小的那一个。基数估计的难度远超想象它需要统计信息、数据分布假设通常是均匀分布或直方图、列之间有相关性假设。当统计信息过期、数据分布不均匀基数估计的结果会产生数量级偏差优化器就会做出灾难性的执行计划选择。这也是为什么实际运维中要定期做统计信息更新比如ANALYZE、UPDATE STATISTICS以及为什么一些简单的SQL会莫名奇妙地走错执行计划。成本公式的细节因数据库产品而异但大框架非常相似。对全表扫描来说代价和表的总行数、块数成正比对索引扫描来说代价和选择度、索引层数、回表次数相关对连接操作来说代价取决于驱动表行数、被驱动表探测代价、内存可用量等。理解这些你才能读懂执行计划里那些数值的含义。4.3 统计信息对执行计划的影响一个被忽视的杀手我在一线工作中遇到过太多因为统计信息问题导致的性能事故。有一个印象很深的例子某业务表的数据量从十万涨到五千万但因为自动统计信息的阈值设置不当分区级的统计信息没有及时更新优化器以为这张表还是十万行于是选择了一个对小表友好的连接顺序——大表驱动小表。结果执行计划的实际运行时间从几十毫秒膨胀到十几分钟。当时排查的完整链路是这样的慢查询日志捕捉到一条SQL的执行时间异常拉长平时几毫秒最近稳定在十几分钟。用EXPLAIN看执行计划发现连接顺序明显反直觉大表作为驱动表小表作为被驱动表。查看统计信息刷新时间发现该表最近一次ANALYZE是在三个月前当时数据量确实是十万行。手动执行ANALYZE强制刷新统计信息。再看执行计划连接顺序已经纠正SQL执行时间恢复到毫秒级。这个案例里没有任何SQL写法的问题纯粹是统计信息滞后导致的优化器判断失误。这个教训我后来在团队里反复强调遇到SQL性能突变第一件事永远先确认统计信息是否新鲜再去怀疑SQL本身的问题。4.4 连接顺序选择的天花板当优化器也无能为力连接顺序的选择是查询优化中最难的问题之一因为n个表的连接顺序有n!种可能每一种还需要考虑对应的连接算法组合。对于7表或者10表以上的连接全枚举的代价就已经高到不可接受了所以现代优化器几乎都采用动态规划和启发式搜索相结合的方式。但这带来一个新的问题——启发式策略在多数情况下表现良好但在极少数边界场景下会选出明显次优的计划。工程师对这类查询的处理方式是分析连接关系图谱找出最小、最有选择性的子集先行连接再逐级扩展或者干脆用查询提示hint锁定连接顺序。我在给客户做性能优化时有一条心得对复杂查询与其让优化器做全空间搜索不如人工拆解。把一个大查询拆成几个中间结果表每一步都确保执行计划可控。这种做法的代价是额外存储和多一步ETL但换来的是执行计划的稳定性和可预测性。对生产环境的稳定性要求而言这是值得的。5. 实战看执行计划以EXPLAIN输出为例的慢查询定位方法论5.1 执行计划阅读的基本顺序从嵌套最深到最外层理论知识说了一大堆现在落到最实务的部分——怎么通过执行计划定位和修复慢查询。一个执行计划通常以树状结构呈现无论是MySQL的EXPLAIN输出、PostgreSQL的EXPLAIN还是Oracle的执行计划输出核心逻辑是一致的。阅读执行计划的正确顺序其实是自内向外、自底向上先看每个表中访问路径的代价再看连接操作是不是按合理顺序执行最后看最外层的结果集构造是否有不必要的开销。很多新手犯的错误是一上来就盯着第一个节点看这容易漏掉关键问题。我从一次具体的性能排查讲起。有一个场景论坛系统的帖子列表页需要展示每个帖子的标题、作者名、最新回复人和回复时间。SQL长这样SELECT p.title, u.name AS author_name, r.name AS reply_user, r.reply_time FROM post p JOIN user u ON p.author_id u.id LEFT JOIN LATERAL (SELECT ru.name, r.reply_time FROM reply r JOIN user ru ON r.user_id ru.id WHERE r.post_id p.id ORDER BY r.reply_time DESC LIMIT 1) r ON TRUE WHERE p.status 1 ORDER BY p.update_time DESC LIMIT 20;这条SQL在数据量上去之后变得很慢压测时平均响应时间达到3秒。先看执行计划的关键部分。5.2 从执行计划里读出的三个问题EXPLAIN ANALYZE输出显示外层post表扫描走了索引idx_post_status_update没问题但内层LATERAL子查询对reply表的探测居然走了全表扫描。为什么原来reply表的post_id列上虽然有索引但索引的统计信息显示该列的重复值极多优化器估算走索引的回表代价高于全表扫描。问题出在回复表和帖子表的数据倾斜——少量热帖集中了大量回复导致优化器做了错误的基数估计。第二个问题是ORDER BY p.update_time DESC要求排序而执行计划的排序节点使用了临时文件排序filesort内存排序缓冲区设置过小导致磁盘排序。第三个问题是LEFT JOIN LATERAL在PostgreSQL里是逐行调用子查询这在逻辑上是串行的无法并行化。当驱动表需要扫描的行数很多时串行代价会被线性放大。5.3 修复方案和实施步骤针对这三个问题我当时的处理方案如下第一步修正基数估计——重新采集reply表的统计信息并且给post_id这一列建立覆盖索引idx_reply_post_user_time (post_id, user_id, reply_time)。覆盖索引可以让子查询中的过滤和排序都在索引层面完成不需要回表。第二步调整排序参数——把排序缓冲区从默认的2MB调整到32MB同时检查是否可以通过调整索引让结果天然有序。最终是在post表的update_time列和status列上建立了组合索引让外层查询的过滤和排序在同一个索引扫描中完成直接消除了排序节点。第三步重写LATERAL子查询。这里我能想到的最佳实践是如果热帖的回复数量本身就不多可以接受一笔额外的预聚合如果热帖集中更适合的方式是把“每个帖子最近回复”这个逻辑做成物化结果。事实上在很多高并发社区场景里这类“最新回复”数据都是异步写入缓存或单独汇总表的实时跑SQL反而是不合理的架构。优化后的执行计划里三个性能瓶颈全部消除SQL平均响应时间从3秒降到80毫秒。这个案例的典型意义在于它同时涉及了统计信息、索引设计、参数配置、SQL结构四个维度而这些恰恰是查询优化中最常见的四个切入点。提示看执行计划时关注三种特定的标志性字段——filtered比例过低说明索引选择性差、filesort或sort节点出现说明排序无法利用索引、临时表出现说明结果集产生中间落盘。这三个信号基本覆盖了90%的慢查询根因。5.4 常见的执行计划误读和我踩过的坑执行计划阅读的坑也值得专门说一说。第一个坑把节点的输出行数当成实际行数。在执行计划中节点输出行数是优化器的基数估计值而不是实际执行行数。如果想看实际行数要用EXPLAIN ANALYZE或者EXPLAIN (ANALYZE, BUFFERS)它会真实执行查询并回传实际行数和实际耗时。只读估算行数很容易被误导。第二个坑忽略缓冲BUFFERS信息。很多执行计划的慢不是慢在计算而是慢在I/O。BUFFERS字段能告诉你这个节点访问了多少个数据块其中多少是命中了共享缓冲区的。如果某个节点读取的块数非常多且命中率低说明存在严重的随机I/O需要通过索引调整来访问更少的数据块。第三个坑直接把执行计划中的总代价拿来排序比较。代价数值是相对的不同数据库、不同版本、不同参数下的代价基准都不一样。我自己更习惯的做法是看计划结构是否符合直觉——有没有哪个节点做了不合理的全表扫描有没有多表连接的驱动顺序反了有没有该走索引却走了扫描。结构对加上实际耗时验证比纠结具体数值更可靠。6. 从演算到现代场景枚举元组、C#值元组解构和毕达哥拉斯三元组的视角6.1 枚举元组当“元组”遇上现代编程语言聊了很多数据库系统的内容我想再扩展一下“元组”这个词在现代编程中的含义因为这个概念对数据库工程师来说既熟悉又陌生。在C#、Python、Rust等现代编程语言中元组tuple是一种轻量级的数据结构用于打包一组异构的值。C#从7.0开始引入了值元组ValueTuple和解构语法这使得开发者可以这样写var (name, age) GetUserInfo(userId); Console.WriteLine(${name} is {age} years old.);这种语法本质上是在做元组的解构——把一个元组变量按位置拆成多个命名变量。这恰好呼应了域演算的思维按域变量取值而不是把整个元组作为一个整体去操作。对数据库开发者来说理解现代语言中的元组操作有一个实际好处当你编写ORM查询或者做DTO映射时你实际上是在关系的元组和编程语言的元组之间做翻译。批量枚举元组、按位置解构、按属性命名这些操作背后的思维模型和关系数据库的行列模型是同构的。6.2 毕达哥拉斯三元组一个经典的域演算思维练习题“noj毕达哥拉斯3元组”这个热搜词也很有意思。毕达哥拉斯三元组指的是满足a² b² c²的三个正整数比如(3, 4, 5)。用这个例子来做域演算的思维练习特别合适因为它需要你声明三个域变量然后描述它们之间的约束关系。用域演算来表达“找出所有毕达哥拉斯三元组”{(a, b, c) | a ∈ N ∧ b ∈ N ∧ c ∈ N ∧ a² b² c² ∧ 1 ≤ a b c ≤ 100}这个公式本质上是一个约束搜索问题的声明式描述。你声明三个整数变量给出取值范围和约束条件剩下的交给执行器去做。这正好对应了SQL中一个经典问题的写法——生成三个范围笛卡尔积后做筛选WITH numbers AS (SELECT generate_series(1, 100) AS n) SELECT a.n AS a, b.n AS b, c.n AS c FROM numbers a, numbers b, numbers c WHERE a.n b.n AND b.n c.n AND a.n * a.n b.n * b.n c.n * c.n;这个写法虽然直观但性能算不上好——范围小时还能接受范围一旦扩大笛卡尔积的规模就是O(n³)优化器也无法把这种约束转换成索引友好的计划。工程上的处理方式通常是缩小搜索范围、用数学边界剪枝、或者事先生成缓存表。这个例子恰好揭示了声明式语言的一个根本矛盾表达简洁不等于执行高效优化器能做的重写是有限度的。6.3 数据库查询优化器在AI和现代数据栈中的角色变迁把话题拉回查询优化器本身。在现代数据栈里查询优化器早已不局限于传统关系型数据库。Spark SQL、Presto/Trino、ClickHouse这些大数据引擎都有自己的一套查询优化策略但底层依然是在做逻辑重写和物理计划选择。它们面对的场景更极端——数据规模更大数据源更异构查询模式更多样。值得注意的是近年来业界在探索用机器学习技术改进基数估计和代价模型。比如把统计信息丢给神经网络去学习数据分布或者用强化学习来决定连接顺序。这些尝试的方向是对的但受限于训练数据获取成本、模型解释性和推理延迟离大规模落地还有距离。对一线工程师来说与其等待优化器变得更智能不如更勤奋地理解它现有的决策逻辑。在当前这个数据环境下有另一种趋势很值得关注很多团队开始主动绕过通用优化器用物化视图、预计算、增量更新等方式来回避“查询时优化”的问题。这其实是一种务实的工程折中——既然查询优化器在复杂场景下难以做到最优不如把复杂计算挪到写入端或离线批处理端。能用预计算解决的查询不要在查询时挑战优化器。7. 备考复习与工作实践的结合路径我的个人操作经验聊了这么多理论、工程和案例最后分享一些实际的备考和工作经验。如果你是准备数据库系统工程师考试的考生我的建议是不要只背符号公式也不要完全放弃理论。把所有演算表达式和关系代数表达式翻译成SQL再翻译回表达式来回做几遍。这个过程的收益远超你的想象——它让你建立的是语义层面的等价关系网络而考试题考察的恰好就是这种等价变换能力。我当初备考时的一个具体做法是把历年真题里的关系代数、元组演算、SQL三者的互转题目自己整理成一张对应表。每种查询模式选择、投影、连接、分组、去重、嵌套都写清楚三种表达方式的对应规则。复习效果非常好而且这份对应表在后来工作中分析SQL执行计划时依然能派上用场。工作中建议重点掌握几个实操技能EXPLAIN的深度使用、统计信息的手动管理、覆盖索引的设计原则、子查询和连接的手工改写。这些技能在慢查询排查中的实用性几乎是日常性的。我给自己团队定的底线是每个人都能在十分钟内定位一条慢SQL的根因并给出至少两种优化思路。能做到这一点数据库系统工程师这个头衔才算真正名副其实。最后再分享一个小技巧。每次优化完一条SQL我会把优化前后的SQL和执行计划的对比截图保存下来附带一段文字说明根因。半年下来就是一个非常有价值的案例库。下次有人问“为什么这条SQL变慢了”直接翻案例库比重新排查一遍高效太多。这种积累方式对个人成长和团队沉淀都有好处。