事实核验: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. 练习与验收

  1. 用 memtable 构造包含 NULL 与重复订单的数据,写出 GMV 口径断言。
  2. 分别查看表达式、生成 SQL 和执行结果,说明三个阶段的差异。
  3. 开启交互模式观察预览,再关闭它并确认不会因打印而执行。
  4. 写一个返回表达式的清洗函数,并在 DuckDB 后端上验证结果。
  5. 为 join 写键唯一、行数和未匹配率三类检查。

8. 延伸阅读

官方来源