想象你是一家餐厅后厨的主厨。晚高峰的菜单上有一桌菜,食材分别装在不同篮子里:一小篮蒜末、一大篮五花肉、一筐时蔬、一碟豆腐。你当然不会把整筐时蔬倒进肉篮里乱翻——你会先决定“先动哪一篮”,再按合理的顺序逐步搭配。数据库执行 SQL 里的 JOIN 操作,和后厨备菜如出一辙:多张表就是多篮食材,连接顺序就是备菜顺序。顺序选对,一桌菜行云流水;顺序选错,后厨翻箱倒柜,顾客等到天荒地老。这篇文章不堆砌复杂的原理,而是带你把“先动哪一篮”这件事彻底想明白:怎么读懂后厨工序单(执行计划)、怎么相信库存台账(统计信息)、什么时候老板该拍板(hint),以及如何评估整桌菜的耗时(代价模型)。读完你会发现,JOIN 顺序优化,其实就是一门“合理排序”的学问。

岗位关联

学会 JOIN 顺序优化,你能像老主厨一样从执行计划中一眼看出查询的“备菜顺序”是否合理,让复杂的多表查询和大数据量关联不再成为线上事故的源头——这是后端工程师与 DBA 从“能跑”迈向“会调”的必修课,也是 AI 工程中大数据处理管道的必备技能。

目录
  1. 可视化演示
  2. 驱动表选择
  3. 执行计划解读
  4. 连接算法
  5. 统计信息
  6. Hint调优
  7. 代价模型
  8. 多表关联
  9. 调优实战
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示JOIN顺序优化的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

JOIN顺序优化:JOIN JOIN顺序对性能的影响机制 驱动表选择的量化标准 核心机制可视化 JOIN顺序优化整体架构与关键组件

驱动表选择

想象你是一家餐厅后厨的主厨。晚高峰的菜单上有一道小炒肉,灶台上摆着两篮食材:一小篮蒜末只有 3 颗,一大篮五花肉有 8 块。要做这道菜,每颗蒜末都要找到编号对应的那块五花肉。聪明的做法是:拿起蒜末篮,一颗一颗去肉篮里翻找,总共只翻 3 次;如果反过来拿起肉篮,一块一块去蒜末里找,要翻 8 次。谁先被拿起,谁就决定了整道菜的动作量。

数据库执行 JOIN 时,同样存在“先拿起哪张表”的问题。在嵌套循环连接中,优化器会选一张表作为驱动表,逐行取出记录,再到另一张被驱动表中查找匹配行。驱动表的行数,直接决定了发起查找的次数——行数越少,查找动作越少。这就是“小表驱动大表”这条最朴素调优原则的由来。

但“小”不是绝对概念。若被驱动表的连接字段没有索引,每次查找都会退化成全表扫描,成本将按两张表的行数相乘;反之若有索引,每次查找只是一次高效的索引探测。驱动表的判定依据有三:行数是否足够小、过滤条件能否提前缩小它、连接字段是否能让被驱动表走索引。一个典型的估算公式如下。

主厨的经验之谈是“先拿量少的篮”,但也得看菜谱:如果肉篮的编号已经按格子排好(索引),拿肉篮未必吃亏。真正要看的,是工序单上估算的总耗时——驱动表选得好,台面清爽、动作利落;选得差,后厨翻箱倒柜。

CNLJ≈Rdrive×(Cprobe+m‾)\displaystyle C_{NLJ} \approx R_{drive} \times \left( C_{probe} + \overline{m} \right)
  1. 驱动表行数 R_drive整个连接的“探测发起次数”等于驱动表的行数,每一行都要去内层查找一次匹配。
    RdriveR_{drive}
  2. 单次探测成本 C_probe每次在内层表做一次索引查找的开销,包含 B+ 树若干层磁盘读取。
    CprobeC_{probe}
  3. 平均匹配行数 m̄每个驱动行平均命中几条内层记录,直接决定结果集大小与回表成本。
    m‾\overline{m}

