事实核验:2026-08-16 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用范围与待维护事项
本文面向已掌握 Python 函数、链式调用及 SQL 聚合与连接的读者,示例限于内存教学数据和本地 DuckDB 后端。运行前需准备 Ibis 的 DuckDB 后端及 memtable 所需依赖;原文未提供经过测试的 Ibis、DuckDB 版本组合,本轮也未运行代码。文中预期结果仅针对给定样例,不代表真实订单的 GMV 口径;迁移其他后端时仍须验证类型、空值、连接基数及窗口排序,交互预览可能触发查询。
Ibis表达式与延迟执行
abstract 学习目标
理解 Ibis 表达式不是内存数据,而是可组合的查询计划;能用 filter、mutate、select、group_by、join 构建逻辑,并明确 compile、execute 与预览的边界。
Ibis 的核心价值是用 Python 表达数据逻辑,再交给后端执行。它不是另一套 Pandas,也不是数据库。学习前先阅读 Ibis学习路线图;本地后端可配合 DuckDB学习路线图。
1. 表达式是什么
官方入门教程说明,Ibis 默认延迟执行:取得表和连续调用方法时,通常只构建表达式并显示 schema;后端计算在 execute()、to_pandas() 等动作发生时才执行。
import ibis
from ibis import _
orders = ibis.memtable(
{
"order_id": [1, 2, 3],
"status": ["paid", "cancelled", "paid"],
"amount": [99.0, 30.0, 120.0],
}
)
expr = (
orders
.filter(_.status == "paid")
.mutate(amount_with_tax=_.amount * 1.06)
.select("order_id", "amount", "amount_with_tax")
)
上例中的 _ 是 Ibis deferred 占位符。执行前,expr 表示一棵有 schema 的操作树。若当前版本或团队规范不使用 _,也可写 orders.status、orders.amount,但长链中要注意引用的是哪一阶段的表。
2. 四种常用表达式
filter:按布尔条件保留行。select:选择或计算输出列,未选择的列会被丢弃。mutate:保留现有列,并新增或替换列。group_by(...).agg(...):按组汇总,改变结果粒度。
daily = (
orders
.filter(_.status == "paid")
.group_by(_.status)
.agg(
orders=_.order_id.nunique(),
gmv=_.amount.sum(),
avg_amount=_.amount.mean(),
)
)
预期结果是一行:status=paid、orders=2、gmv=219.0、avg_amount=109.5。聚合前必须先定义结果粒度;否则新增维度会改变行数和指标含义。
3. 编译与执行是两件事
连接后端后,可以先编译再执行:
con = ibis.duckdb.connect()
bound = con.create_table("orders", orders, overwrite=True)
expr = bound.filter(_.status == "paid").group_by(_.status).agg(gmv=_.amount.sum())
print(ibis.to_sql(expr))
result = expr.execute()
ibis.to_sql 用于检查生成的 SQL;execute 把计划交给后端并取回结果。编译成功只能证明该表达式能生成目标方言,不能证明生产数据类型、权限、资源和结果口径正确。
warning 开启 `ibis.options.interactive = True` 会为了展示结果而部分执行表达式。Notebook 里打印对象因此可能产生真实查询;生产调试不要把“查看变量”当成无副作用操作。
4. Join 的语义与验证
customers = ibis.memtable(
{"customer_id": [10, 20], "segment": ["new", "vip"]}
)
orders2 = ibis.memtable(
{"order_id": [1, 2, 3], "customer_id": [10, 20, 20], "amount": [9, 20, 30]}
)
joined = orders2.left_join(customers, "customer_id")
result = joined.group_by(_.segment).agg(gmv=_.amount.sum()).execute()
预期 new=9、vip=50。迁移到真实后端前,要分别检查右表键唯一性、连接前后行数、空键比例与未匹配比例。Ibis 统一 API 不会自动纠正错误的数据关系。
5. 空值、类型与窗口边界
表达式在构建时会做类型检查,但各后端对字符串、时间、时区、浮点、整数除法和 NULL 的支持仍可能不同。不要用 Python 的直觉推断 SQL 三值逻辑。重要逻辑应准备包含 NULL、零值、负数、重复键和边界日期的小数据集。
窗口计算应按业务粒度选择分区;全表窗口可以不分区,并非所有窗口计算都需要排序。lag、累计值或按排名取 top-N 等依赖顺序的计算,应明确排序规则;若排序键存在并列,还需按业务语义处理并列或补充确定顺序的键,否则结果可能在不同执行中变化。相关 SQL 基础见 SQL学习路线图 与 AI时代分析SQL实战。
6. 可复用表达式函数
业务函数应接收表表达式并返回表达式,不要在函数内部立即 .execute():
def paid_orders(table):
return table.filter(_.status == "paid")
def order_metrics(table):
return table.aggregate(
orders=_.order_id.nunique(),
gmv=_.amount.sum(),
)
metrics_expr = orders.pipe(paid_orders).pipe(order_metrics)
这样函数可以继续组合、编译、替换后端和测试。执行边界由最外层调用者控制,避免工具函数偷偷发起昂贵查询。
7. 练习与验收
- 用 memtable 构造包含 NULL 与重复订单的数据,写出 GMV 口径断言。
- 分别查看表达式、生成 SQL 和执行结果,说明三个阶段的差异。
- 开启交互模式观察预览,再关闭它并确认不会因打印而执行。
- 写一个返回表达式的清洗函数,并在 DuckDB 后端上验证结果。
- 为 join 写键唯一、行数和未匹配率三类检查。
8. 延伸阅读
- 后端连接与数据读写
- 跨后端迁移与测试
- DuckDB直接查询文件
- DuckDB Python集成
- DuckDB性能优化
- Polars学习路线图
- Polars表达式
- Polars Lazy执行
- 从Pandas迁移到Polars
- Pandas数据结构
- Pandas数据操作
- Pandas数据清洗
- 从Pandas到Polars与DuckDB
- dbt Fusion与AI时代SQL语义工程