把 Polars 简化成“更快的 pandas”很容易。这个比较能帮助使用者迅速找到类别,随后也会迅速变成错误的心智模型。理解 Polars 的强方式,是把它看作带有 DataFrame 前端的查询引擎:Python 给用户一套紧凑的表达式语言,Rust 承担执行路径中的相当一部分工作,lazy API 则把一串熟悉的操作转换成一个计划,让数据移动之前先经过优化。[1][2]
这一点重要,是因为分析代码里真正昂贵的部分,很少只是 notebook 中看起来慢的某一行。成本往往来自整条链路意外形成的契约:读取过多列,过早物化,在 join 之后才过滤,重复计算同一个表达式,或者在发现输出只需要很窄的一片数据之前,就把所有内容压进内存。Polars 的架构押注在于:DataFrame 工作流应当像普通代码一样可读,同时像经过规划的查询一样执行。[1][3]
截至 2026-06-13T05:01:00Z UTC,通过 GitHub API 观察到的 pola-rs/polars 仓库数据显示,它拥有 38,745 stars、2,879 forks、2,791 open issues,许可证标签为 MIT,最近一次 push 时间戳为 2026-06-12T17:03:54Z。[5] 最新 GitHub release 为 Python Polars 1.41.2,发布时间是 2026-05-29T17:39:42Z。[6] 这些数字不能证明 Polars 适合某个工作负载,但它们说明这是一项活跃的基础设施项目,已经超出冻结状态下的 benchmark 工程。
图片语境:封面是 Wikimedia Commons 上一张真实的 2012 年 Wikimedia 服务器机房照片。[8] 它虽非 Polars 专属图片,却与本文论点契合,因为文章讨论的是执行纪律:查询计划、流式批次、并行工作,以及在数据路径可见时更容易推理的内存边界。
LazyFrames 改变工作单元
Polars 同时支持 eager 与 lazy 操作,但用户指南对偏好说得很直接:lazy API 通常是更好的路径,因为它让 Polars 端到端处理完整查询,避免逐行执行。[1] LazyFrame 的含义超出保存若干行并等待稍后调用。参考文档把它描述为针对 DataFrame 的惰性计算图或查询表示,由此启用全查询优化与并行能力。[2]
这是第一道真实边界。在 eager 模式下,每个方法调用在下一行运行之前已经完成。在 lazy 模式下,整条链路保持可协商状态,直到 collect() 请求物化。关于 LazyFrames 的独立 walkthrough 也给出了同样的实践区分:scan 会先建立指令,而 .collect() 是结果成为物化 DataFrame 的位置。[10] 这给引擎留下空间去提出实际问题:哪些列真正被需要,哪些谓词能够提前移动,哪些表达式可以简化,哪些分支可以共享工作,哪个源 scan 可以避开多余 I/O。[1][3]
采用 Polars 时的经验很清楚:如果团队评估 Polars 时只迁移 pandas 形状的 eager 片段,就会错过架构层面。值得追问的重点落在工作负载能否在执行前表达成计划,而不只看孤立的 group_by 速度。CSV、Parquet 与 IPC scan 在输入 lazy graph 时更有价值,因为引擎能够在加载那些随后会消失的数据之前,先推理 projection 与 predicate pushdown。[1][3]
这也是 Polars 在 ETL、特征生成、本地 lakehouse 工作、notebook 到 job 的迁移,以及分析型命令行工具中往往显得最连贯的原因。这些工作负载早已超出表格本身。它们是包含 scan、filter、join、grouping、派生列与输出步骤的 pipeline。DataFrame API 让表层使用体验顺手;查询计划则防止这个表层变成意外劳动。
表达式是契约语言
Polars 的表达式系统是第二道架构边界。概念指南把表达式置于核心位置,用户在 select、with_columns、filter 等 context 中,正是通过表达式描述转换。[7] 这一点重要,是因为表达式没有藏在引擎视野之外成为任意 Python callback。它们是结构化操作,引擎能够检查并组合。
这个区分很容易被忽略。在小脚本里,一个 Python 函数会显得比表达式树灵活。到了规模化场景,这种灵活会变成不透明。如果引擎看不到转换,它就无法重新排序,无法下推,无法与相关工作合并,也无法在执行前推理类型行为。Polars 的表达式模型要求用户放弃一部分自由形态的 Python 便利,换取引擎能够优化的操作。
回报会出现在普通代码里。派生列可以定义一次并复用。filter 可以继续附着在计划上,避免提前变成已经物化的布尔 mask。select 可以告诉 scanner,当前工作负载只需要三列,读取范围不扩展到整个文件。group aggregation 可以作为同一张 graph 的一部分接受规划,脱离 Python 侧孤立循环的形态。[1][3][7]
失效路径也很清晰。如果团队持续使用逐行 Python 函数,把每个中间结果都当成需要 eager 检查的对象,或者为了迁就另一套库的习惯而打断 graph,Polars 就失去一部分存在理由。工具仍然可以很快,但架构性会被削弱。最强的 Polars 代码,会尽量把意图留在表达式之内,直到真实的输出边界出现。
优化器 pass 有可见的工作方式
查询计划指南有用,是因为它鼓励用户检查计划,避开含混的性能传闻。Polars 暴露了 explain() 等方法,让用户可以查看 logical plan,并理解引擎如何重组工作。[3] 优化文档列出了让 lazy execution 产生价值的若干 pass,包括 projection pushdown、predicate pushdown、slice pushdown、common subplan elimination、common subexpression elimination 与 expression simplification。[4]
这些术语有实际执行含义。Projection pushdown 决定读取宽数据集,或只读取结果真正使用的列。Predicate pushdown 决定过滤发生在昂贵工作之后,或在条件允许时靠近源头。Common subexpression elimination 决定同一个派生计算被重复执行,或交给引擎复用。Slice pushdown 决定先完整物化再限制,或让更早阶段少做工作。[4]
在这里,Polars 与纯便利型库的定位拉开距离。它给数据团队提供了一个性能检查表面。如果某个查询慢得出人意料,问题可以从“哪一行看起来可疑”转向“引擎构造了什么计划”。这不会消除调优工作,却会让调优少一些玄学。
它也为预期划出边界。优化器很强,但不能证明每一种查询形状都很好。糟糕的 join key、高基数 grouping、昂贵的字符串工作、严重倾斜以及草率的物化,仍然会主导成本。更健康的姿态是编写能给优化器提供良好 graph 的 Polars 代码,在影响较大的场景中检查计划,然后用有代表性的数据测量。
Streaming 是内存边界
Streaming 指南给了 Polars 另一条重要路径:lazy queries 可以按批次执行,因此结果不要求整个数据集一次性留在内存中。[9] 文档描述了向 collect() 传入 engine="streaming" 来启用这种模式,并把它定位为处理不适合舒适地放入内存的数据集的方法。[9]
streaming 的含义,不在于把一台 laptop 变成 warehouse。它意味着执行边界发生变化。能够 streaming 的工作负载,可以通过让批次经过受支持的 operator 来降低内存压力。需要全局状态、依赖不受支持的操作,或者要求输出范围物化的工作负载,仍然会需要内存、不同的查询形状,或者数据库引擎。这里的重点,是 lazy planning 给引擎提供一种执行模式,让内存成为可管理约束,降低突然崩溃的概率。[7]
对生产使用来说,这往往是最实际的测试。如果 Polars 正在文件上运行日常 job,团队需要知道哪些查询会 stream,哪些不会,以及 fallback 形态是什么。团队还应知道源格式是否支持高效 scanning,中间物化是否有意发生,最终的 collect() 是否处在正确边界上。样本能跑通还不够,因为样本会遮住内存形状。
Polars 适合的位置
当团队需要本地或嵌入式分析执行,同时希望获得干净的表达式语言和查询计划主干时,Polars 最有力量。它适合 Python 占比较高的数据工作,其中瓶颈不只来自原始 CPU,也来自多余物化、过度读取、重复转换和脆弱的 notebook 逻辑。它适合愿意把 LazyFrame pipeline 当成可 review artifact 的工程师:检查计划,把转换留在表达式中,让引擎能看见它们。[1][2][3][7]
当组织真正需要共享事务数据库、多用户治理层、长期运行的 serving system,或分布式 warehouse 时,Polars 的优势会变弱。Polars 可以读取和写入严肃的存储格式,但它不承担 catalog、scheduler、access-control plane 或 semantic layer 的角色。如果团队需要这些能力,Polars 应当位于更大的架构之内,避免假扮成整个架构。
最佳 pilot 应避开笼统的“替换 pandas”。选择一个数据路径清晰的 job:scan、filter、derive、join、aggregate、write。用 lazy 方式构建。检查计划。用有代表性的行数和文件布局测试。只在查询形状支持时尝试 streaming。保留一次与现有实现的对比运行,目标放在确认新代码更清楚地呈现工作发生的位置,benchmark 奖杯不应成为目标。
Polars 的真正承诺,不在于每个 DataFrame 都会因为关联到它而变快,而在于 DataFrame 工作可以从一串即时副作用,转变为一个明确计划。这个转变一旦发生,性能更容易解释,内存压力更容易定位,分析代码也会成为 reviewer 能在 job 运行前推理的对象。
Sources
- Polars 用户指南,“Using the lazy API” - lazy execution、automatic query optimization、streaming 与 schema checking。
- Polars Python API 参考,“LazyFrame” - lazy computation graph、whole-query optimization 与 parallelism。
- Polars 用户指南,“Query plan” - plan inspection 与
explain()workflow。 - Polars 用户指南,“Optimizations” - projection、predicate、slice、common-subplan 与 common-expression optimization 等 optimizer pass。
- GitHub API,
pola-rs/polars仓库 metadata,采样于 2026-06-13 - stars、forks、open issues、license label 与 push timestamp。 - GitHub,
pola-rs/polarsPython Polars 1.41.2 release - freshness check 使用的 latest release tag 与 publication timestamp。 - Polars 用户指南,“Expressions and contexts” - expression model,以及 expressions 定义 transformation 的 contexts。
- Wikimedia Commons,“File:Wikimedia Servers-0051 19.jpg” - 文章图片源使用的真实 2012 年 Wikimedia server-room photograph。
- Polars 用户指南,“Streaming” - 通过
engine="streaming"进行 batch execution,以及 larger-than-memory execution boundary。 - Real Python,“How to Work With Polars LazyFrames” - 关于 LazyFrames、scans 与使用
.collect()进行 materialization 的独立实践说明。