执行计划解读

后厨里有一张被反复修改的工序单,上面写着:先洗蒜末、再切肉、然后下锅。主厨每接到新菜式,第一件事就是看工序单顺不顺。数据库也有自己的“工序单”,名字叫执行计划(Execution Plan),用 EXPLAIN 命令就能打印出来。

执行计划不仅显示表的连接顺序,还显示每张表的访问路径:是全表扫描——把整篮食材倒出来挨个看;还是走索引——按编号直接翻到对应的格子。不同数据库的阅读方式不同:MySQL 的传统 EXPLAIN 按行从上往下读,第一行通常是驱动表;PostgreSQL 的 EXPLAIN 是树形结构,要从下往上读,最底层的节点最先执行。

读计划时,优先确认三件事:谁是驱动表、每张表走的是索引还是全表扫描、每层估算的行数是否合理。估算连接结果集大小的简化公式如下。

主厨拿到工序单,先看每步预估多久。如果预估明显离谱——比如写着“黄瓜有 100 万根”,就知道台账(统计信息)该更新了。读懂工序单,是调优的第一步,也是最后验证的唯一依据。

∣R⋈S∣≈f×∣R∣×∣S∣\displaystyle |R \Join S| \approx f \times |R| \times |S|
  1. 表行数 |R| 与 |S|参与连接的两张表在过滤后的估算行数,是连接结果规模的起点。
    ∣R∣,∣S∣|R|, \quad |S|
  2. 选择率 f连接条件下行对能够匹配的比例,来自统计信息;f 越小,结果集越小。
    f=匹配行对数∣R∣×∣S∣f = \frac{\text{匹配行对数}}{|R| \times |S|}

连接算法

后厨有三种搭配食材的手艺。第一种是“逐个试味”:从主料篮取一件,去配料篮里挨个配对,适合量小的精致菜;第二种是“先分桶再配对”:把两篮食材都切好,按编号丢进对应的小桶,同桶直接配对,适合宴席大锅菜;第三种是“先排队再合并”:两边都按编号排好队,两队同步往前走,遇到同编号就组合。

对应到数据库,三种手艺就是三种连接算法。嵌套循环连接(Nested Loop Join)适合一边很小、另一边能走索引的场景;哈希连接(Hash Join)适合大表等值连接,先把较小的一边读进内存建哈希表,再逐行探测另一边;排序合并连接(Sort-Merge Join)适合两边本就按连接字段排好序,或者连接条件是 <、<= 这类不等值场景。

三种算法的代价量级差别很大。嵌套循环最坏情况下要把两张表的行两两比较一遍;哈希连接则把两张表各完整读入、写出一次即可。两者的估算公式如下。

手艺没有绝对好坏,只有合不合适。小份菜品逐个试味最快,宴席大锅菜分桶配对更利索。连接算法也一样:行数、内存、排序状态共同决定选哪种,而驱动表的选择总是与算法配套出现的。

CNL=∣A∣×∣B∣,CHJ≈2×(∣A∣+∣B∣)\displaystyle C_{NL} = |A| \times |B|, \qquad C_{HJ} \approx 2 \times \left( |A| + |B| \right)
  1. 嵌套循环代价 C_NL外层每行与内层每行都要比较一遍,总比较次数是两张表行数之积。
    CNL=∣A∣×∣B∣C_{NL} = |A| \times |B|
  2. 哈希连接代价 C_HJ建哈希表读一遍 A,探测读一遍 B,读写各算一次,总代价近似两倍的表行数之和。
    CHJ≈2×(∣A∣+∣B∣)C_{HJ} \approx 2 \times \left( |A| + |B| \right)

统计信息

后厨角落挂着一本库存台账,记录每篮食材的数量、新鲜度、大小规格。主厨排工序前都要翻一翻:台账写着五花肉只有 3 块,他就不会排 8 道肉菜。数据库的“台账”,就是统计信息(Statistics)。

