每天用导航出门时,你输入目的地,导航会给出几条路线:走高速快但绕远,走小路近但堵车,走环线平稳但多等几个红绿灯。导航最终推荐的那条,是它结合实时路况、历史数据和红绿灯时长,算出的“预计时间最短”路线。数据库里的 SQL 优化器,干的就是完全一样的事——只不过它导航的是一座由数据表组成的城市,目标是把你的一条 SQL 查询,翻译成一条执行起来最省时省力的“数据搬运路线”,也就是执行计划。 这个“智能导航”厉害在哪?你写的 SQL 只是说“我要什么结果”,至于先查哪张表、走索引还是全表扫、两张大表怎么连,全由优化器自己拍板。它拍板的依据,是一堆“路况数据”——统计信息、直方图、代价模型。懂它,你写的 SQL 才真正算“被它温柔以待”;不懂它,你写完就祈祷,祈祷它别选那条看上去绕死人的烂路。
对 AI 从业者而言,理解优化器意味着你能解释“为什么同一条 SQL 换个环境就慢 100 倍”,能带着 trace 去定位问题、用 hint 精准止血,这是数据工程和算法落地中极稀缺的调优能力。
可视化演示
展示全表扫描/索引扫描/Hint强制三种执行计划代价对比、连接顺序与驱动表选择,以及直方图统计对基数估算的影响。
本节配有交互动效(进入视口后自动加载,约 3KB)。
执行计划代价对比 全表扫描代价850 vs 索引扫描代价120 优化器选择代价最低的索引扫描计划 Cost = IO_cost + CPU_cost 基于代价优化器从多个计划中选择成本最低的执行方案
执行计划
你在导航里输入“公司”,导航先做的不是踩油门,而是规划:出小区左转、上高架、第三个出口下、右转进园区。这一串“先走哪、再走哪”的路线描述,就是“执行计划”。数据库收到一条 SQL 时也一样,它会先给数据画出搬运路线,再真正开始读表。这条路线的形状,直接决定了这条 SQL 是 10 毫秒还是 10 秒。
从 SQL 变成执行计划,要经历四步:语法分析先检查句子有没有拼写错误,语义分析确认表和列真的存在,逻辑优化把 SQL 改写成更聪明的等价形式(比如把子查询提前去重),物理优化才真正决定扫描方式、连接顺序。物理优化这一步的产出,就是我们眼里看到的执行计划。在 MySQL 里用 EXPLAIN、在 PostgreSQL 里用 EXPLAIN ANALYZE,就能把这条“导航路线图”打印出来。
执行计划里最基础的分叉是“怎么读一张表”。全表扫描就像一栋没有住户名单的大楼,快递员只能一层层敲门确认谁在家,读多少块就付出多少 IO;索引扫描则像楼下的物业名单,快递员先查名单锁定目标楼层,再去指定房间敲门。一般情况下,过滤比例越小,索引越划算;过滤比例接近 100% 时,全表扫描反而更省事。
执行计划虽是“纸上谈兵”,但它决定了真刀真枪时的路径。就像导航画出的路线图,路没走,胜负已分。
- $R_{rows}$ 总行数统计信息里记录的表总行数,是估算的地基。
- $\mathrm{selectivity}$ 过滤选择性WHERE 条件筛掉多少数据,取值在 0 到 1 之间,1 表示全命中。
- $R_{estimated}$ 估算命中行数两者相乘,得到这条过滤条件下预计要访问的行数,它是后续代价计算的第一块砖。
代价模型
导航 APP 给你报“预计 25 分钟”,这 25 分钟不是简单量距离,而是红绿灯数量、限速、拥堵系数、收费站的综合加权。数据库优化器同样会给每条候选路线打一个“预计用时”分——这个分数叫代价。代价越低,计划越优。
代价从哪来?主要三块:磁盘 IO 代价(读多少数据块)、CPU 代价(处理多少行)、内存/网络代价(排序与传输)。数据库为每类操作预设权重因子:在绝大多数数据库里,一次磁盘 IO 的权重远高于处理一行的 CPU 成本。优化器把所有候选计划的代价都算出来,然后挑数值最小的那个,这就是大名鼎鼎的“基于代价的优化”(CBO)。
这个模型还解释了一个反直觉现象:SQL 写得很“短”不代表执行快。优化器不看你的句子长短,只看代价数字。一条写得很长的 SQL,如果统计信息准确、命中索引,可能比一条“看上去精炼”但触发全表扫描的 SQL 快几十倍。就像绕远的高速可能比穿城的小路更快,导航关心的是时间,不是里程。
优化器就像一个精算师,把每条路的所有成本项摊开、加权、求和,然后淡定地选数字最小的那条。
- $\mathrm{IO\_cost}$ 磁盘 IO 代价按需读取的数据块数量,块越多代价越高。
- $\mathrm{CPU\_cost}$ 处理代价对读出来的行做过滤、比较、连接运算的开销。
- 权重因子 $w_{io}$ 与 $w_{cpu}$数据库根据硬件特征设定的调节系数,通常 IO 权重远大于 CPU,体现“读盘是最贵的动作”。
统计信息
导航的预估时间准不准,取决于路况数据新不新——三个月前的地图,永远不知道你家门口新修的单行道。优化器做代价估算所依赖的“地图数据”,就是统计信息。数据库定期用 ANALYZE 命令给表做“体检”,记录下三件大事:表里有多少行(行数统计)、每列有多少个不同值(列基数 NDV)、数据块分布情况。
列基数是估算的神器。假设 orders 表有 50 万行,status 列有 3 种取值,那么平均每个值大概占 16.7 万行。索引回表成本、连接顺序选择,全都建立在这些估算之上。为了保持新鲜,需要定期收集:MySQL 的 ANALYZE TABLE、PostgreSQL 的 ANALYZE、Oracle 的 DBMS_STATS 都是干这活的。
统计信息一旦过期,就是“拿着三个月前的地图跑今天的路”。最常见的情景:表从 10 万行暴涨到 1000 万行,统计信息没刷新,优化器还按 10 万行估算,认为全表扫描很便宜,结果一条点查被扫了千万行,慢得离谱。这也是 SQL 调优里最常被忽略的第一嫌疑犯。
路况数据越新鲜,导航越值得信赖;统计信息越准确,优化器的每次判断才站得住脚。
WHERE col = 'x' 的估算命中行数:假设每种取值平均出现,用总行数除以不同值个数。- NDV 列不同值个数这一列究竟有多少种取值,统计信息的核心指标。
- $\frac{1}{NDV}$ 平均选择率没有任何额外证据时,优化器假设每个值出现的概率相等。
- $\hat{r}$ 估算行数总行数与选择率相乘,得到这个等值条件预计命中的行数。
连接顺序
两栋楼都要送货,先到 A 栋还是先到 B 栋?这个先后顺序,在 SQL 里就是连接顺序。被导航先带到的那张表,叫驱动表(外层表)。顺序不同,搬运的数据量天差地别:先开车去一栋只有 10 户的小楼,再逐户去另一栋 1000 户的大楼匹配,和反过来做是完全不同的工作量。
最常见的连接方式是嵌套循环(Nested Loop)。它像快递员拿着 A 栋每个住户的名单,跑去 B 栋逐户核对:外层每一行,都要到内层找匹配。所以它的代价大约是“外层扫描一次 + 外层返回行数 × 每行去内层查找的代价”。内层越小,这个乘法越划算。另一种是哈希连接(Hash Join),先给一张表的连接列建一本地毯式的“索引簿”(哈希表),另一张表的每一行只需翻簿子匹配,适合两张大表等值连接。
那么谁来当驱动表?不是 SQL 里写在 FROM 后面的第一张表,而是优化器看统计信息后钦定的“过滤后行数最少”的表。小表驱动大表能让内层索引被反复命中缓存,减少随机 IO。别自己脑补顺序,交给优化器——除非你亲眼看到它选错了。
导航先到哪个地标,从来不看你先提哪个地名,只看怎么走总时间最短。连接顺序是优化器最得意的算术题之一。
- $C_{outer}$ 外层扫描代价读驱动表并且做完过滤的总代价。
- $N_{outer}$ 外层返回行数外层表经过 WHERE 条件后真正留下的行数,它放大内层开销。
- $C_{inner\_per\_row}$ 内层单次匹配代价每一行去内层表查一次匹配的成本,走索引会显著变小。
- 总代价合成外层扫一遍,内层被访问 N 次,总代价随之放大。
Hint调优
导航推荐走高架,但老司机一拍大腿:“走滨江路,这个点我知道不堵!”SQL 里的 Hint(提示)就是这条“老司机指令”,它写在注释里,强制优化器按指定的路线走。调用格式像是给 SQL 贴了张便条:/*+ INDEX(orders idx_status) */ 表示强制走某个索引,/*+ LEADING(a b) */ 表示强制 a 先作为驱动表。
Hint 适合什么时候用?统计信息怎么刷新都不见效、优化器在某个版本里反复抽风、线上正在火急火燎等不到下一次 ANALYZE 的时候。它是“急救止血带”,不是日常保健品。大部分数据库(Oracle / MySQL 8.0 的部分写法 / PostgreSQL 的 pg_hint_plan 插件)都给了你这条“手动接管方向盘”的通路。
但强制路线有个大坑:数据是会成长的。今天这条滨江路畅通是因为车少,半年后车流翻倍,固定走它就会堵死在桥上。Hint 把计划固定死,等于在数据变化面前失去了自适应能力。而且 Hint 越多,系统越难维护——每个 DBA 都要猜你当年为什么这么写。所以业界共识是:能用统计信息解决的,别上 Hint。
老司机也有失手的时候:强制走滨江路一旦遇上临时管制,连换路的余地都没有。Hint 可以是杀手锏,但别让它成为默认姿势。
- $\Pi_{\text{hint}}$ 受约束的计划集合被 Hint 过滤后的合法计划子集,只有走指定索引/顺序的计划才在里面。
- $\arg\min$ 最小化运算在给定集合里找出让代价函数取值最小的那个计划。
- $P^*$ 最终选定计划最优计划的记号,它是优化器在约束下能交出的最好答卷。
直方图统计
路口流量统计说“平均每小时 100 辆车”,可实际上早高峰 800 辆、深夜 0 辆。平均数骗了所有人。列的数据分布也一样,如果没有直方图,优化器只知道“有 3 种取值”,它会天真地认为每种各占三分之一。可现实中 99% 的行都是“已完成”状态,只有 1% 是“待支付”。于是执行 WHERE status = '待支付' 时,优化器按 16.7 万行估算,实际只有 5000 行——基数估算翻了几十倍,计划自然选歪。
直方图就是数据库给数据分布画的“流量热力图”。它把列的值域切成一个个桶(bucket),记录每个桶里有多少行。两种主流画法:频率直方图,每个频繁值独占一个桶,适合不同值个数(NDV)较小的列;高度均衡直方图,让每个桶装下大致相同的行数,适合 NDV 很大、数据分散的列。
有了直方图,优化器就不再迷信“平均值”了。它会看目标值落在哪几个桶里:落在高桶,说明这个值很常见,估算大;落在矮桶,说明罕见,估算小。这就是为什么大厂调优规范里总会有一条:对大表里倾斜严重的查询列,强制收集直方图。
流量热力图让导航知道哪条路高峰会堵,直方图让优化器知道哪些值是数据里的“早晚高峰”。
- $B$ 总桶数直方图把值域切分成多少个桶,桶越多粒度越细。
- $k$ 跨越桶数目标值在直方图里覆盖的桶的数量,覆盖越多说明该值出现越频繁。
- $\mathrm{sel}_{HBH}$ 修正后的选择率桶数占比代替了"1/NDV"的平均分配,从而修正倾斜数据下的估算偏差。
优化器开关
导航 APP 的偏好设置里,你可以勾选“不走高速”“避免拥堵”“优先快速路”。数据库优化器同样有一排“偏好开关”,它们是影响选路策略的系统参数。最常见的两个方向:ALL_ROWS 与 FIRST_ROWS。
ALL_ROWS 模式追求的是“跑完全部结果总的代价最小”,适合报表、批处理;FIRST_ROWS 模式追求“先返回前几行要快”,适合网页分页——用户等不了整张报表,但能接受开头几行秒出。另外还有 optimizer_dynamic_sampling 动态采样:当表没有统计信息或信息太旧时,数据库现场抽几页数据“临时测路况”,用样本估算基数。自适应计划则允许执行到一半发现路况不对,临时换个连接方式,像一个会根据实时车流变道的导航。
开关不是越大越好。动态采样虽然有奇效,但它本身也要花时间;采样页数开得太大,每次查询都先付一笔“测路费”,得不偿失。调这些开关的正确姿势是:先搞清楚业务要的是“首屏快”还是“总量稳”,再去翻参数文档,最后用 A/B 对比验证,而不是听别人说哪个参数好就无脑开。
导航偏好设错,明明想躲拥堵,却总被拉上高架。优化器开关没有银弹,只有跟业务目标对齐,才算真正“对味”。
- $r_{sample}$ 采样命中行数随机抽的少数数据页里,满足条件的行数。
- $\frac{P_{total}}{P_{sampled}}$ 页数放大比例全表总数据页数除以采样页数,是把局部结果外推到全局的倍数。
- $\hat{r}_{dyn}$ 动态采样估算行数采样结果乘以放大比例,得到对全表命中行数的估计。
Trace排查
老司机走错路后最值钱的动作是复盘:打开导航记录,看它当时说滨江路 20 分钟,为什么实际开了 45?数据库的 Trace 就是导航的“行程回放”,它把优化器选路时看过的每一个数字、每一次比较、每一个候选计划的代价都记在日志里。
不同数据库叫法不同:MySQL 有 optimizer_trace 专用跟踪,PostgreSQL 用 EXPLAIN (ANALYZE, BUFFERS) 看真实执行细节,Oracle 有 10053 事件和 DBMS_XPLAN。读 trace 时要抓三件事:第一,统计信息对不对(比如行数是不是差了两个数量级);第二,基数估了多少(是不是因为没直方图把倾斜数据当均匀分布);第三,哪一项权重把计划带偏了(IO 代价还是 CPU 代价)。
调优是一条闭环:发现慢 SQL → 开 trace 看代价明细 → 定位基数偏差 → 刷新统计信息 / 加直方图 / 必要时上 Hint → 复测并对比替代计划的代价差。每一次复盘都在给下一次“选路”积累经验。这也是 AI 时代做数据库自治优化的基础——机器读 trace、找规律、自动调参。
导航回放是司机的复盘神器,Trace 是 DBA 的破案现场。把每次“选错路”的原因记进日志、汇成规律,数据库调优从此不再靠玄学。
- $C_{P_1}$ 当前计划代价优化器现在选的计划的估算代价。
- $C_{P_2}$ 替代计划代价另一个连接顺序或扫描方式下的估算代价。
- $\Delta C$ 代价差二者相减,判断是否值得换路,差越大,越值得改。
AI 应用场景
慢 SQL 智能诊断平台
阶段:异常检测与根因分析
解决的问题:传统慢 SQL 排查靠资深 DBA 手工看执行计划、猜统计信息是否过期,平均一次定位要半小时以上。AI 平台自动收集所有慢查询的 trace 与统计信息快照,用规则模型加机器学习识别“基数偏差过大”“驱动表选反”等典型病灶,把定位时间压缩到秒级。
具体案例:某电商大促期间,SELECT ... WHERE order_status='待支付' 从 50ms 飙升到 8 秒,AI 诊断平台对比直方图后,发现该列数据从 1% 倾斜恶化到 0.1%,统计信息已过期一周,自动给出“立即刷新 + 构建高度均衡直方图”的建议,执行后查询回落到 80ms。
数据库自治调优助手
阶段:自动化决策与闭环优化
解决的问题:传统 DBA 调参靠经验试错,参数组合爆炸式增长。AI 助手把优化器开关、统计信息收集策略、动态采样阈值建模为多臂老虎机问题,根据线上查询延迟反馈自动调节开关组合,形成“感知—决策—验证—复盘”的闭环。
具体案例:一家金融公司把 2000 条批处理 SQL 接入自治调优助手,系统通过 Trace 数据分析发现 40% 的慢查询卡在 FIRST_ROWS 模式下,自动将批处理会话切换为 ALL_ROWS,并将 15 个关键连接列的采样频率提高,整体批处理时长下降 57%。
云数据仓库查询改写引擎
阶段:机器学习推理与成本预测
解决的问题:云厂商的数据仓库每天接收海量分析 SQL,人工优化不现实。利用学习型优化器,把“统计信息 + 历史执行代价”喂给模型,预测哪些计划会超时,自动改写连接顺序或注入 hint,让查询更聪明地利用资源。
具体案例:某数据分析平台使用学习型优化器后,对 1 万条历史慢查询建模,模型识别出“大表等值连接未走哈希连接”的常见病,自动重写 SQL 并注入 HASH_JOIN 提示,P95 查询延迟从 12 秒降至 3.5 秒。
Python 演示
# -*- coding: utf-8 -*-"""MiniCBO: 用统计信息估算代价并挑选连接计划"""import math class TableStat: def __init__(self, name, rows, blocks, ndv): self.name = name self.rows = rows self.blocks = blocks self.ndv = ndv def cost_full_scan(t): """全表扫描代价: 读所有块 + 逐行过滤的CPU开销""" return t.blocks * 1.2 + t.rows * 0.05 def cost_index_filter(t): """索引过滤代价: 树深常数 + 回表行数 + 日志因子""" return 3.0 + (t.rows / t.ndv) * 0.3 + math.log(t.rows + 1) def cost_nlj(outer, inner): """嵌套循环连接代价 = 外层扫描 + 外层行数 x 内层单次代价""" return cost_full_scan(outer) + outer.rows * cost_index_filter(inner) def cost_hash_join(outer, inner): """哈希连接代价: 构建哈希表 + 两表各扫一遍""" return outer.blocks + inner.blocks + outer.rows * 0.08 + inner.rows * 0.08 def pick_plan(t1, t2): """枚举两种连接顺序和两种连接方式, 返回代价最小计划""" best = None for outer, inner in ((t1, t2), (t2, t1)): candidates = [ ("NESTED LOOP 驱动=" + outer.name, cost_nlj(outer, inner)), ("HASH JOIN 驱动=" + outer.name, cost_hash_join(outer, inner)), ] for pname, pcost in candidates: if best is None or pcost < best[1]: best = (pname, pcost) return best def main(): users = TableStat("users", 10000, 400, 2000) orders = TableStat("orders", 500000, 15000, 30000) name, cost = pick_plan(users, orders) print("最优计划:", name) print("估算代价:", round(cost, 2)) print("orders 索引过滤代价:", round(cost_index_filter(orders), 2)) print("orders 全表扫描代价:", round(cost_full_scan(orders), 2)) if __name__ == "__main__": main()
class TableStat:定义一张表的统计信息:行数、数据块数、列基数 NDV,相当于数据库表上的“体检报告”。def cost_full_scan(t):全表扫描代价:读所有块乘以 IO 系数,加上逐行过滤的 CPU 开销。def cost_index_filter(t):索引过滤代价:把“回表行数 = 总行数 / NDV”带入公式,体现选择性越好越便宜。def cost_nlj(outer, inner):嵌套循环代价:外层扫一次,但外层行数会“放大”内层的单次查找成本。def pick_plan(t1, t2):枚举两表两种连接顺序、两种连接方式共 4 个候选,挑最小代价,模拟优化器的核心过程。if __name__ == "__main__":程序入口,用小表 users 和大表 orders 跑一遍,输出最优计划。
常见误区
误区“我只要把所有常用查询列都加上索引,数据库读数据就永远快。索引反正不碍事,多加几个保险。”
正解索引像导航里的“快捷方式图层”,每多一个不是免费的——写入时每个索引都要同步更新,占用磁盘和内存,还会让优化器在选路时徒增无数候选计划。更关键的是,如果列的选择性很差(比如状态列 99% 都是同一个值),走索引反而要回表几万次,不如全表扫描快。索引是手术刀,不是防弹衣。
为何常见:初学者看到“索引加速查询”的结论后,容易线性外推成“越多越好”,忽略了写入成本和选择性这两个关键变量。
误区“同样是查订单,别人写 3 行,我写 8 行,我的肯定更慢。把子查询删掉、把 JOIN 改紧凑点,速度应该能翻倍。”
正解执行速度由优化器选出的执行计划决定,而计划由统计信息、代价模型、连接顺序决定,跟 SQL 文本的篇幅没有直接关系。一个用词简洁但触发全表扫描的 SQL,通常打不过“啰嗦但命中索引”的写法。真正影响性能的是数据分布、索引设计、统计信息新鲜度。
为何常见:人用“最短路径”的直觉理解查询,把代码的文本长度和执行消耗画了等号,这是把“可读性”和“执行代价”两个维度搞混了。
误区“这张表虽然只有 200 行,但既然建了索引,就该强制让它用索引,否则索引不就白建了吗?”
正解小表走全表扫描往往更快,因为全表扫描是顺序读,一次预读就能带走几百行;而走索引要先查 B+ 树再回表,多了两三次随机 IO,反而更慢。优化器不选索引不是“傻”,是代价模型算得明明白白。强行用 Hint 逼它走索引,常常把 0.1ms 的查询拖成 1ms。
为何常见:人们把“索引”神化成万能加速器,忽略了顺序读和随机读的硬件差异,也低估了优化器对微小代价差的敏感度。
误区“上周刚跑过 ANALYZE,统计信息肯定准得很,查询还慢肯定是 SQL 写得有问题,跟数据没关系。”
正解统计信息是某一时刻的快照。表数据如果持续增删改,尤其是大表出现戏剧性增长或倾斜加剧,这个快照会迅速过期。过期统计信息让优化器用错误的行数估算代价,选出的计划常常荒谬到令人咋舌。每次排查慢 SQL,第一步都该先确认统计信息的新鲜度,而不是急着改 SQL。
为何常见:收集统计信息给人“一劳永逸”的安全感,忽略了数据库是动态系统;加上调优时人们更爱改可见的 SQL,而不是去检查隐藏的元数据。
误区“我在 SQL 里把用户表写在最前面,那驱动表一定是它。我只要调整 FROM 的顺序,就能控制连接顺序。”
正解除非你使用 Hint 强制,否则驱动表由优化器根据统计信息和代价模型决定,跟 SQL 书写的表顺序没有必然关系。你就算把大表写在最前面,优化器发现小表过滤后行数更少,照样让小表做驱动。想确认真实的驱动表,用 EXPLAIN 看执行计划,而不是盯着 SQL 原文猜。
为何常见:人们习惯用“先出现的先执行”的直觉理解声明式语言 SQL,而 SQL 恰恰是“声明要什么、不声明怎么做”,执行细节全在优化器手里。
面试题
Q1. 优化器选择执行计划的首要依据是什么?
- A.计划书写的篇幅最短
- B.估算出的代价最低← 正确答案
- C.FROM 表出现顺序
- D.索引数量的总和
答案:B。优化器的核心目标是最小化代价模型算出的总代价,而不是计划文本的长短、表出现的顺序或索引数量。就像导航选预计时间最短的路线,而不是距离最短或路名最好听的路线。
Q2. 一张表只有几百行,查询没有过滤条件,通常最优的执行方式是?
- A.全表扫描省去索引开销← 正确答案
- B.索引扫描一定最快
- C.只看行数做决策
- D.关闭优化器防误判
答案:A。小表全表扫描是顺序读,代价极低;走索引反而增加随机 IO。优化器算过账后通常选全表扫描,强行用 Hint 走索引只会更慢。这体现了“代价模型说了算”而非“索引等于快”。
Q3. 嵌套循环连接中,驱动表的选择原则是?
- A.过滤后行数少者驱动← 正确答案
- B.过滤后行数多者驱动
- C.总行数最多者驱动
- D.由开发者随意指定
答案:A。驱动表的外层行数会放大内层的查找代价(C = 外层 + N_outer × 内层单次代价),所以应该让过滤后返回行数最少的表做驱动。总行数多但过滤后剩很少的表,反而是优质驱动表。
Q4. 以下哪项属于数据库优化器依赖的统计信息?
- A.表行数与列基数← 正确答案
- B.表上的注释文本
- C.应用内存大小
- D.浏览器类型分布
答案:A。统计信息记录的是表行数、数据块数、列 NDV 等数据分布特征。注释文本、应用内存、浏览器信息都不在数据库的视野内。行数和 NDV 是基数估算的两大支柱。
Q5. 某列数据严重倾斜但未收集直方图,等值条件估算行数时优化器默认怎么做?
- A.按 1/NDV 平均分摊← 正确答案
- B.按最大值整列命中
- C.全部行都被命中
- D.固定只返回一行
答案:A。没有直方图时,优化器只知道 NDV,默认每个取值平均出现,用总行数除以 NDV。这在倾斜数据下会严重失真,直方图就是用来修正这种“平均主义幻觉”的。
Q6. 以下哪些手段能够影响优化器的决策?
- A.刷新统计信息← 正确答案
- B.添加索引提示← 正确答案
- C.调整优化模式← 正确答案
- D.插入空行缩进
答案:ABC。统计信息改变基数估算,Hint 强制缩小计划集合,优化器开关改变代价偏好,三者都能实质改变选路结果。而 SQL 里的空行、缩进、多余空格只影响可读性,不影响执行计划。
Q7. 嵌套循环连接中,外层返回 1000 行,内层每次匹配代价 5,外层扫描代价 100,则总代价最接近?
- A.5100← 正确答案
- B.10100
- C.1500
- D.6100
答案:A。代入公式 C = C_outer + N_outer × C_inner = 100 + 1000 × 5 = 5100。要让总代价下降,最有效的方法是减少外层返回行数,或降低内层单次匹配代价(比如加索引)。
Q8. 关于 Hint 的说法正确的是?
- A.数据变化后固定计划可能变差← 正确答案
- B.Hint 可在各数据库通用
- C.它是紧急情况的后手← 正确答案
- D.Hint 让统计信息失效
答案:AC。Hint 把计划固定后,数据增长会让原本的最优变成灾难,所以它适合急救而非日常。Hint 语法是数据库私有的,并不通用;且加 Hint 后优化器仍会使用统计信息计算代价,只是把候选集合缩小了,因此 D 错误。
Q9. 高度均衡直方图中,目标值跨越 k 个桶、总桶数为 B,它的选择率是?
- A.k 除以 B← 正确答案
- B.B 除以 k
- C.k 乘以 B
- D.k 加上 B
答案:A。高度均衡直方图把行数大致均匀装进 B 个桶,目标值覆盖的桶数 k 占总桶数的比例就是选择率 k/B。覆盖桶越多,说明该值越常见,估算命中行数越多。
Q10. 通过 Trace 排查慢 SQL 时,通常能定位到哪些问题?
- A.统计信息过期导致的行数偏差← 正确答案
- B.基数误判引发的连接顺序错误← 正确答案
- C.动态采样调参方向的提示← 正确答案
- D.SQL 中多余空格的位置
答案:ABC。Trace 完整记录优化器的输入与计算过程,能看到它读取的行数、估算的基数、最终选择的代价。由此可定位统计信息过期、基数误差导致驱动表选反,以及动态采样力度是否不足。而空格属于词法层信息,不进入优化器的决策日志。