引擎快了,你的数据却悄悄乱了
📋 总体概括
Polars 2.0 默认切换流式引擎,join/group_by/unpivot 不再保证行序,代价由用户管道买单;同期 Apache Doris 5.0 借 Fluss 打通流与湖的统一查询。两条新闻指向同一趋势:引擎全面流式化,用确定性换吞吐,显式契约成为工程师的生存法则。
📄 正文
2026年10月6日,Polars 2.0 发布。一条不起眼的 release note,让不少数据工程师连夜回查自己的管道:collect() 默认走流式引擎,join、group_by、unpivot 不再保证行序。几乎同一时间,Apache Doris 5.0 宣布打通 Fluss,一条 SQL 同时查湖与流。
两件事看似无关,其实是同一场豪赌的两张赌桌:引擎厂商押注「流式执行是终局」,用确定性换吞吐量。而账单,寄给了每一个没读升级日志的用户。
🔧 一行代码改了默认值
升级日志没人读,等出了事,它就是判决书。
想象一个常见场景:团队周五把 Polars 从 1.x 升到 2.0,回归测试全绿,周一早上业务报表对不上账。代码一行没改,数据一版没换,结果就是不一样。
问题出在最核心的变更上。作者 Ritchie Vink 在发布博文里只用一句话描述了它:LazyFrame.collect() 默认切换到流式引擎,带来大规模的内存与性能收益。但代价写在细节里——对 join、group_by、unpivot 三类算子,除非显式设置 maintain_order,输入行序不再保证。
这不是理论风险。素材中的实测很直白:在 polars 2.0.0 上跑一个 200 万行的 inner join,默认配置下输出就是乱序的;在 join 上显式设置 maintain_order,输入行序才被保留。
还有一条更隐蔽的变更:out-of-core 外存计算默认开启。内存用量到约 80% 就开始向磁盘溢写,磁盘预算默认 64 GB。换句话说,2.0 的执行模型是彻底重写的,不只是「快了一点」。
| 行为 | Polars 1.x | Polars 2.0 |
|---|---|---|
| collect() 执行引擎 | 默认内存内执行 | 默认流式引擎 |
| join/group_by/unpivot 行序 | 隐式保持 | 不保证,需显式设 maintain_order |
| 超内存处理 | 需手动启用流式 | 默认开启,约 80% 内存即落盘 |
| 落盘磁盘预算 | — | 默认 64 GB |
| 性能基准 | — | 厂商自测,脚注声明不可与 TPC 官方结果对比 |
最后那一行值得单独说:发布博文里的基准测试是厂商自己的运行结果,脚注明确写了「不可与官方 TPC 结果比较」。厂商benchmark看看就好,自己的数据自己测,这是老规矩了。
⚠️ 你的管道里,藏着多少隐式契约
行序这东西,用的时候没人想起,丢的时候全都在场。
一位做报表管道的工程师私下吐槽:这类问题在测试环境几乎不会复现——测试数据太小,执行计划碰巧保住了顺序,一上生产规模就露馅。等你发现时,错的不是一行代码,而是一整条下游链路。
素材里点名了两类典型雷区:
- 逐行比对输出的代码:回归测试里的 golden file、快照测试,依赖输出和基线逐行一致,join 一乱序,测试全红或者更糟——全绿但内容错了;
- 按行位置读写文件的管道:上游 join 完按顺序写文件,下游按位置读,中间的顺序假设一旦失效,数据就发生了「错位拼接」。
把决策逻辑画出来,问题出在哪一目了然:
产业逻辑上要说清楚一点:这不是 Polars 的 bug,是行业级的取舍。流式执行的本质是并行加动态调度,要榨干多核和磁盘带宽,行序就是吞吐的敌人。任何主打流式的执行引擎——不管叫 pipeline、streaming 还是 out-of-core——都会走到同一个岔路口:顺序保证要不要做进默认行为。
Polars 选了「不做」。这个选择本身可以辩护,但它提醒所有人一个朴素的道理:默认值就是 API 契约。改默认值不是优化,是一次隐性的 breaking change,只是它没有报错,只会悄悄给出「不同的正确答案」——这比崩溃更危险。
🌊 另一张赌桌:流和湖进了同一口锅
两条链路两套代码,凌晨对账的日子该结束了。
切换视角看另一张赌桌。传统实时数仓团队的日常是这样的:Kafka 链路管增量,湖里的 Paimon 管历史,两套作业、两套口径、两份运维成本,每天凌晨还要跑一遍对账任务,确认两边没漂移。这就是 lambda 架构的原罪——为了「实时」二字,你永远在维护两个世界。
Apache Doris 5.0 和 Fluss 的组合,正面进攻这个结构。按素材披露的能力:Doris 5.0 可以直接读取 Fluss 的 log 表(追加流)和 primary-key 表(可更新状态),并通过 Union Read 把 Paimon 里的历史数据与 Fluss 中的近实时数据拼成一张逻辑表——历史在湖里,增量在流里,查询层负责缝合。一条 SQL 查「全量加增量」,不再是 PPT 词汇。
当然,天下没有免费的缝合。素材里特别提到 Union Read 过程中的 JVM 内存管理——把两套异构存储的数据流实时拼到一起,内存水位、算子调度都是实打实的工程难题。据熟悉这类架构的工程师反馈,统一查询引擎在演示环境行云流水,到生产里拼的往往就是内存管理的细节功力。
这里有一个值得记住的产业判断:查询引擎正在向上吞掉存储选型的话语权。以前是「选好了存储再选引擎」,现在 Fluss 这种「给流一个湖上入口」的存储,本身就是配合引擎设计的。引擎的定义能力越强,中间存储层的品牌存在感就越弱——流与湖的边界,正在从架构问题退化成一个配置项。
📊 同一个方向,两条路线
单机和分布式吵了十年,最后在「流式执行」这里碰了头。
把当前主流数据执行引擎放到一张图上看,格局比口水战清晰得多:
两条路线其实殊途同归:
- 单机向量化阵营(Polars、DuckDB):把向量化执行和 out-of-core 外存计算做到极致,一台机器干掉一个小集群,Polars 2.0 的默认流式化是这一路线的标志性事件;
- 服务端 MPP 阵营(Apache Doris、Trino、Spark):把湖仓和流接入做厚,用 Union Read 这类机制把存储边界模糊掉,Doris x Fluss 是这一路线的最新样本。
它们的共同点值得所有从业者注意:都把流式执行当作一等公民,把内存当缓存而非容器,把磁盘当扩展而非兜底。分歧只在一个点上——语义兜底给不给默认。Polars 把行序的选择权完全交还给用户,而数据库系的引擎通常还要背负更强的兼容承诺。哪条路对?短期看体验,长期看谁的用户管道更少在半夜被告警叫醒。
🧭 给工程师的五条自保清单
趋势你拦不住,能拦住的只有自己的事故。
基于这次变更,可以给依赖 Polars 或正在评估类似引擎的团队一份务实的清单:
再补三条执行层面的建议:其一,锁定版本、灰度升级,把引擎升级当成一次架构变更而不是一个 pip install;其二,在 CI 里专门开一组「流式引擎」的回归任务,让乱序问题在测试环境就暴露;其三,把团队里所有「顺序假设」翻出来审一遍——它们平时不写进文档,只写进事故报告。
小结
回到开头那两张赌桌。Polars 收走了一份没写进文档的行序保证,Apache Doris 把流和湖缝进同一张逻辑表——方向完全一致:引擎把资源调度权全部收回去,把语义选择权交还给用户。
这不是某家厂商的任性,而是数据基础设施向流式执行全面迁移的必经之路。可以预见,未来十二个月会有更多引擎做出类似的默认值变更,而「显式优于隐式」会从 Python 之禅,变成数据工程师的生存法则。下一次升级日志弹出来,建议还是读完再点确定。
本文由本站 AI 辅助聚合生成,原始来源如下: