lakehouse 第 18 章 把读湖拆成四层漏斗:partition pruning → file pruning → row-group/page pruning → 字典过滤,并说明每层元数据从哪来、数据布局如何决定 \(p_{\text{part}}\)、\(p_{\text{file}}\)、\(p_{\text{rg}}\)。那条文章回答 「裁剪链路是什么、为什么有效」。
本文是 query-engine 系列 第 15 篇,回答 「谁在 planner 的哪一步发起裁剪、约束如何传到 Worker 上的 scan 算子」:
- SQL 里的
WHERE如何变成 Connector 的 layout constraint。 - Trino Iceberg connector 在 Metadata → SplitManager → PageSource 的职责切分。
- Spark / DuckDB 同一谓词在 planning 入口 上的差异(能力表引用官方文档,不排名 latency)。
不重复粘贴 lakehouse/18 的漏斗全文与 Parquet 格式细节;需要时跳转 lakehouse/18、08 Iceberg 元数据、02 Parquet。
环境说明:本机 WSL2、无 JVM Trino/Spark。Trino/Spark planner 行为按 476+ / 3.5+ 官方文档与源码结构 归纳;不伪造
EXPLAIN输出。裁剪效果数字复用 lakehouse/18 的 PyIcebergplan_files()+ DuckDB 1.5.4 本机实测(A 级)。
一、与 lakehouse/18 的分工
| 问题 | lakehouse/18 | 本篇(query-engine/15) |
|---|---|---|
| 四层漏斗定义 | ✅ 完整展开 | 仅引用,不复述 |
| 元数据字段含义 | ✅ manifest bounds、PageIndex | 引用 08 |
| 数据布局与 \(p_{\text{file}}\) | ✅ 实测 user_id vs
country |
引用实验结论 |
| SQL 优化器 IR 层级 | 不涉及 | ✅ AST → Logical → Physical → Split |
Trino IcebergMetadata
调用链 |
不涉及 | ✅ |
| Dynamic filter 与 planning 时序 | 概述 | ✅ 与 第 5 篇 衔接 |
| Split 携带什么 | split 大小一句 | ✅ ConnectorSplit + 残余谓词 |
读 lakehouse/18 建立 「读湖时发生了什么」;读本篇建立 「优化器如何发起读湖」。两篇合读才能从 SQL 文本追到 Worker 上的 Parquet reader。
二、从 SQL 谓词到 Layout Constraint
第 2 篇 之后,一条带 Iceberg 表扫描的查询在引擎内大致经历:
flowchart TB
SQL["SQL + WHERE"] --> AN["Analyzer<br/>绑定表/列"]
AN --> LOG["Logical Plan<br/>TableScan + Filter"]
LOG --> OPT["Optimizer<br/>谓词下推/常量折叠"]
OPT --> PHY["Physical Plan<br/>TableScanNode"]
PHY --> META["Connector Metadata<br/>applyFilter / layout"]
META --> ICE["Iceberg TableScan.planFiles()"]
ICE --> SPL["Splits + residual predicate"]
SPL --> SCAN["Parquet PageSource"]
关键分界:
- 引擎内优化(Trino planner、Spark
Catalyst):常量折叠、谓词合并、join 下推——产出对
TableHandle 的 TupleDomain
或等价约束(Trino SPI
ConnectorMetadata.applyFilter)。 - Connector + Iceberg 库:把 TupleDomain
映射为 Iceberg
Expression,调用TableScan.planFiles()做 manifest 层 partition/file pruning(Iceberg spec, Planning;A 级)。 - Worker scan:对每个 split 打开 Parquet,做 row-group/page pruning;残余谓词在向量算子上求值。
lakehouse/18 的 funnel 主要覆盖 步骤 2 的后半 + 步骤 3;本篇聚焦 步骤 1→2 的接口 与 步骤 2→split 的分发。
三、Trino:Metadata 层注入约束
Trino Iceberg connector(文档 476+,源码
trinodb/trino
plugin/trino-iceberg,A 级)在 planning
阶段的典型职责:
| 阶段 | 组件 | 输入/输出 |
|---|---|---|
| 解析表 | IcebergMetadata.getTableHandle |
表名 →
IcebergTableHandle(snapshot、schema) |
| 下推过滤 | applyFilter /
applyProjection |
Logical 约束 → 更窄的 handle + 未下推残余 |
| 布局 | getTableLayouts / 新 API 路径 |
调用 Iceberg planning,得 文件清单 |
| 分片 | IcebergSplitManager.getSplits |
文件 →
IcebergSplit(path、offset、length、delete
files) |
| 扫描 | IcebergPageSourceProvider |
Split → Parquet reader + 动态过滤 |
3.1 常量折叠与分区 transform
Trino 对 Iceberg 隐藏分区
的谓词改写(constant folding + desugaring)发生在
引擎 optimizer,再经
applyFilter 进入 Iceberg(Trino blog, Just
the right time date predicates with Iceberg,
2023-04-11,B 级)。这与 lakehouse/18 第二节「partition
pruning 在 manifest 层」衔接:引擎先改写 SQL
谓词,Iceberg 库再在 manifest entry 上过滤。
3.2 Dynamic filter 与 split 生成时序
第 5 篇 讲过 broadcast/partitioned join;Trino dynamic filter 在 build 侧收集 join key,运行时合并进 probe 侧 scan 的 constraint(Trino 481 docs, Dynamic filtering)。Planning 因此可能是 两阶段:
- Coordinator 先基于静态谓词 planFiles,生成初始 split。
- Join build 完成后,probe scan 等待 dynamic
filter(
iceberg.dynamic-filtering.wait-timeout),再缩小实际读范围。
这是 query-engine 独有 的时序问题:lakehouse/18 讲静态漏斗,本篇补 MPP 上 runtime 约束如何并入 scan。
3.3 EXPLAIN 读什么(不贴未实测输出)
Trino EXPLAIN ANALYZE 中关注(文档 SQL
explain):
- Scan 算子
input rows/input sizevs 表总行数。 dynamicFilters是否出现在 probe 侧 Iceberg scan。- Layout 行上的 partition constraint 文本。
本机未跑 Trino 时,用 lakehouse/18 的 PyIceberg 数文件 + 文档字段对照,避免伪造 CLI 输出。
四、Spark Catalyst:DataSource V2 扫描
Spark 3.5+ 读 Iceberg 走 DataSource V2(Spark SQL Guide;Iceberg Spark 文档,A 级):
| 阶段 | Spark 侧 | Iceberg 侧 |
|---|---|---|
| 分析 | V2ScanRelation |
解析 catalog 表 |
| 优化 | PushDownUtils 谓词/投影下推 | SparkScanBuilder 接收 filter |
| 物理 | BatchScanExec |
planFiles() 文件任务 |
| 执行 | Executor 读 Parquet | 同 Trino ③④ 层 |
df.explain() / SQL UI 看
PushedFilters、PartitionFilters(lakehouse/18
附录 B)。AQE(第 13
篇)在 shuffle 后改分区数,不改变 manifest planning
接口,但会改 scan 并行度。
五、DuckDB:单进程内 Metadata + Scan
DuckDB iceberg 扩展(文档 Lakehouse
formats,A 级)在 同一进程 完成:
- SQL optimizer 下推 filter/project 到
IcebergScan物理算子。 - 扩展读 snapshot/manifest,等价 Iceberg
planFiles()。 - 复用 Parquet scan 做 row-group 跳过。
无 Coordinator:没有 split
跨网络分发,EXPLAIN ANALYZE 看
Total Files Read 与 scan 行数(lakehouse/18
实验二、本篇 第
14 篇 宽表实验)。
六、四层漏斗:Planner 发起点对照
| 漏斗层 | 主要发起者 | Trino 可见性 | Spark | DuckDB |
|---|---|---|---|---|
| ① partition | Iceberg planFiles(),谓词由 engine
折叠后传入 |
Layout / constraint | PartitionFilters |
scan 算子 Filters |
| ② file min/max | 同上 | input size 下降 |
PushedFilters + 文件数 |
Total Files Read |
| ③ row-group/page | Parquet reader(Worker/进程内) | Scan CPU vs input rows | Executor scan | EXPLAIN ANALYZE 行数 |
| ④ dictionary | Parquet reader | 少单独字段 | 同左 | 同左 |
| Runtime | Dynamic filter → 重新约束 scan | dynamicFilters |
Runtime filtering(版本相关) | join filter 下推(形态不同) |
能力边界以各引擎 当前文档 为准,不做跨引擎 latency 排名。
七、实验:复用 lakehouse/18 的 Metadata Planning 数字
下列结果来自 lakehouse/18 本机 PyIceberg
0.11.1 真实执行(10 个按天分区文件),证明
步骤 2 planFiles() 的裁剪——与
Trino applyFilter 后调用 Iceberg planning
的语义一致(Iceberg Java/Python API 同 spec):
| 谓词 | 裁剪类型 | 扫描数据文件数 |
|---|---|---|
| 无 | — | 10 |
event_ts 单日 |
partition pruning | 1 |
user_id 窄区间 |
file pruning(min/max) | 1 |
country = 'CN' |
无法 file prune | 10 |
Planner 视角解读:
- 引擎把
event_ts折叠成对 day transform 的约束 → Iceberg 在 manifest 分区字段上匹配 → ① 生效。 user_id谓词进入planFiles()后与 manifest bounds 求交 → ② 生效。country每文件 min/max 覆盖谓词 → ② 不裁剪;不是 planner 漏下推,而是 stats + 布局决定(lakehouse/18 第三节)。
Row-group 层:lakehouse/18 实验二 DuckDB 1.5.4 对排序 Parquet 窄区间扫描 10000 行 vs 打乱文件,中位耗时 2.6 ms vs 6.3 ms——发生在 Split 打开文件之后,③ 层与引擎无关布局的数据仍一致。
八、常见「下推失效」在 Planner 上的信号
| 现象 | Planner 层根因 | 查什么 |
|---|---|---|
| 扫全表文件 | 谓词未进
applyFilter(函数包裹列、类型不匹配) |
EXPLAIN 是否 Filter 在 Scan 之上 |
| 分区未裁 | 非确定性函数、缺 transform 推导 | SQL 是否裸列比较常量 |
| 文件未裁 | manifest 无 bounds / 列未索引 | $files / PyIceberg
plan_files |
| 行组未跳 | 数据未排序、无 PageIndex | Parquet footer |
| Join 仍扫大表 | 无 static 谓词且无 dynamic filter | Trino dynamicFilters;join 类型 |
排查顺序:先确认约束是否进入 Connector,再查 Iceberg stats,最后查 Parquet 布局——与 lakehouse/18 附录 A 自检清单正交:那边偏 数据与格式,本篇偏 计划形状。
九、小结
- lakehouse/18 = 读湖漏斗与布局;本篇 = SQL planner 如何把谓词交给 Iceberg、MPP 上 split 与 dynamic filter 时序。
- Trino:
IcebergMetadata.applyFilter→ planFiles → Split → PageSource;Spark:V2Scan pushdown → BatchScanExec;DuckDB:单进程 IcebergScan + Parquet。 - 实测数字引用 lakehouse/18 PyIceberg/DuckDB,不伪造 Trino
EXPLAIN。 - 生产内存与 spill 导致的 scan 放大见 第 16 篇;故障排查见 第 17 篇。
下一篇:内存、Spill 与资源隔离
返回 系列目录 · lakehouse/18 读湖漏斗
参考资料
- Apache Iceberg, Table Spec —
Planning、
planFiles()(A 级)。 - Trino Documentation 476+, Iceberg
connector、
ConnectorMetadata、Dynamic filtering(A 级;本机未实测 CLI)。 - Apache Spark SQL Guide 3.5+, Iceberg Spark 文档 — V2 pushdown(A 级)。
- DuckDB Documentation, Iceberg Extension(A 级)。
- lakehouse/18 本机 PyIceberg + DuckDB 1.5.4 实验(A 级)。
- Trino Blog, Just the right time date predicates with Iceberg, 2023-04-11(B 级)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【分布式 OLAP 查询引擎】Trino · Spark · DuckDB · 优化与 MPP 执行
闭合数据平台栈最后一块:从 SQL 解析与 Calcite 式优化,到 Volcano/向量化执行、Trino Coordinator/Worker 与 shuffle,再到 Iceberg connector 下推与生产排查。承接 lakehouse 第 18 章读湖视角,补全「谁在做 planning」的引擎内核层。
【分布式 OLAP 查询引擎】OLAP 查询引擎全景:从单进程到 MPP
从 OLTP/OLAP/HTAP 边界、嵌入式 DuckDB 与分布式 Trino/Spark 分工、批式扫描与交互式查询延迟口径出发,闭合 lakehouse 与 stream-processing 之间的查询层缺口,并给出本系列 18 篇地图。
【分布式 OLAP 查询引擎】Coordinator 与 Worker:Query / Stage / Task / Driver
拆解 Trino Coordinator 与 Worker 的职责边界,从 Query 到 Stage、Task、Driver、Operator 的五层执行模型,Split 调度与 data locality,并与 Spark Driver/Executor/Stage 对照。
【分布式 OLAP 查询引擎】Trino 查询路径:从 SqlQueryExecution 到 Page 流
主线拆解 Trino Coordinator 上 SqlQueryExecution 的生命周期:analyze、plan、fragment、Stage 调度到 Worker Task;Iceberg connector 如何从 snapshot/manifest 过滤生成 Split;Page 在 Operator 链上流动;EXPLAIN 与 EXPLAIN ANALYZE 字段判读。