Chapter 07

Lazy API 与查询优化

Polars 最强大的部分:LazyFrame 构建查询计划,优化器做谓词/投影下推,collect 一次性执行,streaming 处理大于内存的数据。

7.1 为什么要惰性

Eager 模式下,每一步都立刻执行、物化中间结果。假设你从一个 100 列、上亿行的 Parquet 里只想要"某地区 2 列的汇总",Eager 会先把全部读进内存再过滤——浪费巨大。Lazy 则先记录你要做什么,交给优化器统筹后,只读需要的列、只扫需要的行。

LazyFrame
惰性的 DataFrame。所有操作(filter/select/join...)只是往查询计划里加节点,不真正计算。由 df.lazy()scan_* 得到。
查询计划(Query Plan)
一棵描述"如何得到结果"的操作树。优化器会重写这棵树,使其等价但更高效。
collect()
触发实际执行的方法。此刻优化器介入、生成物理计划、多线程跑完,返回一个真正的 DataFrame。
谓词下推(Predicate Pushdown)
filter 条件尽量"下推"到数据读取阶段,读文件时就跳过不满足的行/行组,大幅减少 IO。
投影下推(Projection Pushdown)
把"只需要哪些列"下推到读取阶段,从磁盘只读用到的列。列式格式(Parquet)配合它效果拔群。
公共子表达式消除(CSE)
识别重复计算的子表达式只算一次,结果复用。
streaming(流式引擎)
分批(chunk)执行查询,内存里只保留一小部分数据,从而处理大于内存的数据集。

7.2 scan_* 惰性读取

import polars as pl

# scan 不读数据,只拿到 schema 与 LazyFrame
lf = pl.scan_parquet("huge.parquet")

result = (
    lf
    .filter(pl.col("region") == "华东")   # 会被下推
    .group_by("category")
    .agg(pl.col("sales").sum())
    .collect()                          # 此刻才真正读+算
)

# 也可 scan 一批文件(通配符)
lf = pl.scan_csv("logs/2026-*.csv")

7.3 查看查询计划:explain

lf = (
    pl.scan_parquet("huge.parquet")
      .filter(pl.col("region") == "华东")
      .select(["category", "sales"])
)

# 优化后的计划:可以看到 PROJECT 2/100 columns、SELECTION 已下推
print(lf.explain())

# 看未优化的原始计划做对比
print(lf.explain(optimized=False))

# 图形化(需 graphviz)
lf.show_graph()
怎么读 explain

输出里 PROJECT 2/100 columns 表示投影下推生效,只读 2 列;SELECTION: [col("region") == "华东"] 出现在 Parquet SCAN 节点里,说明谓词已下推到读取层。看到这些就说明优化器在替你省钱。

7.4 collect 与 streaming

# 普通 collect(数据能装进内存)
df = lf.collect()

# 流式执行:处理大于内存的数据
df = lf.collect(engine="streaming")

# 结果太大,别落回内存 → 直接流式写盘
lf.sink_parquet("out.parquet")
lf.sink_csv("out.csv")
sink_ 的威力

sink_parquet 把整条惰性管道边算边写盘,全程内存占用极低。做"读 TB 级原始数据 → 清洗 → 落地"的 ETL,这是首选,不必 collect 到内存。

7.5 Eager 与 Lazy 互转

# Eager → Lazy
lf = df.lazy()

# Lazy → Eager(执行)
df = lf.collect()

# 只想看前几行验证管道对不对,用 fetch/head 省时
df.lazy().filter(...).head(5).collect()

7.6 常见误区

不要在 Lazy 管道里反复 collect

collect 一次就物化并中断优化。把整条链一路写到底、最后只 collect 一次,优化器才能看到全貌做下推。中途 collect 等于把优化拦腰截断。

map_elements 破坏优化

Python 逐行回调 map_elements 是黑盒,优化器无法下推、无法并行,还会拖慢十几倍。能用原生表达式(when/then、str/dt 命名空间)就绝不用它。

7.7 小结与下一步