Polars SQL SELECT 语句实战指南:查询、聚合、排序、JOIN 与表函数
【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars
Polars 是使用 Rust 编写的高性能 DataFrame 查询引擎,其内置的 SQL 接口(polars-sqlcrate)允许开发者直接在SQLContext上执行标准 SQL 语句并把结果转换为DataFrame/LazyFrame。本文以官方文档 docs/source/user-guide/sql/select.md 为骨架,结合仓库源码,系统讲解 Polars SQL 中SELECT的完整用法:基础投影、GROUP BY聚合、ORDER BY排序、多表JOIN、SQL 内置函数与read_csv/read_parquet等表函数。读完本文,你将能够在 Polars 项目中用一条 SQL 完成原本需要多行 DataFrame 表达式才能实现的取数、聚合与多源读取。
1. 从 DataFrame 到 SQLContext:SELECT 的入口
在 Polars SQL 中,SELECT语句用于从已注册的表(本质上是LazyFrame)中检索数据。其基础语法与标准 SQL 一致:
SELECT column1, column2, ... FROM table_name;其中column1, column2, ...是你想选取的列,也可以用通配符*选取全部列;table_name是注册在SQLContext中的表名。
要在 SQL 中查询一个 Python 侧的DataFrame,需要先把它注册进SQLContext。官方示例(见 docs/source/src/python/user-guide/sql/select.py 的df片段)如下:
import polars as pl df = pl.DataFrame( { "country": ["USA", "USA", "USA", "USA", "USA", "Netherlands"], "city": [ "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Amsterdam", ], "population": [8399000, 3997000, 2705000, 2320000, 1680000, 900000], } ) ctx = pl.SQLContext(population=df, eager=True) print(ctx.execute("SELECT * FROM population"))这里的关键点在于:
pl.SQLContext(population=df, eager=True)以关键字参数形式把DataFrame df注册为名为population的表;eager=True表示execute()返回的查询结果是一个急切求值的DataFrame(而非惰性的LazyFrame),便于直接打印查看。
从源码看,SQLContext是polars-sqlcrate 的核心入口(crates/polars-sql/src/context.rs)。它内部维护了一张table_map: Arc<RwLock<PlHashMap<String, LazyFrame>>>,把表名映射到LazyFrame——也就是说,注册进 SQL 的 DataFrame 在底层是以惰性框架形式被引用的。Rust 侧的等价操作是:
use polars_sql::SQLContext; use polars_core::prelude::*; use polars_lazy::prelude::*; let mut ctx = SQLContext::new(); let df = df! { "a" => [1, 2, 3], }.unwrap(); ctx.register("df", df.clone().lazy()); let sql_df = ctx.execute("SELECT * FROM df").unwrap().collect().unwrap(); assert!(sql_df.equals(&df));(该示例即源码中register与execute的 doc 测试,见 crates/polars-sql/src/context.rs。)
1.1 注册 / 注销表
SQLContext在 Rust 侧暴露了完整的表管理接口:
ctx.register(name, lf):将LazyFrame以指定名称注册为表(context.rs 中register实现为向table_map插入一条映射);ctx.register_many(...):一次注册多张表(官方 SQL 示例中注册income表即使用该方法);ctx.unregister(name):按名称移除表;ctx.get_tables():返回当前已注册表名的排序列表。
Python 侧在 py-polars/src/polars/_plr.pyi 中对应暴露了SQLContext.execute(query) -> PyLazyFrame与SQLContext.register(name, lf)等接口。
1.2 惰性还是急切:execute 的底层行为
execute()(crates/polars-sql/src/context.rs)会先解析单条 SQL 语句(parse_single_statement),然后通过can_defer判断是否可以延迟到 DSL → IR 转换阶段再解析。从源码注释看,只要查询是只读的且不涉及用户自定义函数注册,就会走defer_query路径,构建一个DslPlan::SQL惰性计划;只有查询引用了已注册关系或需要用户自定义函数时才会急切执行。这意味着 Polars SQL 的SELECT查询默认可以无缝参与惰性查询优化(谓词下推、投影下推等),与 DataFrame API 的延迟计算模型一致。
2. 分组聚合:GROUP BY
GROUP BY用于按一个或多个列对表中的行分组,并在每个组上计算聚合函数。官方示例(select.py的group_by片段):
result = ctx.execute( """ SELECT country, AVG(population) as avg_population FROM population GROUP BY country """ ) print(result)输出结果形如:
shape: (2, 2) ┌─────────────┬────────────────┐ │ country ┆ avg_population │ ╞═════════════╪════════════════╡ │ USA ┆ 3819800.0 │ │ Netherlands ┆ 900000.0 │ └─────────────┴────────────────┘要点解析:
AVG(population) as avg_population为聚合结果列起了别名avg_population;GROUP BY country后,未出现在GROUP BY中的普通列不能再直接SELECT(标准 SQL 语义),只能选择分组列与聚合表达式;- Polars SQL 支持的聚合函数包括
SUM、AVG、MIN、MAX、COUNT、STDDEV、FIRST等(详见第 4 节)。相关聚合实现与测试可参考 crates/polars-sql/tests/functions_aggregate.rs 与 crates/polars-sql/tests/functions_cumulative.rs。
3. 结果排序:ORDER BY
ORDER BY用于按一个或多个列对查询结果进行升序(ASC,默认)或降序(DESC)排序。官方示例(select.py的orderby片段):
result = ctx.execute( """ SELECT city, population FROM population ORDER BY population """ ) print(result)输出为按population升序排列的城市人口列表:
shape: (6, 2) ┌─────────────┬────────────┐ │ city ┆ population │ ╞═════════════╪════════════╡ │ Amsterdam ┆ 900000 │ │ Phoenix ┆ 1680000 │ │ Houston ┆ 2320000 │ │ Chicago ┆ 2705000 │ │ Los Angeles ┆ 3997000 │ │ New York ┆ 8399000 │ └─────────────┴────────────┘也可以指定多列与方向,例如ORDER BY country ASC, population DESC。Polars SQL 的ORDER BY通过sql_expr模块中的order_by_sort_options转换为排序表达式(crates/polars-sql/src/sql_expr.rs),最终映射到底层LazyFrame的sort逻辑。
4. 多表关联:JOIN
SELECT可以与JOIN组合,把多张注册表按关联键合并。官方示例(select.py的join片段)先注册第二张表income,再进行左连接:
income = pl.DataFrame( { "country": [ "USA", "USA", "USA", "USA", "Netherlands", "Netherlands", "Netherlands", ], "city": [ "New York", "Los Angeles", "Chicago", "Houston", "Amsterdam", "Rotterdam", "Utrecht", ], "income": [55000, 62000, 48000, 52000, 42000, 38000, 41000], } ) ctx.register_many(income=income) result = ctx.execute( """ SELECT income.*, population.population FROM population LEFT JOIN income ON population.city = income.city """ ) print(result)输出结果:
shape: (6, 4) ┌─────────────┬────────────┬──────────────┬────────────┐ │ country ┆ city ┆ income ┆ population│ ╞═════════════╪════════════╪══════════════╪════════════╡ │ USA ┆ New York ┆ 55000 ┆ 8399000 │ │ USA ┆ Los Angeles┆ 62000 ┆ 3997000 │ │ USA ┆ Chicago ┆ 48000 ┆ 2705000 │ │ USA ┆ Houston ┆ 52000 ┆ 2320000 │ │ USA ┆ Phoenix ┆ null ┆ 1680000 │ │ Netherlands ┆ Amsterdam ┆ 42000 ┆ 900000 │ └─────────────┴────────────┴──────────────┴────────────┘要点解析:
SELECT income.*, population.population:income.*是限定通配符,只展开income表的所有列;再显式补上population表的population列;LEFT JOIN ... ON population.city = income.city:以population为左表按city关联。Phoenix在income中没有对应行,因此income侧出现null;ctx.register_many(income=income)一次注册多张表;- Polars SQL 还支持
INNER、RIGHT、FULL等连接类型(见 crates/polars-sql/src/keywords.rs 中的JOIN、LEFT、RIGHT、FULL、INNER、OUTER、SEMI、ANTI等关键字)。连接相关实现位于 crates/polars-sql/src/context.rs(JoinCoalesce、MaintainOrderJoin等),框架层支持semi_anti_joinfeature 等高级连接。
从源码结构看,多表查询中若多个表存在同名列,Polars SQL 提供了限定通配符消歧机制(disambiguate_projection_cols函数,crates/polars-sql/src/context.rs):当tbl1.*与tbl2.*展开后列名冲突时,会用列名:表名的后缀形式避免歧义。
5. SQL 内置函数
Polars SQL 提供了种类丰富的内置函数,官方文档列举了以下几大类(完整清单见源码 crates/polars-sql/src/functions.rs 与 crates/polars-sql/src/keywords.rs):
| 类别 | 函数示例 |
|---|---|
| 数学函数 | ABS、EXP、LOG、ASIN、ACOS、ATAN等 |
| 字符串函数 | LOWER、UPPER、LTRIM、RTRIM、STARTS_WITH、ENDS_WITH |
| 聚合函数 | SUM、AVG、MIN、MAX、COUNT、STDDEV、FIRST等 |
| 数组函数 | EXPLODE、UNNEST、ARRAY_SUM、ARRAY_REVERSE等 |
从源码可以看到这些函数在PolarsSQLFunctions枚举中的定义与 SQL 文档注释,例如:
- 字符串前缀匹配:
SELECT STARTS_WITH(col1, 'a') FROM df;(StartsWith变体,functions.rs); - 字符串后缀匹配:
SELECT ENDS_WITH(col1, 'a') FROM df;(EndsWith变体); - 数组求和:
SELECT ARRAY_SUM(col1) FROM df;(ArraySum变体); - 数组逆序:
SELECT ARRAY_REVERSE(col1) FROM df;(ArrayReverse变体); - 展开数组为多行:
SELECT UNNEST(col1) FROM df;与EXPLODE(Explode变体,底层实现为e.explode(ExplodeOptions { empty_as_null: true, keep_nulls: true, .. }))。
官方示例(select.py的functions片段)演示了在WHERE中使用字符串函数:
result = ctx.execute( """ SELECT city, population FROM population WHERE STARTS_WITH(country,'U') """ ) print(result)输出只保留country以U开头的城市(即全部美国城市):
shape: (5, 2) ┌─────────────┬────────────┐ │ city ┆ population │ ╞═════════════╪════════════╡ │ New York ┆ 8399000 │ │ Los Angeles ┆ 3997000 │ │ Chicago ┆ 2705000 │ │ Houston ┆ 2320000 │ │ Phoenix ┆ 1680000 │ └─────────────┴────────────┘注意:STARTS_WITH/ENDS_WITH也常用于WHERE谓词,实现位于PolarsSQLFunctions的StartsWith/EndsWith变体(functions.rs)。
6. 表函数:直接从文件读取数据
前面所有示例都先把 DataFrame 注册进SQLContext再查询。Polars SQL 还支持在查询中直接读取 CSV、Parquet、JSON 与 IPC 文件,无需预先注册——这就是表函数read_xxx(官方文档中称为 Table Functions)。
官方示例(select.py的tablefunctions片段):
result = ctx.execute( """ SELECT * FROM read_csv('docs/assets/data/iris.csv') """ ) print(result)这会把docs/assets/data/iris.csv直接读成一张表并执行SELECT *。
从源码看,表函数定义于 crates/polars-sql/src/table_functions.rs 的PolarsTableFunctions枚举,目前支持:
| SQL 表函数 | 底层读取器 | 说明 |
|---|---|---|
read_csv('path') | LazyCsvReader | 读取 CSV,自动启用try_parse_dates与missing_is_null(table_functions.rs) |
read_parquet('path') | LazyFrame::scan_parquet | 读取 Parquet |
read_ipc('path') | LazyFrame::scan_ipc | 读取 IPC(Arrow 文件) |
read_json('path') | LazyJsonLineReader | 读取 JSON,目前仅支持 NDJSON(JSON Lines) |
实现要点(crates/polars-sql/src/table_functions.rs):
- 每个表函数只接受一个参数:单引号包裹的文件路径字符串。传入其他形式会报
SQLSyntax错误(源码中使用polars_ensure!(args.len() == 1, ...)校验,且路径必须来自SQLValue::SingleQuotedString); - 返回值为
(PlRefPath, LazyFrame),即文件路径与惰性读取框架,因此FROM read_xxx(...)同样享受惰性扫描优化; - 读取 CSV 时默认开启
try_parse_dates(true)(尝试解析日期列)与missing_is_null(true)(缺失值视为 null)。
因此你可以写出跨文件源的 SQL,例如:
SELECT * FROM read_csv('data/sales.csv') WHERE STARTS_WITH(region, 'EU'); SELECT * FROM read_parquet('data/orders.parquet') ORDER BY order_date DESC;7. 更多 SELECT 变体与深入学习路径
SELECT的常见变体远不止本文所述,Polars SQL 还支持SELECT DISTINCT、WHERE过滤、HAVING、LIMIT/OFFSET、CASE WHEN、UNION/INTERSECT/EXCEPT集合运算、WITH(CTE)、窗口函数(OVER)、* EXCLUDE/RENAME/REPLACE修饰符等(相关关键字见 crates/polars-sql/src/keywords.rs,测试覆盖见 crates/polars-sql/tests/statements.rs 与 crates/polars-sql/tests/simple_exprs.rs)。
如需继续深入,可参考仓库中的以下资源:
- 官方 SQL 用户指南目录:docs/source/user-guide/sql(含
select.md、group_by.md、joins.md等专题); - SQL 完整函数/关键字清单:crates/polars-sql/src/keywords.rs(
all_keywords()与all_functions()); - 函数实现与 SQL 语法示例:crates/polars-sql/src/functions.rs;
- 表函数实现:crates/polars-sql/src/table_functions.rs;
- 核心执行上下文:crates/polars-sql/src/context.rs;
- 测试套件:crates/polars-sql/tests(覆盖聚合、字符串、数学、窗口、累计、UDF、空表函数等场景)。
8. 小结
Polars SQL 的SELECT完整覆盖了关系查询的核心能力:
- 基础投影:
SELECT col1, col2 / *+FROM,通过SQLContext注册表(register/register_many)即可查询; - 分组聚合:
GROUP BY+SUM/AVG/MIN/MAX/COUNT/STDDEV/FIRST等聚合函数; - 排序:
ORDER BY支持多列与ASC/DESC方向; - 多表连接:
LEFT/RIGHT/INNER/FULL JOIN+ON,支持表名.*限定通配符与自动列名消歧; - 内置函数:数学、字符串、聚合、数组四类函数可直接在
SELECT/WHERE中使用; - 表函数:
read_csv/read_parquet/read_ipc/read_json让你免注册直接查询文件,且默认惰性扫描、可参与查询优化。
所有能力在底层都映射为LazyFrame与 Polars 表达式,因此 SQL 写出的查询与 DataFrame API 构建的查询共享同一套优化与执行引擎——这正是 Polars SQL 既能带来 SQL 的声明式便利、又不牺牲性能的原因。
【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考