cmu15445 2025fall lec15 query optimiaztion Pt1

lec15 Query Optimization Pt1

HOW do we go from SQL to a query plan?

parser->binder->optimizer

today's Agenda

bg, Transformations, Heuristic/Ruled-based optimization , cost-based optimization

BG

优化器职责: 逻辑计划->物理计划(必须说明具体怎么做)

三大挑战:

  • 搜索空间巨大 (Large search space):
    对于一个多表连接查询,可能的组合顺序和算法组合成千上万。优化器不能傻傻地每个都试一遍,得有策略地寻找。
  • 准确判断优劣 (Accurately determine):
    它怎么知道 37 次 I/O 的计划一定比 3,151 次的好?这就涉及到了代价模型 (Cost Model)。它会根据表的大小(10,000 条记录)、索引的情况、磁盘 I/O 速度等估算一个"分数"。
  • 高效搜索 (Efficiently search):
    它必须在极短的时间内(通常是毫秒级)找到那个最优(或接近最优)的方案,不能因为优化 SQL 本身就花了半天时间。
  1. 物理算子(Physical Operators)的特性
    图中提到了三个非常重要的实战细节:
  • 定义具体的执行策略(Specific execution strategy):
    逻辑上只是一个 JOIN,但物理上必须决定是 Nested-LoopSort-Merge 还是 Hash Join。这就是决定**访问路径(Access Path)**的过程。
  • 依赖数据的物理格式(Physical format):
    物理算子会根据数据是否已经排好序(sorting)、是否压缩(compression)来选择最快的路。
    • 例子: 如果两张表在磁盘上本来就是按 did 排好序的,优化器会瞬间决定用 Sort-Merge Join,因为"排序"这一大块开销直接省掉了。
  • 不是 1:1 的映射(Not always a 1:1 mapping):
    这是一个高级考点。一个逻辑算子可能会变成多个物理算子的组合,或者多个逻辑算子被合并成一个物理操作。
    • 例子: 逻辑上的 SELECT + JOIN 可能会被合并成一个物理上的 Index Nested-Loop Join(利用索引一次性完成过滤和关联)。
Optimization Granularity(粒度)

选择 # 1:单查询优化 (Single Query) ------ 主流做法

  • 优点: 搜寻最优方案的范围(Search space)小,优化过程非常快。
  • 缺点:
    • 各管各的: 即使你先后运行了两个几乎一样的 SQL,数据库通常也不会复用上一次的结果,而是重新算一遍。
    • 资源竞争: 优化器得考虑当前系统的负载(比如 CPU 此时是不是很忙),来决定要不要给这个查询分配更多资源。

选择 # 2:多查询优化 (Multiple Queries) ------ 高级/数仓做法

  • 优点: 共享经济。如果 10 个人都在查同一张大表的不同维度,优化器可以只扫描一次表,把数据同时分发给这 10 个查询。这就大大节省了 I/O。
  • 缺点:
    • 太复杂: 搜索空间呈指数级爆炸。要同时计算多个 SQL 的最优解,优化器本身可能就会跑很久。
    • 延迟: 有时为了等其他类似的查询一起"拼车"处理,第一个查询的响应时间反而变长了。
observation

它指出了决定一个查询优化器"聪明程度"的三大支柱

  1. Transformations / Enumeration(变换与枚举)------ "解题思路多不多"
  2. Search Algorithm(搜索算法)------ "找答案快不快"
  3. Cost Model(代价模型)------ "判断标准准不准"

Transformations

核心任务:语义等价 (Semantically Equivalent)

logical to logical

目标:

  • 降低执行成本 (Lower cost): 显而易见,比如把 Join 之后再 Filter 变成先 FilterJoin(谓词下推),直接砍掉 90% 的计算量。
  • 解锁后续变换 (Unlock additional transformations): 这是一个"连招"逻辑。有些变换本身可能不怎么省时间,但它能改变查询的结构 ,从而让另一个威力巨大的变换变得可行。
    • 打个比方: 你把嵌套子查询改写成 Join,这一步可能提升一般,但变成 Join 后,优化器就能使用 Index Nested-Loop Join 了,这才是真正的性能杀手锏。
一些技巧

