Excel 的格式税:Tableau 读 Excel 慢怎么解决

同一份数据换个格式存,Tableau 每次读取的代价就不同。读 Excel 慢的根源在文件格式本身——把取数与清洗拆开,中间落一份 Hyper,这笔开销就只付一次。

01 一个被反复提起的现象

Tableau 连接 Excel 的能力,在数据量不大时几乎不构成负担。一旦行数上到几十万,或者在 Prep 里并集多个工作表、多个同结构文件,等待时间就会以肉眼可见的斜率上升。这件事在社区里被反复提起,官方的性能文档却很少正面回应它,原因大概在于,它算不上产品的缺陷,只是文件格式自身的属性所决定的必然代价。

理解这一点之后,选择就变得清楚了:与其想办法让 Tableau 读 Excel 更快,不如考虑让 Tableau 根本不读 Excel。

02 格式税:慢的根源在文件格式

这笔成本,我习惯叫它"格式税"。同一份数据,仅仅因为存成的格式不同,每次读取都要多付一笔固定的开销。它和文件大小不成正比,也没有哪一份报表会记录它,而且只要取数的动作还落在 Excel 上,每刷新一次就要重新缴一次。

要看清这笔税从哪里来,得先回到 xlsx 的定义上。xlsx 本质上是一个 zip 压缩包,里面装着若干份 XML。工作表里每一个单元格都是 XML 里的一段标签,文本值还要去共享字符串表(sharedStrings)里反查索引。这决定了任何工具想取出数据,都必须把整个工作表的 XML 解码一遍。

把这个特性放到 Tableau 的数据源体系里看,会更清楚它的位置:Excel 是 Tableau 常用数据源里,在"无索引、无下推、无并行"这三点上代价最高的一种。 数据库有索引可以跳过无关区域,有查询下推可以把筛选和列选择交给服务端完成,也能多线程并发取数;Excel 三样都没有。没有索引,只能从头扫到尾部;没有下推,你在 Tableau 里只保留三列,解析器仍然要走完全部单元格,先全量读进来再丢掉;没有并行,一个文件一个文件地串行处理。

CSV 同样没有索引和下推,但它是一条纯文本流,顺着读一遍也就读完了;xlsx 要先解压、再解 XML、还要去共享字符串表里逐条反查,同样的行数,代价要高出数倍。这也是为什么后面所有的讨论,针对的都是 Excel,而不是"文件类数据源"这个大类。

在这个基础上,还有两层放大效应。其一是并集,N 个文件对应 N 次独立的打开、解析与类型推断,而且彼此串行;其二是类型合并,一旦某份文件的某一列被识别成数值、另一份被识别成文本,并集就会把整列提升为字符串,此前的工作全部作废重来。文件数量越多,这两层放大的比例就越显著。

还有一处更隐蔽的来源,是工作簿自身的膨胀。文件看起来只有五万行,但整列套过格式、公式下拉到底、残留的条件格式与透视缓存,都会让实际单元格数量远超"行数乘以列数"。同一份数据分别以 CSV 和 xlsx 的形式存在,解析成本可以相差一个数量级。这一点在排查性能时经常被忽略,因为它不会体现在任何一份报告上。这笔税的性质也正在于此:换工具免不掉它,只有换掉格式才能免掉。

03 两条路,一个共同的判断

既然瓶颈在解析这一步,出路也就只有两条:把解析的成本摊薄,或者把解析整个移到 Tableau 之外。

第一条路仍然留在 Prep 里,只是把它放在整条链路的最前端,让它只做一件事:读取 Excel、完成合并与类型统一,然后输出一份 Hyper 文件。这份 Hyper 作为后续所有流程的数据底座,清洗、计算、聚合都从它出发。这样安排的好处是,解析只发生一次,后续无论改多少次清洗逻辑,都不必重新经历一遍 Excel 的解码;同时 Prep 的可视化调试体验完整保留,改字段、加筛选、看预览,都还是原来的操作方式。对于以人工核对为主的场景,这条路几乎没有迁移成本。

