一、自适应查询执行概述
大家好,我是这次课程的主讲人。在数据库内核领域摸爬滚打了十几年,我见过太多因为一条慢查询导致整个系统雪崩的案例。今天咱们聊的自适应查询执行,说白了就是让数据库学会「见风使舵」——根据实际运行情况动态调整执行计划。
1.1 什么是自适应查询执行
传统数据库执行查询时,优化器会先根据统计信息生成一个执行计划,然后一条路走到黑。但统计信息可能过时,或者干脆就是错的。自适应查询执行(Adaptive Query Execution,简称 AQE)打破了这种僵局。
它的核心思想很简单:在执行过程中收集反馈,动态调整后续的执行策略。我习惯把它比作开车——不是出发前就定死路线,而是根据实时路况选择最佳路径。
定义:自适应查询执行是一种在查询运行时,根据中间结果的特征(如数据量、分布、倾斜度等)动态调整执行计划的技术。它让数据库从「静态优化」走向「动态优化」。
举个例子。你写了个 JOIN 查询,优化器估算左表有 100 万行,右表有 50 万行。实际运行时左表只有 10 万行,右表却有 200 万行。传统做法会按原计划执行,性能可想而知。自适应方案会在运行时发现这个偏差,立刻切换 JOIN 策略。
1.2 为什么需要自适应
你可能会问:优化器不是有统计信息吗?为什么还需要自适应?
嗯,这里有几个现实问题:
- 统计信息滞后:数据每天都在变,统计信息可能几天甚至几周没更新了。我在项目中遇到过,一张表实际有 5000 万行,统计信息显示只有 200 万行——差了 25 倍。
- 复杂谓词估算不准:多个过滤条件组合时,优化器的基数估算经常跑偏。比如 WHERE a > 10 AND b LIKE '%abc%',两个条件的选择率相乘,结果往往差之千里。
- 数据倾斜:这是分布式系统的噩梦。某个 key 占了 80% 的数据,Hash Join 时一个节点忙死,其他节点闲死。
- 参数化查询:同一个 SQL 模板,不同参数值对应的数据分布可能天差地别。比如 SELECT * FROM orders WHERE status = ?,status='PAID' 可能返回 100 行,status='PENDING' 可能返回 1000 万行。
我的经验:曾经有个客户,他们的报表查询每天凌晨跑 3 小时。我一看执行计划,优化器选的是 Nested Loop Join,但实际数据量足够用 Hash Join。原因就是统计信息没更新。加上自适应后,查询时间从 3 小时降到了 20 分钟。
1.3 核心挑战与目标
自适应查询执行听起来很美,但实现起来坑不少。我踩过的坑,今天都分享给你们。
核心挑战
| 挑战 | 说明 | 我的踩坑经历 |
|---|---|---|
| 决策时机 | 什么时候该调整?调早了信息不足,调晚了为时已晚 | 曾经在 Spark SQL 中,Stage 刚跑完 1% 就触发重优化,结果误判了 |
| 开销控制 | 自适应本身有开销,不能比不调还慢 | 有个版本加了太多检查点,查询反而慢了 15% |
| 计划稳定性 | 频繁变计划可能导致结果不一致或性能抖动 | 遇到过同一个查询每次执行计划都不一样,DBA 都不敢上线 |
| 分布式协调 | 多节点环境下,如何统一决策是个难题 | 各节点各自为政,结果数据 shuffle 了三次 |
设计目标
一个好的自适应系统,应该追求这几个目标:
- 准确性:动态调整必须基于可靠的数据,不能拍脑袋
- 低开销:自适应本身的 CPU、内存、网络开销要控制在 5% 以内
- 透明性:对用户无感,不需要改 SQL 或加 Hint
- 可预测:相同输入得到相同结果,不能因为自适应导致结果不一致
- 可扩展:从单机到千节点集群,自适应机制都能正常工作
避坑指南:我曾经在一个项目中过度追求自适应,加了太多动态调整点。结果查询计划在执行过程中变了 5 次,每次切换都要重新分配资源,最终性能还不如静态计划。记住:自适应不是万能的,该静态时就静态。
1.4 自适应查询执行的核心流程
下面这张图展示了自适应查询执行的核心流程。我把它画成了流程图,方便你理解整体脉络。
这个流程看起来简单,但每个环节都有学问。比如「运行时数据收集」,收集什么?怎么收集?开销多大?这些我在后续章节会详细展开。
1.5 自适应 vs 传统执行
为了让你更直观地理解差异,我列个对比表:
| 维度 | 传统查询执行 | 自适应查询执行 |
|---|---|---|
| 计划生成 | 优化阶段一次性生成 | 优化阶段生成初始计划,执行中动态调整 |
| 统计信息 | 依赖预收集的统计信息 | 结合预收集信息和运行时真实数据 |
| 应对数据倾斜 | 无特殊处理,可能导致 OOM | 动态拆分倾斜分区,均衡负载 |
| JOIN 策略 | 优化器固定选择,不可变更 | 根据实际数据量切换 Broadcast/SortMerge/Hash Join |
| 并行度 | 优化器固定设置 | 根据中间结果大小动态调整 |
| 适用场景 | 数据分布稳定、查询模式固定 | 数据倾斜严重、统计信息不准、查询多变 |
我的建议:不要一上来就全盘自适应。我习惯的做法是:先跑基线,找出那些「统计信息不准导致性能差 3 倍以上」的查询,然后针对性地开启自适应。这样收益最大,风险最小。
1.6 小结
自适应查询执行不是银弹,但它确实是数据库查询优化的重要演进方向。从 Spark 3.0 的 AQE,到 Apache Flink 的动态优化,再到各大商业数据库的类似特性,这个趋势已经很明显了。
记住三个核心点:
- 时机:在 Stage 边界或算子间做调整,不要过于频繁
- 数据:用运行时真实数据说话,别信估算
- 开销:自适应本身的开销必须可控,否则得不偿失