性能优化
前面我们简单提到过 lazy 和 eager 执行模式的区别。本页将展示如何利用 lazy API 获得显著的性能收益。
Lazy vs Eager
Polars 支持两种运行模式:lazy 和 eager。
在 eager API 中,查询会立刻执行;而在 lazy API 中,查 询只有在真正“需要”的时候才会被执行。将执行推迟到最后一刻,往往能带来明显的性能优势,因此在大多数非交互式场景下,lazy API 更值得优先使用。
示例
我们会沿用前一页中的示例,通过对比展示使用 lazy API 带来的性能收益。下面的代码会统计来自 archive.org 的上传次数。
Eager
import polars as pl
import datetime
df = pl.read_csv("hf://datasets/commoncrawl/statistics/tlds.csv", try_parse_dates=True)
df = df.select("suffix", "crawl", "date", "tld", "pages", "domains")
df = df.filter(
(pl.col("date") >= datetime.date(2020, 1, 1)) |
pl.col("crawl").str.contains("CC")
)
df = df.with_columns(
(pl.col("pages") / pl.col("domains")).alias("pages_per_domain")
)
df = df.group_by("tld", "date").agg(
pl.col("pages").sum(),
pl.col("domains").sum(),
)
df = df.group_by("tld").agg(
pl.col("date").unique().count().alias("number_of_scrapes"),
pl.col("domains").mean().alias("avg_number_of_domains"),
pl.col("pages").sort_by("date").pct_change().mean().alias("avg_page_growth_rate"),
).sort("avg_number_of_domains", descending=True).head(10)
Lazy
import polars as pl
import datetime
lf = (
pl.scan_csv("hf://datasets/commoncrawl/statistics/tlds.csv", try_parse_dates=True)
.filter(
(pl.col("date") >= datetime.date(2020, 1, 1)) |
pl.col("crawl").str.contains("CC")
).with_columns(
(pl.col("pages") / pl.col("domains")).alias("pages_per_domain")
).group_by("tld", "date").agg(
pl.col("pages").sum(),
pl.col("domains").sum(),
).group_by("tld").agg(
pl.col("date").unique().count().alias("number_of_scrapes"),
pl.col("domains").mean().alias("avg_number_of_domains"),
pl.col("pages").sort_by("date").pct_change().mean().alias("avg_page_growth_rate"),
).sort("avg_number_of_domains", descending=True).head(10)
)
df = lf.collect()
耗时对比
在一台普通笔记本电脑(家用网络环境)上运行这两段查询,得到的耗时大致如下:
- Eager:约
1.96秒 - Lazy:约
410毫秒
可以看到,lazy 查询大约比 eager 查询快了 5 倍。其原因在于查询优化器:如果我们把 collect 数据集的操作延迟到最后一步,Polars 就能推断出真正需要哪些列、哪些行,并尽可能在读取数据的早期阶段就应用过滤条件。
对于 Parquet 这类带有元数据(例如某个行组中的最小值、最大值)的文件格式,差距可能会更大,因为 Polars 可以根据过滤条件和元数据,跳过整个行组,而无需将这些数据通过网络读取回来。