
Polars Lazy API 指南延迟执行、查询计划优化与谓词/投影下推【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polarsPolars 以“Rust 编写、极速的 DataFrame 查询引擎”著称而其性能优势的重要来源就是 Lazy惰性API。本篇以用户指南 Concepts 章节中的 lazy-api.md 为骨架结合实际可运行的 Python 示例代码与仓库底层实现系统讲解 eager / lazy 两种模式的区别、为什么延迟执行能带来显著加速、查询规划器如何自动完成谓词下推Predicate Pushdown与投影下推Projection Pushdown以及如何用explain预览查询计划。读完你将掌握何时选择 Lazy API、如何编写惰性查询、如何读懂并验证 Polars 自动生成的优化方案。什么是 Lazy APIeager 与 lazy 两种执行模式Polars 支持两种操作模式Eager即时执行API 调用后立刻执行马上返回中间结果。本文之前介绍的例子使用的都是 eager API。Lazy惰性执行先用LazyFrame描述“想做什么”查询只有在你显式调用collect去**收集collect**时才会真正求值。推迟执行听起来只是换个时机但正是“把执行推迟到最后一刻”给了查询规划器query planner做全局优化的空间这也是为什么在大多数场景下Lazy API 是官方推荐的默认选择。Eager API 示例每步立刻执行先看一段典型的 eager 代码完整内容见仓库示例文件 lazy-vs-eager.pydf pl.read_csv(docs/assets/data/iris.csv) df_small df.filter(pl.col(sepal_length) 5) df_agg df_small.group_by(species).agg(pl.col(sepal_width).mean()) print(df_agg)这段代码基于经典的 Iris 鸢尾花数据集仓库内已附带该数据见 docs/assets/data/iris.csv依次完成了三件事读取整个 Iris 数据集按sepal_length花萼长度过滤只保留大于 5 的行按species物种分组计算sepal_width花萼宽度的均值。由于是 eager API每一步都会立即执行并返回中间结果read_csv先把整个 CSV 完整读进内存filter产生一个中间 DataFramegroup_by再产生下一个中间结果。这可能非常浪费——我们可能读取并处理了大量根本用不到的数据。Lazy 与 eager 的核心区别维度Eager APILazy API执行时机每步调用即执行描述完整查询后调用collect才执行中间结果每步都会物化为 DataFrame中间过程只保留“查询描述”不物化跨步骤优化无法看到后续操作查询规划器可基于全查询做全局优化适合场景探索性分析、关心中间结果正式数据处理追求性能与内存效率为什么要优先使用 Lazy API查询规划器的下推优化如果换用 Lazy API并且把执行一直等到所有步骤都定义好之后查询规划器就能执行一系列优化。对上面这个例子而言最典型的两项是谓词下推Predicate pushdown在读取数据的过程中尽早应用过滤条件因此只读取sepal_length 5的行。投影下推Projection pushdown在读取数据的过程中只挑选查询真正需要的列从而省去加载额外列例如本查询用不到的petal_length、petal_width。Lazy API 示例延迟到 collectq ( pl.scan_csv(docs/assets/data/iris.csv) .filter(pl.col(sepal_length) 5) .group_by(species) .agg(pl.col(sepal_width).mean()) ) df q.collect()注意这里的差别入口从pl.read_csv换成了pl.scan_csv返回的不再是DataFrame而是一个惰性的LazyFrame查询被存储为q此时尚未执行一旦查询定义完毕调用collect才会真正执行它并返回一个普通的DataFrame。这两项下推优化会显著降低内存与 CPU 的负载既能让你在同样的内存里装下更大的数据集也能让处理过程更快。这也是为什么在处理大型数据时惰性查询几乎是必选方案。源码证据这些优化到底在哪里发生“查询规划器自动优化”并非黑盒魔法在仓库中能找到明确的实现位置。查询计划优化发生在 Rust 核心 cratepolars-plan中crates/polars-plan/src/plans/optimizer/mod.rs 是优化器的“调度中枢”其中optimize函数按固定顺序依次调度各类优化 pass谓词下推由 crates/polars-plan/src/plans/optimizer/predicate_pushdown/mod.rs见PredicatePushDown结构体位于该文件第 21 行实现投影下推由 crates/polars-plan/src/plans/optimizer/projection_pushdown/mod.rs见第 40 行的projection_pushdown函数实现。从 optimizer/mod.rs 可以看到一个典型的执行序列slice 下推slice pushdown→谓词下推→ 公共子表达式消除CSE→ 排序折叠sort collapse→ 连接顺序优化join order→投影下推。其中源码注释明确指出谓词下推“Should be run before projection pushdown”应在投影下推之前运行以便只用于过滤条件的列能尽早被丢弃。Eager API 背后的真相底层同样经过优化文档中有一条值得注意的提示Eager API在很多情况下eager API 其实在底层会调用 lazy API并立即collect结果。这样做的好处是查询内部由查询规划器所做的优化依然可以生效。这意味着即使你只使用 eager API例如read_csv后直接链式调用filter、group_by查询内部的局部优化仍会执行——只是无法像完整的惰性查询那样跨整条链路做全局优化。这与 Rust 侧 optimizer/mod.rs 的注释“Dont run optimizations that dont make sense on a single node. This keeps eager execution more snappy.”不要在单一节点上运行无意义的优化以保持 eager 执行更轻快相印证。何时该用 eager何时该用 lazy文档给出的判断标准非常直接一般而言应优先使用 lazy API——除非你对中间结果本身感兴趣或者正处于探索性工作阶段还不清楚最终查询会长什么样。两类典型例外场景关心中间结果需要逐步观察filter、group_by每一步产出的数据内容探索性分析还在摸索数据形态、不确定下一步该做什么此时即时反馈比整体性能更重要。一旦查询形态基本稳定、需要反复执行或面对较大数据量就应切换为 Lazy API。用 explain 预览查询计划Lazy API 的一大价值是可观测性你可以在真正执行之前先看清 Polars 打算怎么跑。在 Lazy API 中可以调用explain让 Polars 生成一段“查询计划”描述——即一旦你collect结果将被执行的计划文本。如果你想了解 Polars 会对自己的查询做哪些类型的优化这个功能尤其有用。对上面定义的惰性查询q可以这样调用print(q.explain())从输出可以立刻看到Polars 确实执行了谓词下推——它只会读取sepal_length大于 5 的那些行同时它也确实执行了投影下推——只读取查询真正需要的列。换言之你在屏幕上看到的查询计划已经是被优化器重写过、与collect时实际执行路径一致的计划。在 Python 侧explain定义于 py-polars/src/polars/lazyframe/frame.py其签名包含几个常用参数format可选plain或tree控制逻辑计划的展示形式默认plainoptimized默认True返回优化后的查询计划设为False可查看未优化的原始计划便于对比优化前后差异optimizations可通过QueryOptFlags精细地开启/关闭各项优化验证某一项优化对计划的影响。进阶用法explain 查看表达式展开结果explain还有一个实用场景——在给定 schema 的上下文中查看**表达式展开expression expansion**会如何演化。这里的背景是 Polars 的“表达式展开”能力当表达式里使用pl.col(pl.Float64)这类按数据类型选取列的写法时Polars 会在编译期把它展开成对 schema 中所有匹配列的一一操作。详见 Concepts 章节的 expressions-and-contexts.md 中关于 expression expansion 的讨论。考虑下面这个来自 lazy-vs-eager.py 的示例表达式(pl.col(pl.Float64) * 1.1).name.suffix(*1.1)它的含义是对 schema 中每一个Float64类型的列都乘以1.1并把结果列名加上*1.1后缀。由于这里还没有任何真实数据我们可以构造一个任意的 schema再用explain观察该表达式会如何展开求值schema pl.Schema( { int_1: pl.Int16, int_2: pl.Int32, float_1: pl.Float64, float_2: pl.Float64, float_3: pl.Float64, } ) print( pl.LazyFrame(schemaschema) .select((pl.col(pl.Float64) * 1.1).name.suffix(*1.1)) .explain() )注意一个细节这里没有调用read_csv/scan_csv而是直接用pl.LazyFrame(schemaschema)从纯 schema构造了一个惰性查询。explain会基于该 schema 告诉我们这个select最终会被展开成哪些具体列float_1、float_2、float_3的运算。这正是“不执行查询也能检查查询正确性”的绝佳示例——你可以在数据真正流入之前就确认表达式展开是否符合预期。collect 与更多延伸内容当你对explain展示的计划满意后就可以通过collect真正物化结果df q.collect()从 Python API 定义frame.py可以看到collect的作用是“把这个LazyFrame物化为一个DataFrame”默认开启全部查询优化并支持通过engine如流式引擎与optimizations参数做进一步控制。如果希望更系统地掌握 Lazy API官方文档提供了专门的一章可在仓库的 docs/source/user-guide/lazy/index.md 找到入口推荐按以下顺序阅读使用 LazyFrameusing.md从文件scan_csv等pl.scan_*系列或既有 DataFrame.lazy()构造惰性查询查询优化optimizations.md系统梳理规划器各类优化Schema 管理schemas.md惰性查询可以在处理数据前先捕获 schema 错误查询计划query-plan.md深入理解explain展示的物理/逻辑计划执行execution.mdcollect、fetch等执行方式细节数据源与输出端sources_sinks.md 以及 Concepts 章节中的 流式执行streaming.md了解如何处理超内存数据集。小结你要做的事推荐 API原因正式处理较大数据集Lazyscan_csvcollect谓词/投影下推等全局优化省内存省 CPU探索数据、观察中间结果Eagerread_csv等每步即时反馈便于快速迭代想确认优化是否生效Lazy explain查看优化后的查询计划逐项验证一句话总结把查询描述得越完整再执行Polars 越有机会把它优化得越快——这正是 Lazy API 的核心设计哲学。结合explain的观察手段你可以让自己的惰性查询既快又可验证。【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考