跳到主要内容

Databend 优化器工作原理

Databend 查询优化器的核心任务,是将 SQL 语句转化为可执行的计划。其工作流程是:构建查询的抽象表示,补充实时统计信息,应用规则重写进行优化,探索不同的 join 顺序,最终选择成本最低的物理算子。

无论是分析报表、JSON 搜索、向量检索还是地理空间分析,都使用同一个优化器——Databend 用一个优化器即可处理所有数据类型

Databend 优化器的核心优势

  • 统计信息自动更新:数据写入时会即时维护行数、取值范围和 NDV,优化器可直接使用最新信息进行选择性估算、join 排序和成本计算,无需手动维护。
  • 结构优先,成本其次:先去除相关子查询、下推谓词和 limit、拆分聚合,缩小搜索空间,尽量将计算推向数据源。
  • 动态规划与 Cascades 协同DPhpy 负责寻找最优 join 顺序,Cascades 在同一 SExpr memo 上选择成本最低的物理算子。
  • 原生支持分布式:规划阶段即可决定本地或分布式执行,必要时将广播改写为基于 key 的 Shuffle,避免热点问题。

示例查询

下面这条查询将贯穿整个优化过程:

WITH recent_orders AS (
SELECT *
FROM orders
WHERE order_date >= DATE_TRUNC('month', today()) - INTERVAL '3' MONTH
AND fulfillment_status <> 'CANCELLED'
)
SELECT c.region,
COUNT(*) AS order_count,
COUNT(o.id) AS row_count,
COUNT(DISTINCT o.product_id) AS product_count,
MIN(o.total_amount) AS min_amount,
AVG(o.total_amount) AS avg_amount
FROM recent_orders o
JOIN customers c ON o.customer_id = c.id
LEFT JOIN products p ON o.product_id = p.id
WHERE c.status = 'ACTIVE'
AND o.total_amount > 0
AND p.is_active = TRUE
AND EXISTS (
SELECT 1
FROM support_tickets t
WHERE t.customer_id = c.id
AND t.created_at > DATE_TRUNC('month', today()) - INTERVAL '1' MONTH
)
GROUP BY c.region
HAVING COUNT(*) > 100
ORDER BY order_count DESC
LIMIT 10;

第一阶段:准备查询并补充统计信息

先梳理查询结构,为其配备准确的统计数据,为后续成本估算奠定基础。

1. 展平相关子查询

EXISTS (...) 改写为 semi-join,确保后续优化在统一的 join 树上进行:

# 改写前(相关子查询)
customers ─┐
├─ JOIN ─ orders
support ───┘ │
└─ EXISTS(...)

# 改写后(semi-join)
customers ─┐
support ───┴─ SEMI JOIN ─ orders

对应的 SQL 变为:

FROM (
SELECT *
FROM orders
WHERE order_date >= DATE_TRUNC('month', today()) - INTERVAL '3' MONTH
AND fulfillment_status <> 'CANCELLED'
) o
JOIN customers c ON o.customer_id = c.id
LEFT JOIN products p ON o.product_id = p.id
JOIN (
SELECT DISTINCT customer_id
FROM support_tickets
WHERE created_at > DATE_TRUNC('month', today()) - INTERVAL '1' MONTH
) t ON t.customer_id = c.id

2. 利用统计信息优化聚合

对于无过滤条件的聚合(如 MIN(o.total_amount)),优化器可直接使用统计信息中的值:

SELECT MIN(total_amount) FROM orders

SELECT table_stats.min_total_amount

在本例中,orders 表有过滤条件,所以需要保持实际计算。

3. 为扫描节点附加统计信息

规划阶段记录每个扫描节点的行数、取值范围、NDV 和直方图,供后续选择性估算和成本计算使用。

4. 归一化聚合

有了统计信息,优化器将可共享的计数统一表示,例如将 COUNT(o.id) 改写为 COUNT(*),避免重复计算:

SELECT c.region,
COUNT(*) AS order_count,
COUNT(*) AS row_count,
COUNT(DISTINCT o.product_id) AS product_count,
MIN(o.total_amount) AS min_amount,
AVG(o.total_amount) AS avg_amount
...

第二阶段:精简逻辑计划

通过规则重写消除不必要的计算。

1. 下推过滤和 LIMIT

# 改写前
Filter (o.total_amount > 0)
└─ Scan (recent_orders)

# 改写后
Scan (recent_orders, pushdown_predicates=[total_amount > 0])

带 LIMIT 的排序也会重新排列,先限制再排序:

# 改写前
Limit (10)
└─ Sort (order_count DESC)
└─ Join (...)

# 改写后
Sort (order_count DESC)
└─ Limit (10)
└─ Join (...)

2. 去除冗余谓词

# 改写前
Filter (1 = 1 AND c.status = 'ACTIVE')
└─ ...

# 改写后
Filter (c.status = 'ACTIVE')
└─ ...

3. 拆分聚合

# 改写前
Aggregate (COUNT/AVG)
└─ Scan (recent_orders)

# 改写后
Aggregate (final)
└─ Aggregate (partial)
└─ Scan (recent_orders)

部分聚合在靠近数据处完成,最终只需合并结果。

4. 将过滤条件下推至 CTE

仅依赖 recent_orders 的谓词(如 o.total_amount > 0)被下推至 CTE 定义,从源头减少数据量:

WITH recent_orders AS (
SELECT *
FROM orders
WHERE order_date >= DATE_TRUNC('month', today()) - INTERVAL '3' MONTH
AND fulfillment_status <> 'CANCELLED'
AND total_amount > 0
)

第三阶段:确定连接策略和物理执行方案

逻辑计划已优化,统计信息就绪,优化器做出关键决策:

1. 选择 join 顺序

DPhpyOptimizer 使用统计驱动的动态规划评估各种 join 顺序,倾向于让过滤后的小维表构建 hash table,较大的 recent_orders 做 probe:

    customers      products
\ /
HASH JOIN (build)
|
recent_orders (probe)
|
SEMI JOIN support_tickets

2. 调整连接语义

a. 将满足条件的 LEFT JOIN 转为 INNER JOIN

示例中 LEFT JOIN products p 带有 p.is_active = TRUE,仅保留匹配行,可安全转为 INNER JOIN:

# 改写前
recent_orders ──⊗── products (LEFT)
filter: p.is_active = TRUE

# 改写后
recent_orders ──⋈── products (INNER)

b. 去除重复 join 条件

o.customer_id = c.id 等条件若重复出现,DeduplicateJoinConditionOptimizer 仅保留一份,避免执行阶段重复计算。

c. 按需交换 join 两侧

启用额外重排时,CommuteJoin 可交换左右输入,让小表构建 hash table,或配合集群分发策略:

# 改写前                     # 改写后(小表负责建 hash table)
customers ──⋈── recent_orders recent_orders ──⋈── customers

3. 选择物理算子和执行模式

CascadesOptimizer 在共享 memo 中比较 hash join、merge join、nested loop 等实现,同时决定计划本地执行还是分布到 warehouse 节点。需要分布式执行且存在大表时,将 broadcast 改写为 Hash Shuffle,避免大表复制到所有节点。最终移除多余 projection 和未使用的 CTE,保持计划精简。

观测与调试

  • EXPLAIN:查看优化器最终生成的执行计划。
  • EXPLAIN PIPELINE:了解算子在各节点上的排布与调度情况。
  • SET enable_optimizer_trace = 1:记录优化器每一步,便于在查询日志中复盘。