统计信息包含三块核心内容:表行数、列基数(某个字段去重后有多少个不同值)、数据分布(直方图)。优化器靠它估算每张表过滤后剩多少行、连接产生的中间结果有多大。台账越准,估算越靠谱;台账过期——比如表里新插了几百万行却从没执行过 ANALYZE——估算就会失真,计划自然跑偏。

估算过滤后行数的最基本公式是“总行数乘以选择率”。

台账不更新,再好的主厨也会把 8 块五花肉当成 3 块来排菜,工序单全错。调优的第一步永远是:先看台账新不新鲜,再谈其他。

c^=⌈N×f⌉\displaystyle \hat{c} = \lceil N \times f \rceil
  1. 表行数 N台账记录的表总行数,是估算的起点。
    NN
  2. 选择率 f过滤条件预计留下的比例,由列基数和直方图推算;f 越小结果越少。
    ff
  3. 向上取整符号保证估算行数至少为 1,避免优化器误判“这张表不用访问”。
    ⌈⋅⌉\lceil \cdot \rceil

Hint调优

大厨参谋(优化器)平时很靠谱,但偶尔也会犯轴:明明台账写着小篮更省事,它偏要先动大篮。这时候老板可以直接拍板:“今天先做排骨,再做时蔬!”在 SQL 里,这个拍板动作就是 Hint。

Hint 是嵌在 SQL 注释里的优化器指令。Oracle 用 /*+ LEADING(t1 t2) */ 指定连接顺序,还可以用 USE_NL、USE_HASH 指定连接算法;MySQL 8.0 提供了 JOIN_ORDER、JOIN_FIXED_ORDER 等 hint。它的语义是“尽量按我说的来”,但优化器如果算出代价悬殊,仍可能部分忽略。

为什么不能一上来就用 hint?因为优化器的目标是最小化总代价,hint 只是给这个最小化问题加一条“顺序约束”,缩小搜索范围。如果统计信息过期或数据分布改变,强制的旧顺序可能变成灾难。正确用法是:先刷新统计信息、检查索引,再用 hint 验证某个顺序假设,最后用 EXPLAIN 对比确认收益。代价公式如下。

老板拍板要拍得有依据——先翻台账、先听参谋,拍错了板,整桌菜反而更慢。hint 是调优的最后固化手段,而不是第一板斧。

Cplan=∑j=1k(Cio(j)+Ccpu(j))\displaystyle C_{plan} = \sum_{j=1}^{k} \left( C_{io}^{(j)} + C_{cpu}^{(j)} \right)
  1. 求和符号把执行计划中 k 个步骤(扫描、连接、排序)的代价依次累加。
    ∑j=1k\sum_{j=1}^{k}
  2. IO 代价 C_io第 j 步读取数据页的磁盘开销,通常是代价大头。
    Cio(j)C_{io}^{(j)}
  3. CPU 代价 C_cpu第 j 步比较、过滤、计算付出的处理开销。
    Ccpu(j)C_{cpu}^{(j)}

代价模型

参谋的手账里,每步备菜动作都被折算成“分钟 + 人力”:洗菜要水费(IO),切菜要功夫(CPU),装盘要占台面(内存)。哪个方案的总分钟数最少,就推荐哪个方案。数据库优化器的手账,就是代价模型(Cost Model)。

代价模型给每个候选计划打分,分三块:IO 代价——读多少磁盘页;CPU 代价——比较与过滤的计算量;内存代价——哈希表等结构占用的空间。其中 IO 通常是大头,因为磁盘访问比内存慢几个数量级。

一个简化的代价公式如下。

主厨不看单步动作快慢,只看整桌菜的预估总分钟数。判断 JOIN 顺序好不好也一样:别凭直觉争论“谁先谁后”,看 EXPLAIN 里各行扫描行数的总和——那才是参谋手账上的真金白银。

