多关系查询计划
多关系(多表)查询的优化难点在于:随着连接数量增加,可选执行计划呈指数级爆炸(大约有 (4^n) 种 n 表连接次序)。因此优化器必须限制搜索空间。
自底向上(生成式)优化
代表系统:System R、DB2、MySQL、Postgres 等
核心思路:从“什么都没有”开始,一步步“构造”更大的计划,并用动态规划选择局部最优,从而得到全局近似最优。
( 一 )典型过程(以 System R 为例)
- 将 SQL 查询分解为查询块(如子查询、子句)
- 对每个查询块生成逻辑算子(选择、投影、连接、聚合等)
- 对每个逻辑算子生成若干物理算子实现(如嵌套循环连接、排序-合并连接、哈希连接)
- 使用动态规划按子集大小迭代构造“左深(left-deep)”连接树,评估每个部分计划的代价,保留代价最小的方案
( 二 )特点
- 易于实现,工程上成熟
- 搜索空间通常限制为左深树,大幅减少组合数
- 可与启发式规则结合(如先做选择、投影)加速搜索
( 三 )简化示例
三张表:A, B, C,条件:A ⋈ B ⋈ C
-
所有连接次序有:
- (A ⋈ B ⋈ C)
- (A ⋈ C ⋈ B)
- (B ⋈ A ⋈ C)
- ……(很多等价形式)
-
自底向上动态规划做法:
- 先找所有单表访问的最优方式(索引扫描 / 全表扫描)
- 然后对所有表对 {A,B}、{A,C}、{B,C} 找最优连接方式
- 最后在这些“最优二表结果”基础上,再加入第三张表,得到最终最优计划
自顶向下(变换式)优化
代表系统:MSSQL、Greenplum、CockroachDB、Volcano 等
从一个完整的初始逻辑查询计划出发,通过一系列规则重写和实现选择,寻找等价但更优的计划。
( 一 )关键机制
-
变换规则(逻辑→逻辑) 例如:
- 连接交换:( R ⋈ S \Rightarrow S ⋈ R )
- 连接结合:( R ⋈ S ⋈ T \Rightarrow R ⋈ S ⋈ T )
- 谓词下推、投影下推等
-
实现规则(逻辑→物理) 将“连接”逻辑算子替换为具体的哈希连接、排序-合并连接等物理算子
-
备忘录(Memo)结构 记录“同一逻辑表达式的不同等价形式和实现”,防止重复搜索
-
Enforcer(属性强制器) 特殊物理算子,用于保证某些输出属性(如排序、有无去重、分区方式),例如:
- 为满足 ORDER BY 引入 sort 算子
- 为分布式连接引入 repartition 算子
( 二 )特点
- 搜索空间更灵活,可处理复杂查询变换
- 依赖规则系统设计与启发式剪枝
- Memo + 自顶向下递归,可避免无效重复计算
数据统计信息
优化器需要数据统计帮助估算选择率与中间结果大小,从而估计成本。
直方图(Histogram)
真实数据常常偏斜(skewed),又不能把每个值都存下来。直方图是常见的压缩统计方式。
(一)等宽直方图(Equi-Width)
- 将取值域按固定宽度划分成多个区间(bucket),每个 bucket 记录该区间内值的频数
- 优点:结构简单,构建代价低
- 缺点:若数据分布非常不均匀,有些桶非常空,有些则非常满(对选择率估计不友好)
示例
假设年龄字段 age ∈ [0, 100],切成 10 个桶,每个区间宽度为 10:
- 桶1:[0, 10),计数 100
- 桶2:[10, 20),计数 1000
- …… 如果绝大多数人年龄集中在 20–30 岁,等宽直方图会浪费很多桶在无人区间。
(二)等深直方图(Equi-Depth)
- 每个桶包含大致相同数量的元组,因此桶宽度不固定
- 对偏斜数据更友好:高密度区间被划分为多个窄桶,低密度区间为宽桶
示例
仍然是年龄数据:
- 桶1:包含约 10% 的记录,对应年龄 [0, 15]
- 桶2:下一个 10% 的记录,对应年龄 [15, 21]
- 桶3:再下一个 10%,对应 [21, 24]
- ……
可以看到年龄集中区间会被切得更细,有利于精确选择率估计。
(三)端偏直方图(End-Biased)
-
使用前 (N-1) 个桶精确存储最频繁的若干值,最后一个桶存储“其他所有值”的平均频率
-
适合存在少数“重元素(heavy hitters)”场景,例如:
- status 字段中 90% 是 “ACTIVE”,10% 分散在其它值
- country 字段中 “US”/“CN”/“IN” 特别多,其它国家很少
示例(N=4)
- 桶1:value = 'US',计数 5,000,000
- 桶2:value = 'CN',计数 3,000,000
- 桶3:value = 'IN',计数 1,000,000
- 桶4:其余所有国家的总计 1,000,000,并只存储平均值(如每个约 100)
当谓词是 country = 'US' 时,估计极精确;
当谓词是 country = 'JP' 时,就用桶4的平均值估计。
(四)Sketch / 草图结构
一些系统用近似结构代替直方图,例如:
- Count-Min Sketch:估计频数
- HyperLogLog:估计去重计数(cardinality)
特点是:空间极小 + 估计近似,适合大规模或流式数据。
抽样(Sampling)
通过对表的子集计算统计信息来近似整个表的选择率。
( 一 )两种方式
- 维护只读样本表:定期或按阈值重建样本
- 直接对真实表在线抽样:如 TABLESAMPLE
( 二 )更新策略
当原表修改量超过某阈值(如 10%)时,重建或更新样本。
( 三 )示例
假设原表有 10 亿行,用 1% 的随机样本(1000 万行)估计选择率:
- 查询:
WHERE age BETWEEN 20 AND 25 - 在样本中有 1,000,000 行符合
- 估计总体满足行数 ≈ 1,000,000 × 100 = 100,000,000 行
- 选择率 ≈ 10%
成本估计(Cost Estimation)
优化器通过成本模型评估不同执行计划,选择代价最小者。
成本指标
常见成本维度:
-
CPU:
- 每条记录或每个算子需要的 CPU 操作
- 单次成本小,但计数准确较难(依赖实现细节与硬件)
-
磁盘 I/O:
- 访问的磁盘块数(block transfers)
- 通常被视为主要成本来源(特别是传统磁盘)
-
内存:
- 算子所需的 DRAM 空间
- 会影响是否需要溢写磁盘(如哈希连接溢出)
-
网络:
- 分布式系统中消息数、传输字节数
由于计划枚举空间巨大,穷举所有执行计划不现实,必须限制搜索空间。
统计信息表
大部分系统会在内部 catalog 中维护统计表,而不是每次都在线扫描全表。
对每个关系 (R),典型统计包括:
- (N_R):表 R 的元组数
- (V(A, R)):属性 (A) 在表 R 中的不同取值数量(distinct)
基于这些信息,可以推导选择基数(Selection Cardinality):
[ SC(A, R) = \frac{N_R}{V(A, R)} ]
表示:在均匀分布假设下,每个不同值大约对应 (SC(A,R)) 行。
示例
- 表 R 有 (N_R = 1{,}000{,}000) 行
- 字段
city有 (V(city,R) = 100) 个不同城市 - 则 [ SC(city, R) = \frac{1{,}000{,}000}{100} = 10{,}000 ]
- 估计
WHERE city = 'Beijing'时,将返回约 10,000 行(假设均匀)
选择谓词与选择率估计
选择率(Selectivity)
选择率 (\text{sel}(P)) 定义为:满足谓词 (P) 的元组占总元组的比例,即一个概率。
- 输出行数 ≈ 输入行数 × 选择率
(一)简单谓词(等值唯一键)
对于唯一键上的等值谓词(如主键)估计很简单:
-
如果主键保证唯一:
WHERE id = 123的选择率 ≈ (1 / N_R)- 返回行数 ≈ 1
这种场景下,索引选择非常明确(如 B+ 树索引)。
(二)复杂谓词(范围、合取)
如图中所示,复杂谓词(范围、多个条件 AND/OR 组合)估计要复杂得多,需要组合多个选择基数。
示例一:范围谓词
- 表 R 有 1,000,000 行,
age∈ [0, 100],假设均匀 - 查询:
WHERE age BETWEEN 20 AND 30 - 选择率 ≈ 区间长度 / 总范围 = 10 / 100 = 0.1
- 结果行数 ≈ 100,000 行
示例二:合取谓词(AND)
-
条件:
age BETWEEN 20 AND 30 AND city = 'Beijing' -
若假设两个谓词独立(重要假设),且
- sel(age) = 0.1
- sel(city='Beijing') = 0.05
-
则整体选择率估计为: [ sel(\text{age}) \times sel(\text{city}) = 0.1 \times 0.05 = 0.005 ]
-
结果行数 ≈ 1,000,000 × 0.005 = 5,000 行
(三)否定谓词(NOT)
利用“概率视角”可以很简单地处理否定谓词:
[ sel(\text{NOT } P) = 1 - sel(P) ]
文中示例给出了一个否定查询,其选择率为 ( \frac{4}{5} ),正好是 1 减去正向查询选择率。
具体示例
WHERE status = 'ACTIVE'的选择率估计为 0.2- 则
WHERE status <> 'ACTIVE'的选择率估计为: [ 1 - 0.2 = 0.8 ]
选择率估计中的关键假设
优化器通常会依赖三大假设来简化选择率与基数估计:
( 一 )均匀分布(Uniform Data)
- 除了少数“重元素”,所有取值在域中均匀分布
- 问题:现实数据往往高度偏斜(如热门城市、热门商品)
( 二 )谓词独立(Independent Predicates)
-
不同属性上的谓词相互独立
-
问题:真实世界中属性往往高度相关,如:
city与country显然相关salary与title相关
-
如果忽略相关性,多条件 AND 的结果可能被估得过低或过高
( 三 )包含原则(Containment Principle)
-
假设连接键域有良好重叠:
- 每个 inner 表中的 key 都能在 outer 表中找到匹配
-
实际中若存在大量“孤立键”(只出现一边),估计也会偏差
这些假设往往不满足,导致基数估计误差,进一步影响成本估计和计划选择。
连接基数估计(Join Size Estimation)
考虑两个关系 (R) 与 (S),在共享属性 (A) 上进行等值连接:
[ R ⋈_{R.A = S.A} S ]
在之前三大假设(均匀、独立、包含)下,常用估计公式为:
[ |R ⋈ S| \approx \frac{N_R \times N_S}{\max(V(A,R), V(A,S))} ]
其中:
- (N_R, N_S):R 和 S 的行数
- (V(A,R), V(A,S)):属性 A 在各表中不同值个数
解释
- 若 A 值在两表中大致均匀分布,且每个 A 值都在两边出现
- 那么每个 A 值在 R 中大约有 (N_R / V(A,R)) 行,在 S 中约有 (N_S / V(A,S)) 行
- 对所有可能的 A 值累加,得到上述近似
具体示例
- 表 R:100,000 行,
dept_id的不同值为 100 - 表 S:200,000 行,
dept_id的不同值为 150 - 则 [ |R ⋈ S| \approx \frac{100{,}000 \times 200{,}000} {\max(100, 150)} = \frac{20{,}000{,}000{,}000}{150} \approx 133{,}333{,}333 ]
如果真实数据中:
- 只有 50 个 dept_id 在两边都有
- 且各 dept_id 频数分布极不均匀 则真实 join 结果可能远小于 1.3 亿行,说明估计误差可能非常大。
误差传播问题
文中指出:估计误差会沿着查询计划向下游传递并放大
-
一个算子的输出基数若估偏:
- 下一个算子的输入基数就会错
- 进一步导致后续算子成本估计连续偏差
-
结果:整个 plan 的成本评估严重失真,优化器可能选择极差的执行计划
小结与建议
( 一 )优化器的基本流程
- 利用统计信息(直方图、抽样、sketch)估计各谓词与连接的选择率与基数
- 在成本模型(CPU/I/O/内存/网络)下评估每个候选执行计划
- 通过自底向上(动态规划)或自顶向下(规则重写)探索有限的计划空间
( 二 )工程上的启示
-
统计信息是否新鲜、是否能反映数据偏斜,对查询性能极其关键
-
自建系统时,要重视:
- 统计表设计(NR,V(A,R) 等)
- 直方图类型选择(等宽 / 等深 / 端偏)
- 抽样频率与代价权衡
-
在使用数据库时,合理地:
- 定期
ANALYZE/ 更新统计信息 - 避免写极端相关、极端偏斜而又让优化器误以为“独立/均匀”的查询模式
- 定期