1.Split Conjunctive Predicates (拆分连词谓词)

  • 做法:WHERE A=1 AND B=2 拆成两个独立的过滤条件:Filter(A=1)Filter(B=2)
  • 目的: 拆开后,优化器就可以灵活地把其中一个条件先"推"下去。比如 A 字段有索引,那就先处理 A=1
    2.Predicate Pushdown (谓词下推) ------ 最常用、威力最大
  • 做法: 也就是我们之前反复提到的,把 WHERE 条件尽可能地往数据源头(Scan)推。
  • 意义: 与其把 100 万行数据连表后再过滤掉 99 万行,不如先过滤掉 99 万行再连表 。这能瞬间把 Join 的压力降低几个数量级。
    3.Replace Cartesian Products with Joins (将笛卡尔积替换为连接)
  • 做法: 识别出那些没有连接条件的 CROSS JOIN,并尝试结合后面的 WHERE 子句将它们转化成真正的 INNER JOIN
  • 意义: 还记得你发的第一张 2,000,000 I/O 的图吗?那就是笛卡尔积。这一步变换能直接阻止这种"自杀式"查询计划的产生。
    4.Projection Pushdown (投影下推)
  • 做法: 尽早只保留需要的列。如果你一张表有 100 个字段,但你最后只要 name,那么在读取数据时就只读 name
  • 意义: 减少数据在内存/网络中的体积。尤其是对于列式存储数据库(如 ClickHouse),这一步能极大减少硬盘读取量。

即便优化器掌握了所有"变换神技",在实际搜索时,它其实是在 "半盲"状态 下工作的。

图中提到了搜索时面临的两个核心挑战:

1.信息的缺失 (Missing Information)

优化器在制定计划时,往往并不知道查询执行时的真实情况

  • 带输入变量的预编译语句 (Prepared Statements):

    如果你写的是 WHERE id = ?,在生成计划时,优化器并不知道这个 ?1 还是 1,000,000

    • 为什么这很重要? 如果 ? 是唯一主键,用 Index Join 最好;如果 ? 范围很大,可能 Sort-Merge Join 更快。优化器在这里必须做抉择(或先生成一个通用计划)。
  • 缺失统计信息 (Missing statistical information):

    数据库需要知道表里大概有多少行、数据分布是否均匀。如果统计信息过期了(比如表里刚导进了 1 亿行数据但没更新统计),优化器就会"看走眼",选出一个极慢的计划。

    2.搜索的本质

    既然信息不全,搜索算法的目标就从"寻找绝对最优解"变成了**"寻找一个足够好的物理计划 (Search for a good physical plan)" 。

heuristics /Rules

这就像是给优化器写了一本"避坑指南"。

  • 核心逻辑: 凭经验办事。比如:"先把能过滤的都过滤了(Selections first)"、"能下推的谓词一定下推"。
    尽早执行限制性最强的选择(Most restrictive selection early): 如果一个条件能一下子过滤掉 99% 的数据,那就第一步做它。
    先选择再连接(Selections before joins): 也就是"减肥后再相亲",减少 Join 操作的负担。
    各种下推(Pushdowns): 包括谓词(WHERE)、限制(LIMIT)和投影(SELECT 字段)。越早下推,后续算子的内存压力就越小。
    简单的连接(join) 谁先写谁先/简单统计
  • 为什么叫它"盲目":
    • 不看数据(Do not need to examine data): 它只看表结构(Catalog),不关心表里有 10 行还是 10 亿行。
    • 死理: 哪怕下推一个过滤条件反而会让查询变慢(极少数极端情况),它也会照做,因为它觉得这是"政治正确"。
  • 现状: 现代优化器通常把这作为第一步预处理,用来快速剪枝,扔掉那些明显傻掉的方案。

优点: 实现调试简单(就是一堆if-else),简单查询跑得快

缺点:依赖魔法数字,处理不了复杂的相互依赖

这是现代数据库的大脑,也是之前那张 37 I/Os 图背后的功臣。

  • 核心逻辑: 枚举出各种等价计划,给每个计划"算一卦",算出一个代表开销的数字(Cost)

  • 怎么选: 谁分低选谁。

  • 为什么它更聪明: 它会看统计信息 。如果它发现 dname='Toy' 的部门在表里占了 90%,它可能就不会选择用索引,而是直接全表扫描(因为此时索引回表反而更慢)。

  • 步骤 :

    计划枚举

    • 单表 (Single relation): 是全表扫描还是走索引?单表只需考虑路径访问方法?
    • 多表 (Multiple relations): 先连 A 和 B,还是先连 B 和 C?用 Hash Join 还是 Index Join?
    • 嵌套子查询 (Nested sub-queries): 是直接执行子查询,还是把它"拉平"变成一个大 Join?
      代价估算(IO CPU 内存/网络)

    搜索终止条件

    search termination

    1.Wall-clock Time (墙上时钟时间)

    • 做法: 定个闹钟,比如 100 毫秒。闹钟一响,不管找到哪一步,都从目前已知的计划里选一个最好的。
    • 适用场景: 对响应时间极其敏感的在线业务(OLTP)。
      2.Cost Threshold (代价阈值)
    • 做法: 设定一个"及格线"。只要找到一个计划,它的成本(Cost)低于这个阈值,优化器就觉得"够用了,收工"。
    • 直白点说: "不要最好,只要够好"。
      3.Exhaustion (穷尽搜索)
    • 做法: 把所有能变的花样全部试一遍,直到无路可走。
    • 适用场景: 通常只在查询非常简单 (比如只有一两张表)或者**子计划/小组(sub-plan/group)**内部使用。
    • 注意: 如果表多了,这种方法会导致计算量爆炸。
      4.Transformation Count (变换次数限制)
    • 做法: 限制优化器尝试"规则"和"变换"的总次数。比如只允许你尝试 500 次等价改写。
    • 逻辑: 尝试的变换越多,搜索树就越深。限制次数就是限制了搜索的深度和广度。