Ctotal=Cio+Ccpu+Cmem,Cio≈Bpages×tpage\displaystyle C_{total} = C_{io} + C_{cpu} + C_{mem}, \qquad C_{io} \approx B_{pages} \times t_{page}
  1. 总代价 C_totalIO、CPU、内存三项之和,优化器在所有候选中选总代价最小的计划。
    CtotalC_{total}
  2. 页数 B_pages需要读取的磁盘页数量,受扫描行数与行宽影响,是 IO 代价的核心变量。
    BpagesB_{pages}
  3. 单页耗时 t_page磁盘读取一页数据的平均耗时,不同存储设备取值不同。
    tpaget_{page}

多表关联

宴席不止一道菜。八道菜的食材篮互相依赖:排骨配时蔬、时蔬配蒜末、蒜末配豆腐。主厨不能只盯一道菜,要把整个备菜流程排成一张网。数据库面对多张表 JOIN 时,也要做同样的“排网”。

图论帮我们看清这张网:每张表是图的一个节点,JOIN 条件是两个节点之间的边。优化器的任务,是在这张图上找一条“先连谁、再连谁”的路径——也就是连接树——让总代价最小。最常见的形态是左深树:每一步只把当前结果与下一张表连接,中间结果逐层传递。

但候选计划的爆炸速度远超直觉。

主厨把八道菜的备菜顺序排成网后,会优先把出菜快、量少的菜放在前面,让台面始终不堆积。多表 JOIN 同理:让中间结果始终小,整张网才能跑得快。

Pleft_deep=n!×2 n−1\displaystyle P_{left\_deep} = n! \times 2^{\,n-1}
  1. 阶乘 n!n 张表的不同连接顺序数量,表越多增长越恐怖,5 张表就有 120 种。
    n!n!
  2. 方法数 2 的 n−1 次方每步连接有两种主要算法可选(嵌套循环 / 哈希),n−1 步连接产生 2 的幂次组合。
    2n−12^{n-1}
  3. 乘积左深树形态下候选计划总数,5 张表即约 1920 个计划,8 张表超过 500 万个,必须靠动态规划与贪心剪枝。
    n!×2n−1n! \times 2^{n-1}

调优实战

晚高峰结束后,主厨开始复盘:为什么昨晚宴席迟了半小时?翻开工序单发现,主菜被排到了最后,配菜先是切了一半台面。调整顺序后,同样的 20 桌菜提前收工。调优从来不是玄学,而是一套可重复的复盘流程。

JOIN 调优的实战流程可以固化为四步:第一步定位——从慢查询日志中找到最耗时的 SQL;第二步读单——用 EXPLAIN 查看连接顺序、访问路径和扫描行数;第三步假设——判断是驱动表选反了、索引缺失了,还是统计信息过期了;第四步验证——调整后重新 EXPLAIN 并对比真实耗时。每步都用数据说话,而不是靠感觉。

验证提升幅度可以用一个简单的公式量化。

复盘的意义不在记住某个具体顺序,而在于掌握“先看工序单、再算总时长”的方法。下一次再遇到慢查询,你不再是一筹莫展地看日志,而是像个老主厨一样,翻开台账、对照工序单、拍板、验证,一气呵成。

G=Tbefore−TafterTbefore×100%\displaystyle G = \frac{T_{before} - T_{after}}{T_{before}} \times 100\%
  1. 优化前耗时 T_before作为基准线的旧查询耗时。
    TbeforeT_{before}
  2. 优化后耗时 T_after调整后的新查询耗时。
    TafterT_{after}
  3. 提升百分比 G两者之差除以基准,量化这次调优的收益,经验上扫描行数降一个数量级,耗时通常也降一个数量级。
    Tbefore−TafterTbefore×100%\frac{T_{before} - T_{after}}{T_{before}} \times 100\%

AI 应用场景

推荐系统特征宽表的构建

阶段:特征工程

解决的问题:构建用户-行为-商品特征宽表时,多张大表随意 JOIN,驱动表选错导致任务跑数十分钟甚至数小时;AI 工程师往往把时间浪费在等查询上,而不是调模型。

