职场新人小林第一天上班,老板递给他一厚沓订单,说:“帮我整理出华东大客户的清单。”小林有两种做法:一种是先跑去档案室,把华东客户名单一次性抄回来,再对照订单逐条标注——跑腿一趟搞定;另一种是老板每看一张订单,他就跑去档案室查一次“这个客户是不是华东的”——订单有多少张,他就得跑多少趟。前者对应独立子查询,后者对应相关子查询。幸运的是,办公室里还有一位精明的办公室主任(MySQL优化器),他会偷偷把小林反复跑腿的活儿改造成更高效的方式,甚至让林子和老板同桌办公,这就是半连接(Semi-Join)优化。本文用“老板与助理”的职场故事,带你彻底搞懂子查询的执行成本、IN与EXISTS的取舍、半连接原理与JOIN改写技巧,学会写出高效又清晰的SQL。
掌握子查询优化,是后端工程师从“能写SQL”进阶到“能写快SQL”的关键门槛。它直接影响线上接口的响应速度、数据库负载和架构稳定性,是面试与实战中的高频考点。
目录
可视化演示
展示正态分布、均匀分布和指数分布的概率密度函数曲线,动态演示参数变化对分布形状的影响,含随机采样散点动画。
本节配有交互动效(进入视口后自动加载,约 3KB)。
正态分布 N(μ,σ²) 钟形曲线,μ决定中心位置,σ决定曲线宽度和高度 约68%数据落在μ±σ范围内,95%落在μ±2σ范围 f(x) = (1/σ√2π) · e^(-(x-μ)²/2σ²)
子查询基础:助理的两种汇报模式
老板要一份华东大客户的订单清单,助理小林有两种办事方式:一种是先跑去档案室,把华东客户名单一次性抄回来,再对照订单逐条审批——跑腿只花一趟;另一种是老板手里攥着订单,每看一行就问一句“这客户是华东的吗?”,小林就得跑去档案室查一次,再跑回来汇报——订单有多少行,他就要跑多少趟。同样是子查询,执行方式的天壤之别从此开始。
SQL里这两种方式对应两类子查询。独立子查询(Uncorrelated Subquery)不引用外层任何列,像“抄名单”,数据库先执行它一次,把结果缓存起来供外层使用;相关子查询(Correlated Subquery)引用外层表的列,外层每一行都要触发一次内层执行,跑腿次数被外层行数成倍放大。理解两者的执行顺序差异,是判断一条查询“快不快”的直觉起点。
假设外层订单表有 M 行,内层客户表有 N 行。独立子查询的成本约等于“一次物化 + M 次哈希探测”;相关子查询在最坏情况下要执行 M 次内层扫描,每次扫描 N 行,总成本接近 M×N 行的读取量。这就是为什么大表上的相关子查询会突然拖垮数据库——它不是慢了一点,而是直接把执行次数放大了一个数量级。
1. C_once:独立子查询只执行一次内层查询的固定成本,对应“抄一次名单”
2. M × C_probe:外层 M 行每行在子查询结果集合中做索引或哈希探测,单次探测很快
3. M × C_scan:相关子查询对外层 M 行各执行一次完整内层扫描,单次成本远高于探测
4. 两式对比:相关子查询的劣势来自“执行次数”而非单次速度,当 M 远大于 N 时差距更悬殊
聪明的老板自然会让小林“一次抄全名单”。但现实中有些需求天然需要逐行确认,这时就要靠后面的办公室主任(优化器)和同桌办公(JOIN)来拯救了。理解两种子查询,是走上查询优化之路的第一步。
IN与EXISTS:清单汇报与点头确认
还是那份华东客户名单,这次小林换了两套汇报方案。IN 方案是把整个华东客户档案全部复印一份放在老板桌上,老板自己拿订单逐一比对;EXISTS 方案是老板每念一个客户名,小林只回答“在”或“不在”,不用复印任何文件。前者是搬运整箱资料,后者是逐条口头确认,体力消耗完全不同。
SQL中 IN 子查询先把内层结果集整体取出,与外层值做集合成员判断;EXISTS 则对外层每一行做存在性检测,只要内层找到一条匹配就立刻返回真。很多老资料说“EXISTS 比 IN 快”,其实在 MySQL 5.7 之后,优化器会把大多数等值 IN 也改写成半连接,两者的执行计划常常一模一样。真正的性能差异,来自数据量的分布和索引的利用。
真正影响性能的是谁的数据量小:当内层结果集很小、外层很大时,EXISTS 的“找到就停”能显著减少扫描;当内层结果集很大时,IN 一次性物化后建哈希表反而更高效。核心口诀是“小表驱动大表”——让数据量小的表先执行、当驱动,再让大表的每一行去探测小表的结果,从而把对比次数压到最低。
1. 集合 S:子查询返回的结果集,例如华东客户 id 的集合
2. ∃ 存在量词:表示“至少能找到一条匹配”,正好对应 EXISTS 的语义
3. s = R.A:匹配条件为 s 等于外层行字段 A,即关联列的等值比较
4. R.A ∈ S:字段值属于集合 S,正是 IN 操作符的集合成员语义
5. ⟺ 等价符号:说明 IN 与 EXISTS 可以互相改写,差异只在优化器如何执行
老板问“在不在”(EXISTS)和翻名单(IN)得到的结果完全一样,但消耗的体力不同。如果名单就三页纸,翻一眼更快;如果老板要核对一万个客户,直接把名单做成速查卡(物化 IN)更省力。关键看哪边数据量小,谁就驱动。
半连接优化:主任的去重卡片魔法
办公室主任看不下去小林的跑腿方式了。他心想:与其让老板每问一个客户,小林就去档案室翻半天,不如提前把华东客户名单抄成一张去重卡片——同一个客户只记一次——放在老板手边。老板随时问起,翻卡片秒回。这就是半连接优化的雏形卡片,也是 MySQL 优化器最拿手的魔法之一。
半连接(Semi-Join)是优化器处理 IN/EXISTS 子查询的三大策略之一。它先把子查询结果物化(Materialize)成一张去重的临时表,然后让外层表的每一行去临时表里探测:有匹配就保留外层行,没有就丢弃,并且绝不返回内层表的任何列。这正好对应子查询“只关心是否存在”的语义,多返回一列都是浪费。
关系代数用符号 ⋉ 定义半连接:R ⋉ S 等于 R 与 S 做等值连接后,只保留 R 的列。因为普通连接时内层重复值会让 R 的同一行出现多次,所以“物化阶段去重”是半连接高效的关键。MySQL 通过 optimizer_switch 中的 semijoin=on、materialization=on 开关控制这类转换,触发前提通常是等值关联且内层无聚合、无 LIMIT。
1. R ⋉ S:半连接运算符号,结果只包含 R 中能与 S 匹配的行
2. π_{R.*}:投影运算,保留 R 表的全部列,丢弃 S 表的全部列
3. R ⋈ S:等值连接,按 k 列值相等把两表行拼接在一起
4. 整体含义:半连接 = 连接后再投影,天然没有重复行
主任的去重卡片一石二鸟:物化只跑一次档案室,不用老板反复问;卡片上去重后每个客户只出现一次,老板绝不会因为同一客户被重复审批而困惑。这正是半连接在数据库内核里做的事——用一张小卡片,化解千万次跑腿。
JOIN改写:让助理坐上办公桌
主任又有了新主意:与其老板问、小林答,不如让小林直接坐到老板对面,两人各拿一份表,同步念出客户编号,编号相同的订单和客户就配对。这就是把子查询改写成 JOIN——从“来回跑腿”变成“同桌办公”。办公效率瞬间提升,但办公规则也要重新约定。
示例中,SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE region='华东') 可以改写为 SELECT DISTINCT o.* FROM orders o JOIN customers c ON o.customer_id = c.id WHERE c.region='华东'。JOIN 让优化器直接选择嵌套循环或哈希连接,省去子查询逐行启动的开销。IN 表达“属于集合”,JOIN 表达“按条件配对”,两种思维方式在等值场景下可以互换。
改写有个关键坑:如果内层表在关联列上存在重复值,或者一对多关系导致同一订单被匹配多次,JOIN 会产生重复结果。这违背了子查询“只看是否存在”的语义。因此改写时通常需要保留 DISTINCT 或 GROUP BY。只有内表键唯一时,JOIN 才与 IN 完全等价;否则必须手动去重。
1. R ⋈ S 连接:普通连接会因一个 R 行匹配多个 S 行而放大结果行数
2. DISTINCT(……):SQL 的去重操作符,消除连接产生的重复左表行
3. ≡ 等价符号:说明“去重后的连接”与“半连接”在任意数据集上结果相同
4. R ⋉ S 半连接:天然去重、不返回内层列,是衡量 JOIN 改写正确性的标准
同桌办公高效,但也要防“同一笔订单被两个同名客户档案各配一次”的乌龙——DISTINCT 就是主任在流程单上盖的“仅保留一项”章。办公方式变了,审批结论必须和原来完全一致,这是所有改写的底线。
执行计划:看懂主任的工作报告
每次查询开工前,主任都会提交一份工作安排表(EXPLAIN),写明:谁先跑腿(驱动表)、用什么目录(索引)、预计翻多少本档案(rows)、子查询是独立完成还是合并办公(select_type)。老板不用猜,一行行读下来就能看出问题出在哪。
EXPLAIN 输出的关键字段包括:select_type(SIMPLE / SUBQUERY / MATERIALIZED 等)、type(ALL 全表扫描 / range 范围 / ref 非唯一索引 / eq_ref 主键等值)、key(实际使用的索引)、rows(预计扫描行数)。当 select_type 出现 SUBQUERY 或 MATERIALIZED,说明子查询被单独物化了一次;当 type 为 ALL,说明存在全表扫描。
rows 是优化器对扫描行数的估算,它决定了嵌套循环的放大倍数。当 rows 异常放大——例如 1000 万 ≈ 外层 1 万 × 内层 1000——说明优化器选择了逐行驱动的计划,子查询未能被半连接化。此时应检查关联列类型是否一致、统计信息是否过期、optimizer_switch 的 semijoin 是否开启。优化的目标,就是让 rows 的总乘积尽量贴近实际返回行数。
1. N_table:优化器统计信息中的表总行数(基数)
2. σ 选择性:过滤条件保留的数据比例,取值在 0 到 1 之间
3. rows_est:预计扫描行数,数值越小代表该步骤的代价越低、计划越优
看到 rows 从千万级骤降到十万级,老板就知道主任重新排了班——助理不用再为每个订单跑档案室了。EXPLAIN 是数据库写给程序员的“述职报告”,读懂它,才能和优化器好好配合而不是互相猜忌。
索引与性能:给档案室装目录
档案室给每个客户编号做了目录卡(索引)。小林接到“查客户 57”的任务,先翻目录卡直接定位到第 57 号档案,而不是从 1 号柜子开始一本本翻。索引把“逐柜查找”变成“翻目录定位”,速度天差地别。在子查询的世界里,索引同样是加速探测的关键道具。
在子查询中,索引的作用体现在两个关联列上:内层子查询里的 id(通常是主键,天然有索引)和外层表用于探测的 customer_id(建议人工建索引)。有了索引,相关子查询每次探测从全表扫描 O(N) 降为 B+ 树查找 O(log N),EXISTS 和半连接的探测成本都大幅下降,整个查询的响应时间可能缩小几个数量级。
但索引不是万能的。如果一列只有两种取值(如性别),选择性极低,优化器宁可全表扫描也不走索引;如果关联列做了函数运算或隐式类型转换(如字符串列与数字比较),索引会直接失效。更高阶的覆盖索引(查询所需列全部在索引树中)还能省去回表,是子查询提速的进阶手段。
1. |D|:索引列去重后的不同键值个数,代表“目录里有多少个词条”
2. N:表的总行数,代表“档案室里总共有多少份档案”
3. Sel 选择性:越接近 1 区分度越高,索引高效;越接近 0 则索引几乎无用
目录卡能加速,但要是全档案室都叫“华东客户”,目录再精细也没意义。索引解决的是“找到匹配后还要翻很多本”的问题,而子查询真正的大敌是“每行都让助理跑一趟”——所以先改写、再建索引,顺序千万别反。
实战调优:后厨流程大改造
大促当天,数据库 CPU 告警,订单接口超时一片。DBA 翻出慢查询日志,发现元凶是一条订单查询:每读一行订单,就去客户表确认一次“是不是华东客户”。十万订单,等于让小林十万次往返档案室。问题一目了然,改造方案也呼之欲出。
定位慢查询的常规流程是:先用 mysqldumpslow 或 performance_schema 找出总耗时最高的 SQL;再用 EXPLAIN 观察 select_type、rows、type 等字段;若发现 SUBQUERY 且 rows 被放大,就尝试等价改写成 JOIN 或半连接友好写法,最后对比改写前后的执行计划。
实战改写对照:
改写后,EXPLAIN 中 select_type 从 SUBQUERY 变为 SIMPLE(或出现半连接标记),rows 从千万降到十万量级。衡量改进幅度,可以用改善率公式直观计算。
1. rows_before:改写前 EXPLAIN 中预计扫描的总行数,例如 1000 万
2. rows_after:改写后 EXPLAIN 中预计扫描的总行数,例如 10 万
3. 1 减比例:计算减少量占原来的百分比,乘 100% 化为直观的百分点
改完之后,小林终于坐到了老板对面(JOIN),十万订单 30 毫秒跑完。主任在季度总结里写道:最大的浪费不是档案多,而是让助理重复跑同一条路。优化子查询,本质上就是消灭重复跑腿。
-- 优化前:相关子查询,外层每行触发一次内层扫描SELECT o.id, o.amountFROM orders oWHERE EXISTS ( SELECT 1 FROM customers c WHERE c.id = o.customer_id AND c.region = '华东'); -- 优化后:JOIN 改写(customer_id 关联),用 DISTINCT 防重复SELECT DISTINCT o.id, o.amountFROM orders oJOIN customers c ON c.id = o.customer_idWHERE c.region = '华东';
最佳实践:高效团队的办事守则
经历几轮爆单磨炼,老板把主任和小林的经验总结成一本《办事守则》:能批量取回的资料绝不让助理逐行确认;能同桌办公就同桌办公;改了办公流程,必须用 EXPLAIN 报告验收效果。守则简洁,但每条都来自真实的事故与复盘。
工程上落地为三条原则。一是可读性优先:IN 表达 “属于集合” 的语义更直观,SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE region='华东') 一眼就能看懂业务意图;二是热点查询优先 JOIN:在高频路径上用可预期的执行计划换取性能稳定;三是改写后必须对比 EXPLAIN,用 rows 和 type 说话,不能凭感觉判断快慢。
改写保留的唯一底线是逻辑等价:新语句与原语句在任意数据集上返回相同的结果集。此外,不要频繁改动 optimizer_switch 的 semijoin / materialization 开关,让优化器保留自动改写的能力;MySQL 8.0+ 可以用 EXPLAIN ANALYZE 实测各阶段耗时,让每个优化决策都有数据支撑。
1. Q_original:改写前的原始子查询语句(IN / EXISTS 写法)
2. Q_rewritten:优化改写后的 JOIN 语句
3. ≡ 逻辑等价:两条语句在任意数据集上返回相同结果,是安全改写的数学前提
守则不是教条:如果老板今天只查一个订单,让小林跑一趟档案室(相关子查询)完全没问题,总成本反而最低。优化的本质是“根据场景选最省力的办事方式”,而不是把所有子查询都套上 JOIN 的模板。数据结构、数据量、执行频率,三者共同决定最优解。
AI 应用场景
慢SQL智能诊断平台
阶段:代码分析与性能优化
解决的问题:传统 DBA 靠经验在慢查询日志中人工定位低效子查询,效率低且易遗漏。AI 平台自动解析 SQL 语法树,识别“相关子查询 + 大外层表”的危险模式,结合 EXPLAIN 历史数据给出改写建议,将诊断时间从小时级缩短到秒级。
具体案例:某电商团队每天产生 2 万条慢查询记录,AI 诊断平台标记出其中 30% 来自嵌套子查询,自动区分“可物化的独立子查询”和“需改写的相关子查询”,推送诸如“将 EXISTS 改写为 JOIN 并加 DISTINCT”的建议。上线后接口平均响应时间从 800ms 降到 120ms,数据库负载显著下降。
AI辅助SQL改写工具
阶段:查询生成与等价重写
解决的问题:ORM 框架自动生成的多层子查询可读性差、性能不可控,人工改写成本高昂。AI 工具学习历史改写规则(IN→EXISTS、子查询→JOIN、语义保持),在 CI 阶段自动重写查询并用成本模型验证收益,防止低效 SQL 流入生产环境。
具体案例:某 SaaS 公司的研发流水线接入 AI-SQL 体检插件。插件将 WHERE id IN (SELECT ...) 且内外无关联的语句标注为“可物化”,把关联型子查询标注为“建议 JOIN”,并自动输出改写后的 SQL 与 EXPLAIN 对比。运行两个月,数据库 CPU 峰值降低 35%,SQL 评审效率提升一倍。
Python 演示
"""模拟SQL子查询优化:相关子查询 vs 半连接物化""" class SubqueryDemo: """用Python列表模拟orders与customers两张表,统计内层扫描行数""" def __init__(self, orders, customers): self.orders = orders # 订单表: [(order_id, customer_id), ...] self.customers = customers # 客户表: [(customer_id, region), ...] self.gc_count = 0 # 累计扫描customers表的行数 def correlated_in(self, region='华东'): """写法一:相关子查询,外层每行触发一次内层扫描""" result = [] for oid, cid in self.orders: for pcid, pregion in self.customers: self.gc_count += 1 if pcid == cid and pregion == region: result.append(oid) break return result def semi_join(self, region='华东'): """写法二:半连接物化,内层先执行一次并去重""" east_ids = set() for pcid, pregion in self.customers: self.gc_count += 1 if pregion == region: east_ids.add(pcid) result = [] for oid, cid in self.orders: if cid in east_ids: result.append(oid) return result def run_compare(self, region='华东'): """执行两种写法并打印性能对比""" self.gc_count = 0 r1 = self.correlated_in(region) cost1 = self.gc_count self.gc_count = 0 r2 = self.semi_join(region) cost2 = self.gc_count print('相关子查询结果:', r1[:8], '...') print('半连接物化结果:', r2[:8], '...') print('结果一致:', r1 == r2) print('customers扫描行数: 相关子查询=%d, 半连接=%d' % (cost1, cost2)) print('加速比: %.1fx' % (cost1 / cost2)) def main(): """生成1000条订单和100位客户,其中约1/4是华东客户""" orders = [(i, i % 100) for i in range(1000)] customers = [(i, '华东' if i % 4 == 0 else '华北') for i in range(100)] demo = SubqueryDemo(orders, customers) demo.run_compare() if __name__ == '__main__': main()
self.orders` 与 `self.customers模拟两张数据库表,外层订单、内层客户,对应正文中的 orders 与 customers。self.correlated_in()模拟相关子查询:外层每行都完整遍历一次内层表,对应WHERE EXISTS (...)。for oid, cid in self.orders:外层订单循环,相关子查询中每一行都会触发内层扫描。if pcid == cid and pregion == region:内层匹配条件,对应 SQL 的c.id = o.customer_id AND region='华东'。east_ids.add(pcid)半连接物化的核心:先执行内层并把结果放入集合自动去重。if cid in east_ids:哈希探测 O(1) 判断,对应优化器把子查询改为临时表探测后的匹配方式。r1 == r2验证改写前后结果集一致,呼应“逻辑等价”的改写底线。cost1 / cost2计算加速比,让优化收益一目了然。
常见误区
误区网上都说子查询性能差,我看到子查询就直接改写成 JOIN,这样总没错吧?反正 JOIN 听起来就比嵌套查询高级。
正解现代 MySQL 优化器会把绝大多数等值 IN/EXISTS 子查询自动改写成半连接或 JOIN,语法形式本身不决定快慢。真正决定性能的是执行计划:是否物化、驱动顺序、索引利用。盲目改写反而可能因遗漏 DISTINCT 而产出重复数据。判断依据永远是 EXPLAIN,而不是“哪种写法更高级”。
为何常见:很多优化文章写于 MySQL 5.5 时代,那时子查询确实普遍低效。结论被代代相传,逐渐变成了“子查询 = 慢”的刻板印象。
误区前辈说 EXISTS 比 IN 快,我以后把所有 IN 都改成 EXISTS,准没错,反正多写几个字母不亏。
正解在 MySQL 5.7+ 中,IN 和 EXISTS 走到最后常常是同一个半连接执行计划,不存在“永远更快”。当内层结果集小且无重复时,IN 先物化建哈希表反而可能更快;当外层小、内层大时,EXISTS 的“找到即停”才有优势。具体选谁,要看表的数据量和索引,用 EXPLAIN 验证。
为何常见:EXISTS 在旧版 MySQL 和部分数据库(如早期 Oracle)中确实有性能优势,这一结论被泛化到所有场景,忽略了优化器版本和表结构的差异。
误区既然有半连接优化,我就把低效子查询照常写出来,MySQL 自己会改好,不需要我动手优化。
正解半连接改写有严格前提:必须是等值关联、内层无聚合/排序/分组、optimizer_switch 的 semijoin 开关开启。碰到非等值条件(如 >)、带 LIMIT、含聚合函数的子查询,优化器可能退回逐行执行,执行次数被外表行数放大。学会看 EXPLAIN 确认是否真的被优化,必要时手动改写,依然重要。
为何常见:学习了优化器原理后容易产生“优化器万能”的错觉,没注意到触发条件。EXPLAIN 中 select_type=SUBQUERY 往往就是“没被优化好”的信号。
误区查询慢就是缺索引,我给关联列都加上索引,子查询肯定快了,还用得着改 SQL 吗?
正解索引降低的是单次探测代价(从 O(N) 到 O(log N)),但相关子查询的瓶颈是“执行次数 = 外层行数”。一万行外表即使每次都走索引探测,也要启动一万次内层查询,函数调用和上下文切换的开销远大于索引收益。应优先做半连接/JOIN 改写减少执行次数,索引只是辅助手段。
为何常见:索引是 SQL 优化中最常听到的“银弹”,容易把所有慢查询都归因于缺索引,忽略了“执行次数”这一结构性瓶颈。
面试题
Q1. 相关子查询与独立子查询的本质区别是?
- A.子查询能否引用外层表的列← 正确答案
- B.子查询返回结果的记录条数
- C.子查询中是否包含聚合函数
- D.子查询是否使用 GROUP BY 子句
答案:A。相关子查询的核心特征就是引用外层表的列,导致外层每行都要重新执行一次内层查询;独立子查询不引用外层列,执行一次即可。返回条数、聚合函数、GROUP BY 都只是可能的特征差异,不是本质区别。理解这一点才能把握两种子查询的成本模型。
Q2. 用 EXISTS 改写 IN 子查询的主要目的是?
- A.让子查询能够引用外层表的列
- B.小表驱动大表降低扫描成本← 正确答案
- C.消除子查询中的排序开销
- D.让查询结果集自动完成去重
答案:B。EXISTS 的语义是“找到一条匹配即停止”,当内层表较小且有索引时,外层逐行探测的成本远低于把整个内层结果集物化出来比对,这就是“小表驱动大表”的核心思想。A 是相关子查询的特性,C 和 D 与 EXISTS 的主要目的无关。
Q3. 半连接优化最核心的优化动作是?
- A.将两个查询简单的合并执行
- B.提前物化子查询结果并去重← 正确答案
- C.强制全表扫描得到精确基数
- D.在外层表新建辅助的索引
答案:B。半连接优化的关键是先把子查询结果物化成一张去重临时表,再让外层逐行探测。物化让内层只执行一次,去重避免同一匹配反复命中。A 只是合并而非去重,C 是反向操作,D 是辅助手段而非核心动作。
Q4. EXPLAIN 中哪个标记表示子查询被单独物化?
- A.select_type 字段显示为 SIMPLE
- B.select_type 字段显示为 SUBQUERY← 正确答案
- C.type 字段显示为 ALL 全表扫描
- D.key 字段显示为 NULL 无索引
答案:B。select_type=SUBQUERY 或 MATERIALIZED 说明子查询被单独执行并物化为临时表,需要在执行计划中重点关注它的 rows 成本。SIMPLE 表示该查询没有子查询,ALL 是全表扫描的访问方式,key 为 NULL 表示没有可用索引,都与“子查询物化”无直接关系。
Q5. 外表 10000 行、内表 100 行,相关子查询最坏要扫描内表多少次?
- A.100 次,相当于内表总行数
- B.10000 次,相当于外表行数
- C.10100 次,两表行数之和
- D.100 万次,两表行数之积← 正确答案
答案:D。相关子查询对外表的每一行都会触发一次内层查询,所以子查询执行的次数是 10000 次(外表行数);最坏情况下每次内层都要全表扫描 100 行,总的行扫描量是 10000 × 100 = 1,000,000 行。这就是相关子查询在大表上灾难性慢的根本原因——总成本是行数的乘积。
Q6. 子查询改 JOIN 时正确的做法是?
- A.等值关联条件下直接改写← 正确答案
- B.结果重复时需添加 DISTINCT← 正确答案
- C.任何子查询都适合改写成 JOIN
- D.改写后需用 EXPLAIN 验证← 正确答案
答案:ABD。等值关联(如 cid = id)是子查询改 JOIN 的基础条件;当内表存在一对多关系时 JOIN 会产生重复行,必须用 DISTINCT 去重;改写后对比 EXPLAIN 的 rows 与 type 是标准验收方式。C 错误——带 LIMIT、聚合、非等值条件的子查询改写风险很高,不是所有情况都适合。
Q7. 哪些情况会让 IN 子查询表现很差?
- A.外层是大表且关联列无索引← 正确答案
- B.内层结果集过大且未物化← 正确答案
- C.内层与外层存在等值关联条件
- D.优化器选择了全表逐行过滤← 正确答案
答案:ABD。外层大表无索引会让外层探测的成本极高;内层结果集过大且未物化时,临时表的构建和维护开销巨大;优化器选择全表逐行过滤说明没有走半连接优化路径,rows 会被放大。C 是子查询可以被半连接优化的前提条件,本身不是性能差的原因。
Q8. 半连接物化策略最核心的收益是?
- A.外层每行都做快速的哈希探测
- B.内层先执行物化并自动去重← 正确答案
- C.优化器跳过全部索引扫描工作
- D.相关子查询被完全转换为常量
答案:B。半连接物化先把内层子查询执行一次并构建去重临时表,避免每个外层行重复启动子查询,同时去重消除了重复匹配带来的行数放大。A 描述的是物化后的探测方式,不是核心收益;C 和 D 都是对优化器行为的曲解。
Q9. 改写后 EXPLAIN 的 rows 从 1000 万变成 10 万,正确解读是?
- A.表的统计信息已经完全过期
- B.优化器选择了更优的驱动关系← 正确答案
- C.索引数量增多导致扫描变快
- D.查询缓存命中率大幅提升
答案:B。rows 骤降说明执行路径从“逐行触发子查询”(成本被放大为乘积)变为“一次物化 + 哈希探测”,这是驱动关系和数据访问方式改变的结果,与统计信息、索引数量、查询缓存无关。rows 是优化器估算的扫描行数,缓存不影响估算,索引若未变化也不会导致 rows 缩减。
Q10. 编写高效子查询时应该遵循哪些原则?
- A.外层尽量由小表驱动大表← 正确答案
- B.内层结果集尽量保持较小← 正确答案
- C.所有子查询都改写成 JOIN
- D.关联列上建立有效的索引← 正确答案
答案:ABD。小表驱动大表能减少探测次数,内层小结果集能降低物化成本,关联列索引能加速内层探测,三者都是高效子查询的正确原则。C 错误——子查询并非都要改写成 JOIN,优化器会自动处理多数等值场景,可读性和验证同样重要,盲目改写反而可能引入重复行等问题。