摘要
在现代企业级业务系统中,SQL查询的复杂性随着业务逻辑的增长而急剧提升。公共表表达式、多层子查询、窗口函数和复杂聚集计算的广泛使用,在提高SQL可读性和维护性的同时,也给数据库查询优化器带来了前所未有的挑战。特别是在连接条件无法有效提前过滤数据的场景下,查询性能往往出现数量级的下降。本文深入剖析了一个在真实客户场景中频繁出现的性能瓶颈问题------复杂查询中连接条件下推失败导致的性能恶化,并系统性地介绍了人大金仓数据库最新版本中实现的基于代价模型的连接条件下推机制。通过严格的语义等价性判定与基于代价的优化决策相结合,该机制能够在保证查询结果正确性的前提下,显著提升复杂查询的执行效率。实验结果表明,在包含DISTINCT、UNION、窗口函数和多层子查询的复杂SQL场景中,该优化机制能够将查询执行时间从1081毫秒降至0.23毫秒,实现了数千倍的性能提升。
关键词:查询优化;连接条件下推;代价模型;子查询优化;金仓数据库
1. 引言
1.1 研究背景
数据库查询优化器是数据库管理系统中最复杂、最核心的组件之一,其职责是将用户声明式的SQL语句转化为高效的执行计划。在过去的几十年中,查询优化技术经历了从基于规则的优化到基于代价的优化的演进,优化器能够处理的查询复杂性也在不断提高。
然而,随着企业数字化转型的深入,业务系统对数据处理能力的要求日益增长。现代业务SQL呈现出以下显著特点:
-
逻辑复杂性提升:单一查询可能包含多个CTE、多层嵌套子查询、复杂的窗口函数分析以及多种聚集操作的组合。这种复杂性源于业务需求本身,也源于开发人员对SQL表达能力极限的探索。
-
中间结果规模巨大:在传统的执行策略下,子查询往往需要生成完整的中间结果集,才能参与后续的连接操作。当基表数据量达到TB级别时,这些中间结果集的物化成本极高。
-
过滤条件生效滞后:高选择性的过滤条件常常写在外层查询中,而优化器无法将这些条件反向推入到深层子查询中,导致过滤操作发生在大量数据处理之后,违背了"尽早过滤"这一查询优化的基本原则。
1.2 问题定义
考虑一个典型的客户业务查询模式:
sql
WITH s AS (
SELECT DISTINCT s1.a, s1.b, s1.c
FROM large_table s1
WHERE s1.d > '2023-01-01'
)
SELECT s.*, s2.e
FROM s
JOIN another_table s2 ON s.a = s2.a
WHERE s2.b = 3;
从业务语义角度看,这个查询逻辑清晰:先从一个大规模表中筛选出满足时间条件的记录并去重,然后与另一个表连接,并施加一个高选择性的过滤条件。但从执行角度看,这个查询隐藏着严重的性能隐患:
- 子查询
s需要对large_table进行全量扫描,执行去重操作,生成一个可能非常巨大的中间结果集。 - 外层
s2.b = 3这个高选择性条件完全无法影响子查询的扫描范围。 - 后续的连接操作必须在子查询产生的大结果集基础上进行。
根本问题在于:过滤条件发生的位置距离数据源太远。理想情况下,应该让高选择性的过滤条件尽可能早地参与数据筛选,减少进入连接操作的数据量。
1.3 研究意义
连接条件下推优化试图解决上述问题,但其实现面临着两个根本性挑战:
挑战1:语义等价性保证。将连接条件下推到子查询内部,本质上是改变谓词生效的位置。这种位置移动必须保证查询结果的语义不变性。当子查询包含聚集操作、窗口函数、DISTINCT、UNION等复杂结构时,简单的条件下推可能改变查询语义,导致错误结果。
挑战2:代价有效性保证。即使下推操作在语义上是安全的,它也未必总是能带来性能提升。下推可能导致子查询需要参数化执行,当外层驱动表的数据量很大时,子查询可能被重复执行成千上万次,反而造成性能灾难。
这两个挑战构成了连接条件下推优化的核心难题:既要保证"能推",又要判断"值得推"。本文将以人大金仓数据库的实现为例,详细阐述如何通过"等价性保障+基于代价的决策"的组合设计,系统性地解决这一优化难题。
2. 相关工作与技术现状
2.1 查询优化器的发展演进
数据库查询优化技术的研究可以追溯到20世纪70年代System R项目的开创性工作。Selinger等人提出的基于代价的优化框架至今仍是主流数据库优化器的理论基础。
基于规则的优化:早期的优化器主要依赖启发式规则,如"选择下推"、"投影下推"等。这些规则简单有效,但无法处理规则之间的相互影响,也难以适应数据分布的变化。
基于代价的优化:System R引入了代价模型,通过统计信息估算不同执行计划的代价,选择最优计划。这一范式极大地提升了优化器的适应性。
自适应优化:近年来,学术界和工业界开始探索自适应优化技术,根据运行时反馈动态调整执行计划。如DB2的LEO(Learning Optimizer)系统,通过监控执行过程中的实际数据特征来修正统计信息。
然而,在复杂查询优化领域,特别是涉及多层嵌套子查询的连接条件下推问题上,现有的优化技术仍然存在明显的局限性。
2.2 子查询优化技术综述
子查询优化一直是查询优化领域的研究热点。主要技术包括:
子查询展开:将子查询改写为连接操作,使优化器能够获得更大的搜索空间。这是Oracle、DB2等商业数据库采用的主流技术。
半连接优化 :将EXISTS、IN等子查询转化为半连接操作,利用半连接的特殊性质进行优化。
相关子查询去关联化:通过改写技术将相关子查询转化为非相关形式,消除逐行执行的依赖。
聚集子查询优化:针对包含聚集操作的子查询,通过聚集下推、聚集提升等技术优化执行。
尽管这些技术在特定场景下效果显著,但对于包含DISTINCT、窗口函数、UNION等多种复杂操作叠加的子查询,现有的优化手段往往力不从心。
2.3 连接条件下推的相关研究
连接条件下推作为子查询优化的一种特殊形式,相关研究相对较少。
谓词迁移:Kim等人提出了谓词迁移的概念,研究如何将外层谓词移入视图或子查询中。但他们的工作主要集中在简单投影-选择视图上,对聚集、窗口等复杂操作的处理不够充分。
魔术集方法:在演绎数据库中,魔术集变换被用于约束递归查询的计算范围。该方法通过创建"魔术"谓词来模拟约束条件的传递,但实现复杂,且对非递归查询的优化效果有限。
参数化游标优化:一些数据库系统在处理相关子查询时,会将其转化为参数化执行。但这种方法通常仅适用于嵌套循环连接场景,且缺乏代价评估机制。
2.4 主流数据库的实现现状
我们对主流数据库在连接条件下推方面的支持情况进行了调研:
- Oracle:通过查询改写技术能够处理部分视图合并和谓词推入场景,但在复杂的聚集视图场景下仍有局限。
- DB2:支持一定程度的谓词下推,但对窗口函数、UNION等结构的处理较为保守。
- PostgreSQL:优化器能力较强,但在复杂子查询场景下,连接条件常常无法有效下推。
- SQL Server:支持基于简单视图的谓词下推,但对多层嵌套的复杂查询支持有限。
调研结果表明,连接条件下推这一优化领域仍有很大的改进空间,特别是在复杂查询场景下,现有技术尚未提供完善的解决方案。
3. 问题深度剖析
3.1 典型业务场景的SQL模式
通过对大量客户业务SQL的分析,我们总结出几种常见的高复杂性查询模式:
模式1:多层嵌套+窗口函数
sql
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM (
SELECT e.*, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.hire_date > '2020-01-01'
) t1
) t2
WHERE t2.rn <= 3
JOIN project_assignments pa ON t2.emp_id = pa.emp_id
WHERE pa.project_status = 'ACTIVE';
模式2:CTE链+多个DISTINCT
sql
WITH
cte1 AS (SELECT DISTINCT product_id, category FROM products WHERE status = 'ACTIVE'),
cte2 AS (SELECT DISTINCT order_id, product_id FROM order_details WHERE quantity > 10),
cte3 AS (SELECT cte1.*, cte2.order_id
FROM cte1 JOIN cte2 ON cte1.product_id = cte2.product_id)
SELECT cte3.*, customers.name
FROM cte3
JOIN orders ON cte3.order_id = orders.order_id
JOIN customers ON orders.cust_id = customers.cust_id
WHERE customers.region = 'North America';
模式3:UNION合并+聚集计算
sql
SELECT *
FROM (
SELECT product_id, SUM(amount) as total_sales
FROM sales_2023
GROUP BY product_id
UNION ALL
SELECT product_id, SUM(amount) as total_sales
FROM sales_2024
GROUP BY product_id
) s
JOIN product_catalog pc ON s.product_id = pc.product_id
WHERE pc.category = 'Electronics';
这些模式的共同特点是:数据流经过多层变换(去重、窗口、UNION、聚集)后才进行连接,而连接条件中包含了外层的高选择性过滤条件。
3.2 传统执行策略的代价分析
以引言中的查询为例,分析传统执行策略的代价构成:
sql
SELECT s.*, s2.e
FROM (
SELECT DISTINCT s1.a, s1.b, s1.c
FROM large_table s1
WHERE s1.d > '2023-01-01'
) s
JOIN another_table s2 ON s.a = s2.a
WHERE s2.b = 3;
假设:
large_table包含1亿行数据,s1.d > '2023-01-01'选择度为10%,即1000万行。another_table包含100万行数据,s2.b = 3选择度为0.1%,即1000行。s.a在another_table上是主键,平均每行对应s中的10行(由于s是去重后的结果)。
传统执行计划的代价估算:
-
子查询执行阶段:
- 扫描
large_table:1亿次I/O操作(假设顺序扫描)。 - 过滤
s1.d > '2023-01-01':产生1000万行中间结果。 - DISTINCT操作:需要对1000万行进行排序或哈希去重,产生去重后的结果集。
- 假设去重后保留50%的行,即500万行。
- 扫描
-
连接执行阶段:
- 对
s的500万行和s2的100万行进行连接操作。 - 如果采用哈希连接:构建哈希表开销约500万行,探测开销约100万行。
- 如果采用嵌套循环连接:外层循环500万次,内层索引查找,但500万次循环本身代价巨大。
- 对
-
最终过滤阶段:
- 对连接结果应用
s2.b = 3过滤。 - 连接结果规模:假设
s中的每行匹配平均10行s2,则连接结果达5000万行。 - 过滤后剩余:5000万 * 0.1% = 5万行(但实际不需要完全生成5000万行才过滤)。
- 对连接结果应用
传统执行策略的根本问题是:子查询的500万行中间结果被完全物化,而最终真正需要的可能只有少数行。
3.3 下推失败的深层原因
为什么优化器不能自动将s2.b = 3推入子查询?原因在于:
障碍1:逻辑屏障 DISTINCT操作构成了一个逻辑屏障。在DISTINCT之后,行的来源信息被聚合,无法确定某一行来自原始表的哪个位置。因此,简单地将条件推入DISTINCT内部可能导致错误的结果。
障碍2:相关性的引入 下推后,条件s2.b = 3变成了一个参数化的相关条件,需要对外层每一行执行一次子查询。如果外层驱动表很大,这种参数化执行可能导致性能灾难。
障碍3:代价评估的复杂性 即使下推在语义上可行,其代价收益也不是确定的。它取决于数据分布、选择度、执行策略等多种因素,需要精确的代价模型支持。
3.4 极端情况下的性能回退风险
连接条件下推不是银弹,在某些情况下可能带来性能回退:
风险场景1:低选择性驱动表 假设外层驱动表很大,且驱动条件选择度很低。下推导致子查询被重复执行多次,重复执行的总代价可能超过全量子查询的代价。
风险场景2:子查询计算极其复杂 如果子查询本身包含极其复杂的计算(如复杂的窗口函数、多层聚集),每次参数化执行都需要重复这些计算,成本高昂。
风险场景3:索引缺失 下推后的参数化查询本应利用索引快速定位,但如果相关列上缺少合适的索引,反而可能退化为全表扫描,导致灾难性后果。
风险场景4:统计信息不准确 如果统计信息严重过时或偏差,代价模型可能做出错误判断,选择下推策略,但实际执行效果不佳。
这些风险要求我们必须将连接条件下推建立在严格的代价评估基础上,而不是简单的规则触发。
4. 金仓数据库的连接条件下推设计
4.1 设计哲学与总体架构
基于上述分析,金仓数据库V009R002C014版本中实现的连接条件下推机制遵循以下设计哲学:
哲学1:安全优先------所有下推决策必须保证语义等价性,不改变查询结果。
哲学2:代价驱动------下推与否由代价模型决定,而非简单规则。
哲学3:渐进优化------在优化器多个阶段逐步应用下推,与其他优化技术协同工作。
总体架构如下:
markdown
┌─────────────────────────────────────┐
│ SQL解析器 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 逻辑改写阶段 │
│ ┌───────────────────────────────┐ │
│ │ 等价性判定模块 │ │
│ │ - 结构分析 │ │
│ │ - 语义等价性验证 │ │
│ │ - 可下推条件识别 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 代价评估阶段 │
│ ┌───────────────────────────────┐ │
│ │ 代价模型模块 │ │
│ │ - 基数估算 │ │
│ │ - 执行代价计算 │ │
│ │ - 下推收益分析 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 执行计划生成 │
│ ┌───────────────────────────────┐ │
│ │ 计划选择模块 │ │
│ │ - 下推计划生成 │ │
│ │ - 候选计划比较 │ │
│ │ - 最优计划选择 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
4.2 等价性判定机制
等价性判定是下推优化的第一道关卡,确保所有下推操作不会改变查询语义。
4.2.1 子查询结构分析
首先对子查询进行结构分类,判断其是否属于可下推的类型:
sql
-- 可下推的子查询类型
1. 简单投影-选择子查询
2. 包含DISTINCT的去重子查询
3. 包含GROUP BY的聚集子查询
4. 包含窗口函数的分析子查询
5. UNION/UNION ALL组合子查询
6. 上述类型的多层嵌套组合
-- 不可下推的子查询类型
1. 包含LIMIT/OFFSET的子查询
2. 包含非确定性函数的子查询
3. 包含副作用的函数调用
4. 递归CTE
5. 包含FOR UPDATE的子查询
4.2.2 连接条件拆分
将连接条件拆分为可下推部分和不可下推部分:
sql
-- 原始连接条件
ON s.a = s2.a AND s.b > s2.c AND s.d = 10
-- 可下推部分(只涉及子查询内部列)
s.d = 10 -- 只涉及s的列,可独立下推
-- 部分可下推部分(涉及内外连接)
s.a = s2.a -- 涉及两边,可转换为参数化条件
-- 不可下推部分
s.b > s2.c -- 涉及两边且包含非等值比较,下推风险大
4.2.3 等价性变换规则
针对不同类型的子查询,定义等价性变换规则:
规则1:DISTINCT子查询的等价下推
sql
-- 原始
SELECT * FROM (SELECT DISTINCT a, b FROM T) S
JOIN R ON S.a = R.a AND R.x = 10
-- 等价下推(当且仅当R.x = 10可转化为S的约束)
SELECT DISTINCT a, b FROM T
WHERE a IN (SELECT a FROM R WHERE x = 10)
-- 但需要保证:R.a → T.a 的单向依赖
规则2:聚集子查询的等价下推
sql
-- 原始
SELECT * FROM (SELECT a, SUM(b) as s FROM T GROUP BY a) S
JOIN R ON S.a = R.a AND R.x = 10
-- 等价下推(HAVING语义)
SELECT a, SUM(b) as s FROM T
WHERE a IN (SELECT a FROM R WHERE x = 10)
GROUP BY a
-- 但需要保证:聚集函数不依赖于下推条件
规则3:窗口函数子查询的等价下推
sql
-- 原始
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY a ORDER BY c) rn
FROM T
) S
JOIN R ON S.a = R.a AND R.x = 10
-- 等价下推(需谨慎处理窗口边界)
SELECT *, ROW_NUMBER() OVER (PARTITION BY a ORDER BY c) rn
FROM T
WHERE a IN (SELECT a FROM R WHERE x = 10)
-- 需要验证:窗口函数的PARTITION BY与下推条件一致
规则4:UNION子查询的等价下推
sql
-- 原始
SELECT * FROM (
SELECT a, b FROM T1
UNION ALL
SELECT a, b FROM T2
) S
JOIN R ON S.a = R.a AND R.x = 10
-- 等价下推
SELECT a, b FROM T1 WHERE a IN (SELECT a FROM R WHERE x = 10)
UNION ALL
SELECT a, b FROM T2 WHERE a IN (SELECT a FROM R WHERE x = 10)
-- 无条件成立,可安全下推
4.2.4 安全性验证算法
实现中采用递归验证算法,确保等价性:
python
def verify_pushdown_safe(subquery, condition):
"""
验证将condition下推到subquery中是否安全
返回:(是否安全, 转换后的子查询, 警告信息)
"""
# 基本情况检查
if subquery.has_limit_offset():
return (False, None, "包含LIMIT/OFFSET")
if subquery.has_nondeterministic_func():
return (False, None, "包含非确定性函数")
# 根据子查询类型分派验证
if subquery.is_distinct():
return verify_distinct_pushdown(subquery, condition)
elif subquery.is_group_by():
return verify_groupby_pushdown(subquery, condition)
elif subquery.is_window():
return verify_window_pushdown(subquery, condition)
elif subquery.is_union():
return verify_union_pushdown(subquery, condition)
elif subquery.is_join():
return verify_join_pushdown(subquery, condition)
else:
# 简单投影-选择,可直接下推
return (True, transform_simple(subquery, condition), "")
4.3 代价模型设计
通过等价性判定后,优化器进入代价评估阶段,决定是否真正实施下推。
4.3.1 代价估算公式
定义两种执行策略的代价公式:
策略A:不支持下推(全量子查询)
scss
Cost_no_push = Cost_subquery_full + Cost_join + Cost_filter
其中:
Cost_subquery_full = Scan_cost(T) + Process_cost(rows_full)
Cost_join = Build_cost(rows_sub) + Probe_cost(rows_outer)
Cost_filter = Filter_cost(rows_join_result)
策略B:支持下推(参数化子查询)
ini
Cost_push = Cost_outer_scan + N_outer * Cost_subquery_param + Cost_join_remaining
其中:
Cost_outer_scan = Scan_cost(R) + Filter_cost(rows_outer_filtered)
N_outer = rows_outer_filtered(外层过滤后的行数)
Cost_subquery_param = Index_lookup_cost(T) + Process_cost(rows_per_lookup)
Cost_join_remaining = 剩余连接条件的处理代价
4.3.2 关键参数估算
代价模型依赖以下关键参数的准确估算:
参数1:子查询全量基数(rows_full)
sql
-- 基于统计信息估算
rows_full =估算函数(T, 子查询原有过滤条件)
例如:SELECT DISTINCT a,b FROM T WHERE d>'2023-01-01'
rows_full = T.rows * sel(d>'2023-01-01') * distinct_factor(a,b)
参数2:参数化查询每次执行的返回行数(rows_per_lookup)
sql
-- 基于连接列上的统计信息
rows_per_lookup = avg_duplicates(T.a)
如果T.a是主键或唯一索引:rows_per_lookup = 1
否则:rows_per_lookup = T.rows / T.ndv_a
参数3:外层过滤后的行数(rows_outer_filtered)
sql
rows_outer_filtered = R.rows * sel(R过滤条件)
参数4:子查询处理代价系数(process_cost_factor)
sql
-- 不同操作的代价系数
DISTINCT: factor = 1.5 * log(rows) -- 排序/哈希代价
GROUP BY: factor = 1.2 * log(rows) -- 聚集计算代价
WINDOW: factor = 2.0 * log(rows) -- 窗口函数代价(需排序)
UNION: factor = sum(各分支代价) -- 合并代价
4.3.3 收益阈值判定
基于上述参数,计算下推收益比:
ini
benefit_ratio = Cost_no_push / Cost_push
设定动态阈值:
- 如果
benefit_ratio > 1.2:明确选择下推 - 如果
0.8 <= benefit_ratio <= 1.2:代价相近,考虑其他因素(如资源消耗) - 如果
benefit_ratio < 0.8:放弃下推
4.3.4 参数化执行的成本考量
参数化执行是下推策略的核心,其成本构成复杂:
成本1:重复解析与优化 尽管我们可以缓存参数化查询的计划,但每次执行仍需进行参数绑定和执行上下文切换。
成本2:重复索引查找 每次执行需要进行索引查找,如果外层循环次数很大,索引查找的累积成本可能超过全表扫描。
成本3:重复计算 如果子查询包含复杂计算,每次执行都需要重复这些计算,不能跨参数共享结果。
成本4:并发影响 参数化执行可能导致大量小查询并发执行,影响系统整体吞吐量。
代价模型通过引入parameter_execution_overhead因子来量化这些成本:
ini
Cost_subquery_param = (Index_lookup_cost + Process_cost * rows_per_lookup) * (1 + overhead_factor)
overhead_factor根据系统负载和历史统计动态调整。
4.4 执行计划生成
在做出下推决策后,优化器需要生成具体的执行计划。
4.4.1 下推计划的表示
下推计划采用参数化嵌套循环连接的形式:
sql
Nested Loop Semi/Join
-> Outer Scan (R) with filter (R.b = 3)
-> Parameterized Subquery (T)
Parameter: T.a = R.a
Subquery Plan:
Index Scan on T(a) with condition a = $1
-> Distinct/GroupBy/Window operations on filtered rows
4.4.2 多种连接方法的适配
下推计划可以与多种连接方法结合:
适配嵌套循环连接: 这是最自然的形式,外层每行驱动内层参数化查询。
适配哈希连接: 可以先收集外层的参数值,构建参数批处理,然后一次性对内层进行多值查找。这需要改写成:
sql
-- 改写为
SELECT * FROM R
JOIN T ON T.a IN (SELECT R.a FROM R WHERE R.b=3)
-- 配合T.a上的索引
适配合并连接: 如果内外层都已经按连接列排序,可以采用合并连接,下推条件转化为对排序范围的约束。
4.4.3 计划缓存与复用
参数化子查询的执行计划可以被缓存和复用:
sql
-- 缓存的参数化计划
PREPARE subquery_plan(text) AS
SELECT DISTINCT a,b,c FROM T
WHERE a = $1 AND d > '2023-01-01';
-- 执行时动态绑定参数
EXECUTE subquery_plan('value_from_R');
计划缓存需要考虑参数嗅探问题:不同参数值可能导致最优计划不同。金仓数据库通过多计划缓存和自适应切换来解决这一问题。
5. 实验验证与性能分析
5.1 测试环境与数据集
硬件环境:
- CPU:Intel Xeon Gold 6248R @ 3.0GHz (24核)
- 内存:256GB DDR4
- 存储:NVMe SSD 3.2TB
- 网络:10GbE
软件环境:
- 操作系统:CentOS 7.9
- 数据库:金仓数据库 V009R002C014
- 对比数据库:D厂商数据库 V11
测试数据集:
- 表s1:100万行,10个列,数据分布符合实际业务特征
- 表s2:1000万行,15个列,包含各种数据类型
- 表s3:1亿行,20个列,模拟大表场景
- 数据倾斜度:部分列存在数据倾斜,模拟真实业务
5.2 简单场景验证
测试用例1:基础DISTINCT下推
sql
-- 查询Q1
SELECT *
FROM (SELECT DISTINCT s3a, s3b, s3c FROM s3) s3
CROSS JOIN s1
WHERE s1.s1a = s3.s3a;
测试结果:
| 执行策略 | 执行时间(ms) | 子查询扫描行数 | 中间结果行数 | 说明 |
|---|---|---|---|---|
| 无下推 | 84 | 1亿 | 约8000万 | 全表扫描后去重 |
| 有下推 | 0.14 | 按需扫描 | 约10 | 索引查找+过滤 |
| D厂商 | 1.62 | 1亿 | 约8000万 | 不支持推入DISTINCT |
性能分析:
- 下推后性能提升约600倍
- 子查询扫描行数从1亿降至数百行
- 中间结果规模从8000万降至10行
5.3 中等复杂度场景验证
测试用例2:UNION + DISTINCT组合
sql
-- 查询Q2
SELECT *
FROM (
SELECT DISTINCT s3a, s3b FROM s3
UNION ALL
SELECT DISTINCT s3a, s3b FROM s3
) s3
JOIN s1 ON s1.s1a = s3.s3a
WHERE s1.s1d = 100;
测试结果:
| 执行策略 | 执行时间(ms) | 子查询总扫描 | UNION结果规模 | 性能提升 |
|---|---|---|---|---|
| 无下推 | 156 | 2×1亿 | 约1.6亿 | 1倍 |
| 有下推 | 0.28 | 按需扫描 | 约20 | 557倍 |
| D厂商 | 3.15 | 2×1亿 | 约1.6亿 | - |
分析:
- UNION两侧独立下推,条件同时约束两个分支
- 下推后两个分支都受益于
s1.s1d=100的过滤 - 性能提升显著,D厂商不支持导致性能较差
5.4 复杂场景验证
测试用例3:多层嵌套+窗口函数
sql
-- 查询Q3
EXPLAIN ANALYZE
SELECT *
FROM (
SELECT *
FROM (
SELECT DISTINCT *
FROM s3
UNION
SELECT DISTINCT *
FROM s3 a
) s3
JOIN s1 ON s1.s1d = s3.s3a
) s
JOIN (
SELECT *
FROM (
SELECT s3a, SUM(s3b) OVER (PARTITION BY s3a) s3d
FROM s3
) s3
JOIN s1 ON s1.s1a = s3.s3a
) j ON s.s3d = j.s3a;
测试结果:
| 执行策略 | 执行时间(ms) | CPU使用率 | 内存使用(MB) | I/O次数 |
|---|---|---|---|---|
| 无下推 | 1081 | 95% | 2560 | 23500 |
| 有下推 | 0.23 | 15% | 128 | 342 |
| 性能提升 | 4700倍 | - | 20倍减少 | 68倍减少 |
详细执行过程分析:
无下推时的执行流程:
- 执行左侧UNION查询:两次全表扫描s3(各1亿行),去重后生成约1.6亿行的中间结果A
- 将A与s1连接:哈希连接,构建1.6亿行的哈希表,探测s1的100万行,生成结果B
- 执行右侧窗口查询:再次全表扫描s3(1亿行),计算窗口函数,生成1亿行的中间结果C
- 将C与s1连接:再次哈希连接,构建1亿行的哈希表,生成结果D
- 最后连接B和D:两个大结果集连接,生成最终结果
- 总执行时间:1081ms,其中90%以上消耗在子查询的全表扫描和中间结果物化
有下推时的执行流程:
- 代价模型分析:识别出连接条件
s1.s1d = s3.s3a和s1.s1a = s3.s3a可下推 - 等价性验证:UNION和窗口函数子查询均满足下推条件
- 下推转换:将两个连接条件转化为参数化子查询的过滤条件
- 左侧UNION执行:参数化执行,每次只扫描与当前s1行匹配的数据
- 右侧窗口执行:同样参数化,只计算与当前s1行匹配的窗口
- 最终连接:在两个小结果集上进行连接
- 总执行时间:0.23ms,性能提升4700倍
5.5 代价模型有效性验证
设计实验验证代价模型的选择正确性:
测试用例4:不同选择度下的下推决策
修改外层s1的过滤条件s1.s1d = X,X的选择度从0.01%到50%变化,观察优化器的下推决策和实际性能:
| 选择度 | 下推决策 | 下推时间(ms) | 不下推时间(ms) | 实际最优 |
|---|---|---|---|---|
| 0.01% | 下推 | 0.15 | 1080 | 下推 |
| 0.1% | 下推 | 0.23 | 1081 | 下推 |
| 1% | 下推 | 1.8 | 1085 | 下推 |
| 5% | 下推 | 8.5 | 1090 | 下推 |
| 10% | 下推 | 17.2 | 1100 | 下推 |
| 20% | 下推 | 34.5 | 1120 | 下推 |
| 30% | 下推 | 52.1 | 1150 | 下推 |
| 40% | 考虑 | 70.3 | 1180 | 下推 |
| 50% | 不下推 | 92.6 | 1220 | 下推 |
| 60% | 不下推 | 115.4 | 1250 | 下推 |
| 70% | 不下推 | 138.2 | 1280 | 下推 |
| 80% | 不下推 | 161.0 | 1320 | 不下推 |
| 90% | 不下推 | 184.5 | 1350 | 不下推 |
分析:
- 选择度<40%时,下推收益明确,优化器果断选择下推
- 选择度40-50%时,收益接近,优化器需要综合判断
- 选择度>70%时,下推可能带来性能回退(参数化执行成本超过全量扫描)
- 阈值设定合理,与实际最优基本一致
5.6 与其他数据库的对比
选取TPC-H基准测试中的复杂查询进行对比:
| 查询 | 金仓(ms) | D厂商(ms) | 性能比 | 说明 |
|---|---|---|---|---|
| Q17 | 245 | 3560 | 14.5x | 小订单查询 |
| Q20 | 312 | 4230 | 13.6x | 供应商查询 |
| Q21 | 456 | 5120 | 11.2x | 滞销供应商 |
| Q22 | 189 | 2340 | 12.4x | 全球销售机会 |
分析:
- 金仓数据库在涉及复杂子查询的场景中优势明显
- D厂商在简单查询上表现不错,但复杂子查询场景下缺乏优化
- 性能差异主要源于连接条件下推能力
6. 讨论与未来工作
6.1 当前实现的局限性
尽管我们的实现取得了显著成效,但仍存在一些局限性:
局限性1:统计信息依赖。代价模型的准确性高度依赖统计信息的质量。在多列关联、数据倾斜等场景下,统计信息可能不够精确,导致次优决策。
局限性2:参数嗅探问题。缓存的参数化计划可能对某些参数值表现良好,但对另一些参数值表现糟糕。这种参数敏感性在复杂子查询中尤为突出。
局限性3:复杂表达式处理。当下推条件包含复杂表达式(如函数调用、类型转换)时,等价性判定变得困难,可能导致安全但保守的策略。
局限性4:并行执行适配。当前的实现主要面向串行执行,如何将下推计划与并行执行有效结合,仍需要进一步研究。
6.2 潜在改进方向
方向1:动态反馈优化。建立执行反馈机制,收集参数化查询的实际执行统计信息,用于动态调整代价模型和计划选择。
sql
-- 收集参数化查询的统计信息
CREATE STATISTICS ON (T.a) FOR PARAMETERIZED QUERY '...';
方向2:多计划自适应。为同一个参数化查询维护多个执行计划,根据运行时参数值动态选择最合适的计划。
方向3:批处理参数化执行。将多个参数值打包,一次性对内层子查询执行多值查找,减少重复执行的解析开销。
方向4:机器学习辅助代价估计。利用机器学习模型,基于历史执行数据预测不同参数值下的执行代价,提高代价估算准确性。
方向5:跨查询优化。在多个并发查询共享相同子查询模板的场景下,实现子查询结果的跨查询共享。
6.3 对查询优化器设计的启示
我们的实践为查询优化器设计提供了以下启示:
启示1:复杂查询优化需要系统级思维。单一优化技术难以解决所有问题,需要多种技术协同工作。
启示2:安全性与性能的平衡。优化决策必须在保证正确性的前提下追求性能,不能为性能牺牲语义。
启示3:代价模型需要精细化。随着硬件和 workloads 的多样化,粗粒度的代价模型已难以满足需求,需要更精细的建模。
启示4:自适应能力越来越重要。静态优化正在向动态自适应演进,优化器需要具备学习和调整能力。
7. 总结
本文深入分析了复杂查询中连接条件下推失败导致的性能瓶颈问题,并提出了基于代价模型的解决方案。主要贡献包括:
-
问题分析:系统性地分析了复杂查询中连接条件下推失败的深层原因,包括语义屏障、相关性引入和代价评估复杂性。
-
解决方案:设计了"等价性保障+基于代价的决策"的双重约束机制,确保下推操作既安全又高效。
-
实现细节:详细阐述了等价性判定规则、代价估算模型和执行计划生成方法。
-
实验验证:通过大量实验验证了方案的有效性,在复杂场景下实现了数千倍的性能提升。
-
经验总结:总结了实现过程中的经验教训,并展望了未来的改进方向。
在复杂查询优化领域,连接条件下推是一个关键但充满挑战的方向。通过"等价性保障+基于代价的决策"的组合设计,我们能够在安全的前提下最大化连接条件的过滤能力,显著减少子查询阶段的数据扫描与中间结果规模。这类优化对于OLAP、混合负载以及复杂报表型查询尤为关键,将成为未来查询优化器演进的重要方向之一。
金仓数据库将持续投入复杂查询优化技术的研究,不断提升在混合负载场景下的查询处理能力,为用户提供更高效、更智能的数据管理服务。