具体案例:某推荐团队每天用 500 万行行为表 JOIN 10 万行用户表和 50 万行商品表。最初把用户表放在最后当被驱动表,扫描次数爆炸,任务耗时 1.2 小时;改为让用户表(10 万行)作驱动表逐层连接,并刷新统计信息后,耗时降到 3 分钟。

大模型训练数据的清洗去重

阶段:数据预处理

解决的问题:清洗海量语料时,需要把“重复指纹表”与“主语料表”JOIN 找出重复项;表体量都在亿级,连接算法选错会在构建哈希表时内存溢出(OOM),任务反复重启。

具体案例:用哈希连接把 2 亿行主语料与 3000 万行重复指纹表关联,选择行数较少指纹表建哈希(build),内存占用稳定在 4GB 以内,任务从“频繁 OOM 重启”变为稳定 15 分钟完成。

用户画像聚合报表服务

阶段:BI 分析与运营报表

解决的问题:运营日报需要多表 JOIN 计算活跃、付费、留存指标;连接顺序不佳导致凌晨定时任务超时,数据团队每天上班第一件事是处理告警。

具体案例:通过 EXPLAIN 发现驱动表选错且统计信息过期,执行 ANALYZE 刷新台账并调整连接顺序后,日报生成时间由 50 分钟降到 6 分钟,定时任务从此稳定收敛。

Python 演示

python50 行
# -*- coding: utf-8 -*-"""小表驱动大表: 模拟优化器评估多表JOIN顺序的最小代价""" TABLE_SIZES = {    'users': 100_000,    'orders': 5_000_000,    'items': 500_000,    'deals': 2_000_000,} ROW_SCAN = 0.1      # 扫描一行数据的基础代价INDEX_PROBE = 2.0   # 对内表索引发起一次探测的代价  def nested_loop_cost(outer_rows, inner_rows):    """嵌套循环连接估算: 外层每行都要发起一次索引探测"""    return outer_rows * (ROW_SCAN + INDEX_PROBE) + inner_rows * 0.01  def plan_cost(order):    """给定连接顺序, 累计每一步的估算代价"""    total = 0.0    current = TABLE_SIZES[order[0]]    for i in range(1, len(order)):        inner = TABLE_SIZES[order[i]]        total += nested_loop_cost(current, inner)        current = min(current, inner)   # 连接后中间结果只会变小或持平    return total  def best_and_worst(tables):    import itertools    plans = list(itertools.permutations(tables))    best = min(plans, key=plan_cost)    worst = max(plans, key=plan_cost)    return best, worst  def main():    tables = ['users', 'orders', 'items', 'deals']    best, worst = best_and_worst(tables)    print('最优连接顺序:', ' -> '.join(best))    print('最优估算代价: %.2f' % plan_cost(best))    print('最差连接顺序:', ' -> '.join(worst))    print('最差估算代价: %.2f' % plan_cost(worst))    print('差距倍数: %.2f' % (plan_cost(worst) / plan_cost(best)))  if __name__ == '__main__':    main()
目的:用 Python 模拟数据库优化器的代价评估,遍历 4 张表的所有连接排列,找出代价最小与最大的 JOIN 顺序,直观展示“小表驱动大表”能拉开多少倍的性能差距。
  1. 'orders': 5_000_000,台账里 orders 是最大的表,有 500 万行,是天生不适合当驱动表的“大肉篮”。
  2. return outer_rows * (ROW_SCAN + INDEX_PROBE) + inner_rows * 0.01外层行数乘上探测成本,是整步代价的大头;内层规模只占小头。
  3. current = min(current, inner)模拟连接后中间结果取较小者,保持“小规模中间结果”,这正是多表连接下推的核心思想。
  4. best = min(plans, key=plan_cost)用代价函数当标尺,从 24 种排列中挑出总代价最小的最优顺序。
  5. print('差距倍数: %.2f' % (plan_cost(worst) / plan_cost(best)))打印最优与最差的差距,用数字感受顺序选择的威力。

常见误区

