最后更新: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 10(2026 年 4 月发布)是唯一稳定支持 20+ SQL 与 DataFrame 后端的 Python 表达式库,代码零改动即可切换 DuckDB → Snowflake → BigQuery。
与 pandas 相比,Ibis 采用惰性求值:表达式先构建计算图,触发 execute() 或 to_polars() 时才推送到后端,避免把 TB 级数据拉到内存。
在 12k 行 e-commerce join 基准上,Ibis + DuckDB 后端比等价 pandas 代码快 8.4 倍,比 SQLAlchemy 手写 SQL 少写 62% 的代码。
Ibis 10 新增 ibis.range、更完善的 window frame 下推、原生 Polars 1.x 后端,并弃用了旧的 backends.pandas 内存后端。
迁移路径清晰:pandas 的 groupby().agg()、merge、assign 都能一对一映射到 Ibis 表达式,学习曲线约为 3 到 5 天。
与 SQL 相比,Ibis 表达式可组合、可类型检查,能被 mypy 静态分析。缺点是复杂窗口函数与递归 CTE 的表达仍略显冗长。
本页目录
Ibis 是什么?它解决了 Python 数据栈的什么问题
Ibis 10 新特性一览(2026)
Ibis vs pandas vs Polars vs SQLAlchemy 对比
安装 Ibis 10 并跑通第一个查询
后端一览:DuckDB、Polars、Snowflake、BigQuery、Postgres
如何把 pandas 代码迁移到 Ibis
Ibis 真的比 pandas 快吗?12k 行基准实测
进阶模式:窗口函数、UDF 与 Deferred 表达式
生产环境使用 Ibis 的四条实践建议
常见问题解答
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))
技巧: 在 CI 里用 DuckDB 后端跑单元测试,成本几乎为零;在 staging 环境里再切到 Snowflake XSMALL warehouse 做一次 smoke test。我的团队通过这种双层验证把 Snowflake 的月度账单降低了 41%。
如何把 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 同样适用。
我用自己维护的 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。
注意: 不要用 Ibis 处理「每行都要跑 Python 函数」的场景。如果你的核心逻辑在 UDF 里,Ibis 会把数据拉回 Python,速度反而不如原生 Polars。这种情况请直接用 Polars 或 pandas + Numba。我上个项目就在这里翻过车,UDF 一多,20 秒的查询直接飙到 3 分钟。
进阶模式:窗口函数、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 负责查询。