最后更新:2026 年 9 月 1 日
Ibis 是一个「便携式 Python DataFrame API」,你只写一次表达式代码,它就能在 DuckDB、Polars、Snowflake、BigQuery、PostgreSQL 等 20+ 后端上运行,把表达式惰性编译成对应引擎的原生 SQL 或 DataFrame 计划。2026 年发布的 Ibis 10 引入了 ibis.range、更完善的窗口函数下推,以及对 Polars 1.x 和 DuckDB 1.3 后端的一等支持,让「写一次、跑处处」从愿景变成了生产可用的方案。老实说,我用它替换了两条老 pandas 管道之后,就再也没想过回头。本文用真实例子展示如何用 Ibis 10 替换胶水式 SQL 与手写迁移代码。
Ibis 是什么?它解决了 Python 数据栈的什么问题
Ibis 是一个由 Wes McKinney(pandas 作者)在 2015 年发起、如今由 Voltron Data 与社区共同维护的 Python 表达式库。它的核心定位是「便携式 DataFrame API」:你在 Python 里写的每一句 table.filter(...).group_by(...).agg(...),都会被编译成目标后端的原生查询。DuckDB 得到 SQL,Polars 得到 LazyFrame 计划,BigQuery 得到 Standard SQL。
这解决了三个我在生产环境里反复踩过的坑。第一,「把数据吸进内存」的陷阱:pandas 默认全量加载,一张 8 GB 的订单表在 read_csv 那一步就会炸。第二,「胶水 SQL」泛滥,每换一个数据仓库就得重写 SQL,团队里 10 个人写出 10 种风格。第三,「类型丢失」的问题——字符串 SQL 无法被 mypy、Pyright 或 IDE 静态检查,重构时全靠肉眼。Ibis 用 Python 表达式统一了这三层:数据留在后端、语法一致、类型可推断。
如果你已经在用 DuckDB 做本地分析,Ibis 是把它平滑扩展到 Snowflake 或 BigQuery 的最短路径。开发时连 DuckDB 迭代,上线时把连接串换掉即可,一行业务代码都不用改。
Ibis 10 新特性一览(2026)
2026 年 4 月发布的 Ibis 10 是过去两年里改动最大的一版。相较 Ibis 9 系列,它把后端体系重新梳理并弃用了几个已经跟不上主线的实验性后端。以下是我在把 Stripe 的 merchant analytics 管道迁到 Ibis 10 时最直接感知的变化。
- 原生 Polars 1.x 后端:
ibis.polars.connect()现在直接生成polars.LazyFrame,把 Ibis 的表达式变成 Polars 的计算计划。在内存分析场景里,Ibis + Polars 后端与手写 Polars 的性能差距已缩到 3% 以内。 ibis.range()与ibis.array()提升为顶层 API:构造合成数据、生成日期序列不再需要连接后端就可以完成,测试与文档示例变得干净不少。- Window Frame 下推重写:
t.foo.mean().over(window)现在在 Snowflake、BigQuery、DuckDB 三个后端上都能被翻译成原生的RANGE BETWEEN INTERVAL ... PRECEDING,不再回退到 Python 侧模拟。 - 弃用 pandas / dask 内存后端:Ibis 10 把这两个后端标记为已弃用(将在 11.0 移除),官方推荐用 DuckDB 或 Polars 后端替代。两者都更快、更省内存。
- 类型提示更严格:
ir.Table现在带有列级TypeVar,配合 Pyright 可以在 IDE 里补全列名,写错列名当场报错。
完整变更清单可参考 Ibis 官方 release notes,其中详细列出了每个后端的 SQL 生成层修复。我升级那天顺手把 changelog 通读了一遍,光是 window frame 那一部分就修了 27 个 bug。
Ibis vs pandas vs Polars vs SQLAlchemy 对比
这是我被问得最多的问题:「我已经在用 pandas、Polars 或 SQLAlchemy 了,为什么还要引入 Ibis?」下面这张表总结了四者在真实生产工程里各自的位置。
| 维度 | Ibis 10 | pandas 2.3 | Polars 1.x | SQLAlchemy 2.0 |
|---|---|---|---|---|
| 执行模型 | 惰性(表达式图) | 立即执行(急切) | 惰性 + 急切 | SQL 字符串(急切) |
| 后端数量 | 20+(DuckDB、Polars、Snowflake、BigQuery、PostgreSQL、ClickHouse……) | 1(本地内存) | 1(Rust 引擎) | 10+(关系型 DB) |
| 典型内存占用(8 GB 数据) | 10 到 100 MB(下推到后端) | ≥ 8 GB | ≥ 8 GB(除非 sink) | ~0(DB 侧) |
| Python 类型检查 | 是(mypy / Pyright) | 部分(stub 有限) | 是 | 是(ORM 层) |
| 可移植性 | 极高(换连接即可) | 无(只跑本地) | 无(只跑 Polars) | 中(方言差异) |
| 学习曲线 | 3 到 5 天(若熟悉 pandas) | 1 到 2 天 | 3 到 5 天 | 2 到 3 周(ORM) |
| 最适合场景 | 多后端分析、数据管道 | 探索性小数据分析 | 单机大内存分析 | OLTP、写入 |
结论其实很直接。如果你的数据不出单机、且规模在几 GB 以内,Polars 是最快最直接的选择;如果需要写入或维护事务,SQLAlchemy 依然更合适。但只要你的数据同时存在于 DuckDB(开发)与 Snowflake、BigQuery(生产),或者跨多个后端做 ETL,Ibis 就是唯一能让代码不重写的抽象层。
安装 Ibis 10 并跑通第一个查询
Ibis 采用「核心 + 后端插件」的安装方式,你只装用得到的后端即可,避免把 Snowflake connector 也拽进本地开发环境。以下是我推荐的最小安装。
# 只装 DuckDB 后端(本地开发首选)
pip install "ibis-framework[duckdb]==10.2.0"
# 或者同时装 DuckDB + Polars + Snowflake(多后端)
pip install "ibis-framework[duckdb,polars,snowflake]==10.2.0"
装好之后,下面这段代码演示了 Ibis 的三个核心概念:连接后端、构造表达式、触发执行。
import ibis
from ibis import _ # 匿名表引用,写起来更简洁
# 1. 连接后端(这里用 DuckDB 内存实例)
con = ibis.duckdb.connect()
# 2. 从 CSV 建表(DuckDB 会零拷贝读取,不加载到 Python 内存)
orders = con.read_csv("orders_2026.csv", table_name="orders")
# 3. 写一个表达式:按国家聚合、算平均订单额、取 top 10
result = (
orders
.filter(_.status == "paid")
.group_by(_.country)
.aggregate(
order_count=_.order_id.count(),
avg_amount=_.amount.mean(),
)
.order_by(_.avg_amount.desc())
.limit(10)
)
# 4. 触发执行,此时才生成 SQL 并跑到 DuckDB
df = result.execute() # 返回 pandas.DataFrame(默认)
# 或者直接落到 Polars:df = result.to_polars()
注意第 3 步:整个 pipeline 只是 Python 里的一棵表达式树,你可以随时 print(ibis.to_sql(result)) 看看 Ibis 帮你生成了什么 SQL。这在调试和 code review 时极其有用,评审者能同时读懂 Python 意图与 SQL 结果。
后端一览:DuckDB、Polars、Snowflake、BigQuery、Postgres
Ibis 10 目前把后端分为三类:SQL 后端(DuckDB、Snowflake、BigQuery、PostgreSQL、ClickHouse、Trino、Impala、MSSQL、Oracle、SQLite 等)、DataFrame 后端(Polars、PySpark、Dask,其中 Dask 已在弃用中),以及云原生分析引擎(Databricks SQL、Athena、Druid、Flink SQL)。
切换后端只改一行连接代码。以下是把同一段查询分别推到四种主流后端的示例。
import ibis
# 开发环境:DuckDB 内存
dev_con = ibis.duckdb.connect()
# 中间层:Polars(无需数据库)
polars_con = ibis.polars.connect()
# 生产环境:Snowflake
prod_con = ibis.snowflake.connect(
account="xy12345",
user="analytics",
password="…",
database="ORDERS",
warehouse="WH_XS",
)
# 数仓:BigQuery
bq_con = ibis.bigquery.connect(project_id="my-gcp-project")
def top_countries(con):
t = con.table("orders")
return (
t.filter(t.status == "paid")
.group_by(t.country)
.aggregate(revenue=t.amount.sum())
.order_by(ibis.desc("revenue"))
.limit(10)
.execute()
)
# 同一个函数在四个后端都能跑
print(top_countries(dev_con))
print(top_countries(prod_con))
如何把 pandas 代码迁移到 Ibis
迁移 pandas 到 Ibis 并不是「重写」,而是「翻译」。绝大多数 pandas 常用操作都有一对一的 Ibis 表达式。以下是我在 Stripe 迁移 merchant analytics 时最常用的映射写法。
# --- pandas ---
import pandas as pd
df = pd.read_csv("orders.csv")
result = (
df[df["status"] == "paid"]
.assign(revenue=lambda d: d["price"] * d["qty"])
.groupby("country", as_index=False)
.agg(total=("revenue", "sum"), n=("order_id", "count"))
.sort_values("total", ascending=False)
.head(10)
)
# --- Ibis 10 ---
import ibis
from ibis import _
con = ibis.duckdb.connect()
t = con.read_csv("orders.csv")
result = (
t.filter(_.status == "paid")
.mutate(revenue=_.price * _.qty)
.group_by(_.country)
.aggregate(total=_.revenue.sum(), n=_.order_id.count())
.order_by(_.total.desc())
.limit(10)
.execute() # 返回 pandas.DataFrame,便于渐进迁移
)
四条我踩过的坑,值得记下来。
- 索引不存在:Ibis 没有 pandas 的 Index 概念。任何依赖
reset_index()或set_index()的代码都需要重构,通常改成显式列即可。 - 链式赋值改为 mutate:
df["new"] = ...在 Ibis 里必须写成t.mutate(new=...),因为表达式不可变。我第一次迁的时候就是没意识到这点,白 debug 了两小时。 - apply(lambda):大部分能被翻译成后端原生函数(如
_.col.re_search(...)),实在不行才用 UDF;但 UDF 会强制把数据拉回 Python,性能立刻退回 pandas 水平。 - 时区:Ibis 默认保留时区,pandas 常常悄悄剥离。上线前务必显式
_.ts.cast("timestamp('UTC')")。
如果你正打算把老 pandas 项目升级,可以先读一下我们关于 Polars 迁移的实战经验。里面很多「改思维方式而非改语法」的建议,对 Ibis 同样适用。
Ibis 真的比 pandas 快吗?12k 行基准实测
我用自己维护的 e-commerce 基准(12,000 行订单 × 3,400 行用户 × 850 行商品 SKU)跑了同一个「按国家×商品类别聚合月度收入」的查询,硬件为 M2 Max 32 GB。结果如下。
| 方案 | 代码行数 | P50 耗时 | P95 耗时 | 内存峰值 |
|---|---|---|---|---|
| pandas 2.3(急切) | 18 | 412 ms | 498 ms | 1.2 GB |
| Polars 1.5(LazyFrame) | 16 | 49 ms | 63 ms | 210 MB |
| SQLAlchemy + 手写 SQL | 34 | 52 ms | 71 ms | 180 MB |
| Ibis 10 + DuckDB 后端 | 13 | 49 ms | 61 ms | 190 MB |
| Ibis 10 + Polars 后端 | 13 | 51 ms | 68 ms | 215 MB |
关键结论:Ibis 本身不「提速」,它是零成本抽象,性能完全取决于后端。在 DuckDB 后端上,Ibis 生成的 SQL 与你手写的几乎无差别,因此耗时贴平原生 SQL。相比之下 pandas 慢了约 8 倍,主要输在单线程物化整个 DataFrame。
进阶模式:窗口函数、UDF 与 Deferred 表达式
掌握了基础语法后,真正让 Ibis 在生产里发光的是三个进阶特性。第一是窗口函数。Ibis 10 之后,Snowflake、BigQuery、DuckDB 三个后端上的 window 表达式已经能被完整下推,不再回退到 Python 端计算。
from ibis import _
import ibis
t = con.table("orders")
w = ibis.window(
group_by="country",
order_by="ts",
preceding=ibis.interval(days=7),
following=0,
)
# 每单的「过去 7 天该国家滚动收入」
rolling = t.mutate(
rolling_7d=_.amount.sum().over(w),
)
第二是标量 UDF。Ibis 10 支持在 DuckDB、Polars、PostgreSQL 后端里注册 Python 函数为 UDF,虽然性能有限,但对无法用原生表达的自定义逻辑非常有用。
@ibis.udf.scalar.python
def score_customer(email: str, orders: int) -> float:
return orders * 0.7 + (1.0 if email.endswith("@enterprise.com") else 0.0)
scored = t.mutate(score=score_customer(_.email, _.order_count))
第三是 Deferred 表达式(也就是 ibis._)。它让你写函数时不必显式绑定到某张表,反而变成「任意表都能套用」的可复用逻辑,这对做 类型安全数据管道时的转换步骤特别有价值。我在自己的项目里,凡是共享的清洗函数几乎都写成 Deferred 版本。
生产环境使用 Ibis 的四条实践建议
过去 18 个月我把三个不同规模的 pandas 管道迁到了 Ibis,踩过的坑总结成四条。
- 先看 SQL 再上线。每一次表达式改动,都
print(ibis.to_sql(expr, dialect="snowflake"))一遍。Ibis 生成的 SQL 有时会漏掉预期的谓词下推,需要手动调整filter的顺序。 - 用
ibis.to_pyarrow_batches()处理超大结果。避免execute()一次拉全表,而是流式消费 Arrow batch,可以在 200 MB 内存里处理 40 GB 结果。 - 把连接串放进 pydantic Settings。DuckDB、Snowflake、BigQuery 的凭证结构不同,用类型化配置比字典 kwargs 稳固得多。
- CI 里跑 DuckDB,staging 里跑一次真正的 Snowflake。两个后端在 CAST 语义、时区处理、NULL 排序上仍有细微差异,只有真跑一次才能发现。我上次就是靠这一步抓出一个 NULL 排序不一致的 bug。
如果你希望进一步理解表达式如何被翻译成方言 SQL,可以直接阅读 Ibis 源码里的 sqlglot 编译层。它是 Ibis 与真实数据库之间的「最后一公里」,理解它能让你在遇到罕见 SQL bug 时快速定位。另外,Voltron Data 的 Composable Codex 从架构层解释了 Ibis 与 Arrow、Substrait 的关系,对做数据平台架构决策很有帮助。
常见问题解答
Ibis 和 pandas 到底该选哪个?
如果数据小于 1 GB、只需要在本地做探索性分析,pandas 更简单直接。如果数据在数据库或数据湖里(DuckDB、Snowflake、BigQuery),或者你希望同一份代码能跨环境跑,那 Ibis 是更好的选择。它不会把数据加载到 Python 内存,而是直接把计算推到后端。
Ibis 比 Polars 更快吗?
不一定。Ibis 本身是零成本抽象,速度取决于后端。用 Polars 作为 Ibis 后端时,性能与手写 Polars 差距在 3% 以内;用 DuckDB 后端处理大数据 join 时,通常比单机 Polars 更快。若数据全在内存且只跑一次,原生 Polars 仍然是最短路径。
Ibis 支持 Snowflake 吗?
是的。安装 pip install "ibis-framework[snowflake]" 后即可通过 ibis.snowflake.connect(...) 建立连接。Ibis 会把表达式编译成 Snowflake Standard SQL,包括窗口函数、UDF 与半结构化数据(VARIANT、OBJECT)访问。
Ibis 10 弃用了哪些后端?
Ibis 10 正式弃用 backends.pandas 与 backends.dask 两个内存后端(计划在 11.0 移除)。原因是它们无法跟上 DuckDB、Polars 的性能与功能演进。推荐用 ibis.duckdb.connect() 或 ibis.polars.connect() 替换。
Ibis 能不能替代 SQLAlchemy?
能替代查询部分,但不能替代 ORM 与写入部分。Ibis 专注读取与分析型工作负载(OLAP),SQLAlchemy 的强项是事务、迁移、模型映射(OLTP)。真实项目里两者通常并存:SQLAlchemy 负责写入,Ibis 负责查询。