误区我只要把行数最小的表写在 FROM 最前面,MySQL 就会让它当驱动表,而且一定最快。网上都这么说,照着做准没错。

正解驱动表由优化器根据代价决定,SQL 书写顺序基本不影响执行顺序;而且“小表驱动大表”只在嵌套循环连接且内层有索引时严格成立。若用的是哈希连接,两边都要完整读一遍,谁当驱动差别不大;若内层没有索引,嵌套循环会变成两层全表扫描,行数小的一方也救不回来。正确做法是看执行计划里的实际顺序和访问路径,而不是盯着表的大小猜。

为何常见:口诀式教学流传太广,“小表驱动大表”被简化成“写在前面 + 最小表 = 一定快”,忽略了索引、连接算法等前提条件。

误区我一直在 MySQL 里用 EXPLAIN,从上往下看顺序,换成 PostgreSQL 应该也一样吧?

正解MySQL 传统 EXPLAIN 输出按行列表,第一行即先访问的表;但 PostgreSQL 的 EXPLAIN 是树形输出,从下往上读,最底层节点最先执行。若用 MySQL 8.0 的 EXPLAIN FORMAT=TREE,也是树形嵌套结构,同样需要先读最内层。不同数据库阅读方式不同,把顺序读反会把驱动表看成被驱动表,调优方向全反。

为何常见:只接触过一种数据库,把工具细节当成了放之四海皆准的通用知识。

误区昨天加 `/*+ LEADING(a b) */` 快了 10 倍,以后所有查询都照抄这个套路,写进代码就再也不管了。

正解hint 只是给优化器一个“顺序约束”,数据量变化、统计信息更新、索引新增后,被强制执行的旧顺序可能变成灾难。正确姿势:先刷新统计信息、检查索引,再用 EXPLAIN 验证假设,hint 作为“最后的固化手段”并写好注释。把 hint 当万能药,等于老板不看台账乱拍板,迟早翻车。

为何常见:调优案例中 hint 效果立竿见影,容易让人忽略其背后的前提:统计信息准确、数据分布稳定、索引结构不变。

误区反正 A JOIN B 和 B JOIN A 结果一样,先连谁应该没什么差别吧?数据库最后都会算对的。

正解结果集在多表等值连接下确实相同(满足交换律),但执行代价天差地别。差的顺序可能让中间结果膨胀到几千万行,后续每一步都要扫描大得多的数据集;好的顺序让中间结果始终保持很小。优化器本质上就是在成千上万种“语义相同、代价不同”的计划里挑最小的,顺序正是代价差异的最大来源。

为何常见:把“语义等价”误解成“性能等价”,忽略了数据库是过程式计算,步骤顺序决定中间产物大小。

面试题

Q1. 关于数据库 JOIN 中的“驱动表”,下列说法正确的是?

单选题 · easy

  • A.驱动表一定是查询里最大的表
  • B.驱动表通常是连接中先被访问的表← 正确答案
  • C.驱动表只存在于哈希连接中
  • D.驱动表由 SQL 书写顺序决定

答案:B。驱动表是执行计划里最先被访问、逐行发起探测的表,由优化器根据代价选择,与 SQL 书写顺序无关;哈希连接同样有驱动和被驱动之分。就像主厨先拿哪一篮食材,跟点菜单上写得靠前靠后没关系。

Q2. 嵌套循环连接的总成本主要取决于下列哪一项?

单选题 · easy

  • A.磁盘剩余空间的大小
  • B.外层驱动表行数多少← 正确答案
  • C.数据库版本号的高低
  • D.连接字段的字符编码

答案:B。嵌套循环中驱动表每一行都要去内层发起一次探测,所以外层行数就是探测次数,是总成本的主导因素。这也是“小表驱动大表”原则的数学根源。

Q3. MySQL 中查看某条 SELECT 语句执行计划的命令是?

单选题 · mid

  • A.SHOW PROCESSLIST
  • B.DESCRIBE TABLES
  • C.EXPLAIN SELECT← 正确答案
  • D.SHOW CREATE TABLE