Access Path Transformation(对应single-relation)

这步是决定:为了拿到数据,我是该"走路"去(全表扫描),还是"骑车"去(利用索引)? '

下面这些参数都不重要,可跳过

优化器会权衡以下几个关键因素来计算每种路径的"成本":

1.Selectivity of predicate (谓词选择率) ------ 最重要的参数

  • 意思: 你的过滤条件能过滤掉多少数据?
  • 成本逻辑:
    • 如果你查 id = 1(选择率极高,只有一行),索引绝对最快。
    • 如果你查 gender = 'Male'(选择率很低,占了一半人),数据库可能会觉得走索引还得不断"回表",反而不如直接全表扫描 来得痛快。
      2.Data structures (数据结构)
  • B+Tree vs. Hash Table:
    • 如果你是范围查询(age > 20),B+树索引才管用,哈希索引直接废掉。
    • 如果是等值查询(id = 100),哈希索引可能比 B+树更快。
      3.Sort order (排序顺序)
  • 伏笔: 如果你后续有个 ORDER BY 操作,而某个索引本身就是排好序的,优化器会给这个索引"加分",因为它能帮你省掉后面专门排序的开销。
    4.Data accoutrements (数据"配饰"/附加属性)
  • INCLUDE: 这是指覆盖索引(Covering Index)。如果你要查的字段全在索引里,不需要回表看原始行,成本会大幅下降。
  • Zone maps: 常见于列式存储,记录了每一块数据的最大最小值。如果查询条件不在这个范围内,直接整块跳过,这叫"谓词下推"的极致物理实现。
    5.Compression / Encoding (压缩与编码)
  • 如果数据被高度压缩,读取它会省 I/O,但解压会费 CPU。优化器需要计算这个权衡。
single-relation query planning(对于单表查询,最重要的就是选最好的accessing method,也就是做好access path transformation)

sargable: search argument able 可搜索参数化

如果一个查询条件是sargable, 这意味着优化器可以直接利用索引来快速定位数据,而不是扫全表

Multi-relation query planning(单表基础上,重点在表与表间)

选择1 : bottom-up / forward chaining 自底向上(理论上优于自顶向下)

选择2 : top-down / backward chaining 自顶向下

Bottom-up

使用广度优先搜索(BFS)

讲到这里结束了,只是介绍了system R的实现

Top-down

使用深度优先搜索(DFS)

相关推荐
郝学胜-神的一滴1 小时前
干货版《算法导论》03:动态数组 × 链表的极致平衡艺术
java·数据结构·c++·python·算法·链表
2301_766283441 小时前
如何在 Go 中使用 gocql 执行本地 CQL 脚本文件
jvm·数据库·python
dFObBIMmai1 小时前
MongoDB防注入攻击指南
jvm·数据库·python
彳亍1011 小时前
如何解决Oracle启动ORA-00119错误_网络服务名与listener相关性
jvm·数据库·python
SamDeepThinking1 小时前
IntelliJ IDEA 中有什么让你相见恨晚的技巧?
java·后端·程序员
weixin_459753941 小时前
c++怎么编写多线程安全的跨平台文件日志库_无锁队列与异步IO【附源码】
jvm·数据库·python
夏恪1 小时前
如何用 IDBKeyRange 范围匹配检索特定区间的本地数据
jvm·数据库·python
SamDeepThinking1 小时前
为什么选微服务而不是动态扩容单体
java·后端·架构
2301_766283441 小时前
如何防止SQL拼接漏洞_使用PDO对象实现安全的SQL交互
jvm·数据库·python