第二条路是把取数交给 Python,读取之后直接把结果写成 Hyper,Tableau Desktop 连接这份 Hyper 即可,中间不需要任何其他环节。这条路过去的主要顾虑是清洗逻辑要写成代码,失去了 Prep 那种即时预览的反馈。但这个顾虑在今天的份量已经变了:整个取数与清洗的流程都可以交给 AI,由它写出一份脚本,再包一个双击就能运行的入口。macOS 上做成 .command 文件,Windows 上用 .bat,或者直接做成系统里的快捷指令,点击之后自动跑完、自动覆盖输出。做好这一次之后,日常操作和双击一个 .tfl 文件没有区别。

这两条路背后其实是同一个判断:把取数与清洗分开。取数是低频、稳定、一次性的动作,清洗是高频、反复、需要试错的动作。把两者捆在同一个流程里,就意味着每次调整清洗逻辑,都要重新付出一次取数的代价。而只要在中间落一份 Hyper,这笔代价就只付一次。

04 为什么落地格式就是 Hyper

有人会问,既然要落中间文件,为什么落在 Hyper 上,而不是 CSV 或者 Parquet。这个问题其实不需要太多论证,因为 Hyper 是 Tableau 自己的格式,天然支持。

Tableau 原生支持的"单文件"数据源只有几类:Excel、文本文件、统计文件(SAS、SPSS、R)、Tableau 数据提取,以及 PDF。Hyper 属于其中唯一不带解析成本的一种,Desktop 可以直接连接,列类型是固定的,体积不到同等 CSV 的一半。而其他格式都多一道手续:CSV 要先想清楚编码和类型推断,Parquet 可能需要安装第三方的连接器,DuckDB 和 SQLite 要走 ODBC 驱动,在 macOS 上配置繁琐,通用 ODBC 连接在 Tableau 里还有功能上的限制。多一层转换,就多一处可能出错的地方,也多一份需要维护的文件。

更关键的是,Python 这一侧有官方支持。Tableau 提供了 tableauhyperapi 这个包,可以直接创建 Hyper 文件,不需要任何中间格式作为终点。代码可以交给 AI 来写,包括选哪条路径、类型怎么映射、空值怎么处理,都有成熟的写法可以照着来。

于是整条链路收敛成:Excel 进来,AI 写好的脚本处理,写出 Hyper,Tableau 接上。中间没有需要人照看的环节,时间和维护成本都不高。

05 怎么选,以及什么时候该往上走

两条路的分界,主要看两件事。清洗逻辑是否离不开可视化调试,以及这份数据最终由谁来维护。如果流程需要反复试错、或者要交给不写代码的同事接手,走第一条路更稳;如果取数环节本身足够复杂,文件多、结构不一、需要从文件名里提取字段,而后续清洗相对固定,那走第二条路更省事。

至于数据量,需要单独说一句。本地机器适合承担的是几百 MB 级别的取数与清洗,再往上,无论是 Python 脚本还是 Prep 流程,都不适合继续压在个人电脑上。数据量到千万行级别、需要多人多工具同时消费、需要增量刷新或者保留历史版本时,正确的做法是往上走一层,把流程交给 Tableau Server,用 Prep Conductor 定时执行,让服务器去承担这部分负载。在本地硬扛大规模的库操作,短期看是省了配置,长期看是给自己埋了过多的坑。

06 结语

Excel 在数据分析里扮演的角色,一直是一个方便的交换格式,而不是一个查询引擎。让它承担后者的职责,无论在哪个工具里都会遇到同一堵墙。真正值得改变的,是我们对"取数"这个动作的默认设定:默认它应该是一次性的、前置的、与后续清洗解耦的。一旦这样安排,那些原本摊在每一次刷新里的等待,就只会在第一次付出。

No comments yet