答案:C。EXPLAIN 显示执行计划,包括连接顺序、访问路径和估算行数,是调优的“工序单”。SHOW PROCESSLIST 看当前会话,DESCRIBE 看表结构,SHOW CREATE TABLE 看建表语句,都不是查看计划。

Q4. 关于哈希连接(Hash Join),下列说法正确的是?

单选题 · mid

  • A.要求两个表先各自排序再合并
  • B.适合大表等值连接且内存较充足← 正确答案
  • C.只能用于行数少于十张的表
  • D.执行过程完全不占用任何内存

答案:B。哈希连接先把一边(通常较小)读入内存建桶,再逐行探测另一边,适合大表等值连接并需要一定内存。先排序再合并是排序合并连接的做法;完全不占内存的描述明显错误。

Q5. PostgreSQL 的 EXPLAIN 树形输出中,最先执行(最先被访问)的表位于?

单选题 · hard

  • A.文本输出的最顶部一行
  • B.文本输出的最底部一行← 正确答案
  • C.输出中带 Index Scan 的行
  • D.与表名的字母顺序一致

答案:B。PostgreSQL 的 EXPLAIN 从下往上读,最底层节点最先执行,先被访问的表在底部。如果沿用 MySQL 从上往下读的习惯,会把驱动表与被驱动表完全判反。

Q6. 下列哪些做法有助于优化多表 JOIN 查询?(多选)

多选题 · mid

  • A.定期 ANALYZE 刷新统计信息← 正确答案
  • B.确认连接字段建有索引← 正确答案
  • C.删除表上所有已有索引
  • D.让较小的表先参与连接← 正确答案

答案:ABD。刷新统计信息让优化器的行数估算准确,连接字段的索引让探测高效,小表先连接控制中间结果规模。删除索引会让嵌套循环退化成双层全表扫描,是最常见的反向操作。

Q7. 关于优化器的代价模型,下列说法正确的有?(多选)

多选题 · hard

  • A.代价通常包含 IO 与 CPU 开销← 正确答案
  • B.统计信息会直接影响行数估算← 正确答案
  • C.加 hint 后优化器不再算代价
  • D.内存占用也在代价考量之内← 正确答案

答案:ABD。代价模型综合 IO、CPU、内存三方面,统计信息是行数估算的输入。hint 只是给优化器加约束、缩小搜索空间,优化器依然会评估代价,所以 C 错误。

Q8. 哈希连接中构建哈希表(build)通常选择哪一侧?

单选题 · mid

  • A.行数较少的那一侧← 正确答案
  • B.行数较多的那一侧
  • C.书写顺序靠后的那一侧
  • D.统计信息缺失的那一侧

答案:A。build 阶段要把选中一侧读入内存建哈希表,选行数较少的一侧能少占内存、少读数据。相当于后厨先给量少的小篮食材分桶,台面更省空间。

Q9. n 张表做多表连接时,连接算子(JOIN 节点)的数量是?

单选题 · hard

  • A.n 张表就需要 n 个连接算子
  • B.n 张表就需要 n 减一个连接算子← 正确答案
  • C.n 张表就需要二的 n 次方个
  • D.n 张表就需要 n 的阶乘个

答案:B。每次连接把两个结果合成一个,n 张表最终合成一个结果,需要 n−1 次连接。n! 和 2^n 描述的是候选计划的排列方法与搜索空间,而不是算子个数,容易与本题混淆。

Q10. 阅读执行计划时,正确的做法包括?(多选)

多选题 · easy

  • A.先确认哪张表是驱动表← 正确答案
  • B.关注每层的估算行数← 正确答案
  • C.忽略访问路径的信息
  • D.对比顺序调整前后的计划← 正确答案

答案:ABD。阅读计划先看驱动表与访问路径,估算行数能暴露统计信息是否过期;调优后必须对比新旧计划验证收益。忽略访问路径等于不读工序单上的关键步骤。