查询引擎:Parquet 文件格式的守门人
要点:主流查询引擎不支持读取较新的 Parquet 编码,这迫使 DuckDB 等系统默认写入较旧的编码,从而牺牲了压缩率。
Apache® Parquet™ 格式
Apache Parquet 是一种流行的、免费的、开源的、面向列的数据存储格式。数据库系统通常将数据从 CSV 和 JSON 等格式加载到数据库表中,然后再进行分析,而 Parquet 旨在直接进行高效查询。Parquet 考虑到用户通常只想读取部分数据,而不是全部数据。为了适应这一点,Parquet 被设计为读取单个列,而不是总是必须读取所有列。此外,可以使用统计信息来过滤掉文件的某些部分,而无需完全读取它们(这也称为区域地图)。此外,由于轻量级列压缩和通用压缩的结合,以 Parquet 格式存储数据通常会产生比 CSV 或 JSON 小得多的文件。
许多查询引擎实现了 Parquet 文件的读取和写入。因此,它也可用作数据交换格式。例如,Spark 在大型分布式数据管道中写入的 Parquet 文件稍后可以使用 DuckDB 进行分析。由于许多系统都可以读取和写入 Parquet,因此它是 Delta Lake™ 和 Iceberg™ 等数据湖解决方案的首选数据格式。虽然 Parquet 肯定存在缺陷,研究人员和 公司正在尝试使用新的数据格式来解决这些缺陷,但不管你喜欢与否,Parquet 似乎至少会存在一段时间。
所以,既然我们在这里,我们不妨尽量发挥它的作用,对吧?SQL 也有其缺陷,虽然研究人员肯定尝试创建不同的查询语言,但我们仍然困在 SQL 中。DuckDB 接受这一点,并尝试 使 之 更佳。Parquet 开发人员也对他们的格式做同样的事情,通过偶尔更新它,带来使格式更好的新功能。
更新
如果 DuckDB 在某个版本中为其内部文件格式添加了新的压缩方法,则所有后续版本都必须能够读取它。否则,在更新到 1.2.0 后,您将无法读取由 DuckDB 1.1.0 创建的数据库文件。这称为向后兼容性,对开发人员来说可能具有挑战性。它有时需要保留遗留代码并创建从旧到新的转换。保持对旧格式的支持非常重要,因为更新 DuckDB 比重写整个数据库文件容易得多。
向后兼容性对于 Parquet 也很重要:应该可以读取多年前编写的 Parquet 文件。幸运的是,大多数主流查询引擎仍然可以读取 Parquet 1.0 格式的文件,该格式于 2013 年发布,至今已有十年以上。对格式的更新不会威胁向后兼容性,因为查询引擎只需继续能够读取旧文件。但是,同样重要的是,查询引擎要添加对读取较新文件的支持,以及旧文件,以便我们也可以开始写入新的和改进的 Parquet 文件。
这里变得棘手了。如果我们今天推出更新,我们不能期望查询引擎明天就能读取最前沿的 Parquet 格式。我们不能在一段时间内开始写入新格式,因为许多查询引擎将无法读取它。《鲁棒性原则》指出:“发送时要保守,接受时要开放。” 如果我们将其应用于 Parquet 文件,查询引擎应努力读取新的 Parquet 文件,但至少默认情况下不要写入它们。
编码
DuckDB 非常喜欢轻量级压缩。因此,对于即将推出的 DuckDB 1.2.0 版本,我们很高兴在我们的 Parquet 写入器中实现了 DELTA_BINARY_PACKED
、DELTA_LENGTH_BYTE_ARRAY
(在 2015 年的 Parquet 2.2.0 中添加)和 BYTE_STREAM_SPLIT
(在 2019 年的 Parquet 2.8.0 中添加)编码。DuckDB 最初创建于 2018 年,自 2020 年以来一直能够读取 Parquet,并且自 2022 年以来一直能够读取 DELTA_BINARY_PACKED
和 DELTA_LENGTH_BYTE_ARRAY
编码,自 2023 年以来一直能够读取 BYTE_STREAM_SPLIT
。
但是,尽管这些新编码在 1.2.0 中可用,但 DuckDB 默认情况下不会写入它们。如果 DuckDB 这样做,我们的许多用户将会有令人沮丧的体验,因为一些主流查询引擎仍然不支持读取这些编码。如果用户的下游应用程序无法读取文件,那么拥有良好的压缩率对用户没有帮助。因此,我们必须默认禁用写入这些编码。它们仅在 COPY
命令中设置 PARQUET_VERSION v2
时使用。
早至 0.9.1 (2023 年底发布) 版本的 DuckDB 已经可以读取使用设置
PARQUET_VERSION v2
序列化的文件。
压缩数据几乎总是在文件大小和写入所需时间之间进行权衡。让我们看看下面的例子(在配备 M1 Max 的 MacBook Pro 上运行)
-- Generate TPC-H scale factor 1
INSTALL tpch;
LOAD tpch;
CALL dbgen(sf = 1);
-- Export to Parquet using Snappy compression
COPY lineitem TO 'snappy_v1.parquet'
(COMPRESSION snappy, PARQUET_VERSION v1); -- 244 MB, ~0.46 s
COPY lineitem TO 'snappy_v2.parquet'
(COMPRESSION snappy, PARQUET_VERSION v2); -- 170 MB, ~0.39 s
-- Export to Parquet using zstd compression
COPY lineitem TO 'zstd_v1.parquet'
(COMPRESSION zstd, PARQUET_VERSION v1); -- 152 MB, ~0.58 s
COPY lineitem TO 'zstd_v2.parquet'
(COMPRESSION zstd, PARQUET_VERSION v2); -- 135 MB, ~0.44 s
当使用 Snappy (DuckDB 针对 Parquet 的默认页面压缩算法,主要关注速度,而不是压缩率) 时,启用编码后,文件会小 ~30%,写入速度会快 ~15%。当使用 zstd (主要关注压缩率,而不是速度) 时,启用编码后,文件会小 ~11%,写入速度会快 ~24%。
压缩率在很大程度上取决于数据压缩得有多好。以下是一些更极端的例子
CREATE TABLE range AS FROM range(1e9::BIGINT);
COPY range TO 'v1.parquet' (PARQUET_VERSION v1); -- 3.7 GB, ~2.96 s
COPY range TO 'v2.parquet' (PARQUET_VERSION v2); -- 1.3 MB, ~1.68 s
整数序列 0, 1, 2, … 使用 DELTA_BINARY_PACKED
可以很好地压缩。在这种情况下,文件会小 ~99%,写入速度几乎快两倍。
压缩浮点数要困难得多。尽管如此,如果存在模式,数据将被很好地压缩
CREATE TABLE range AS SELECT range / 1e9 FROM range(1e9::BIGINT);
COPY range TO 'v1.parquet' (PARQUET_VERSION v1); -- 6.3 GB, ~3.83 s
COPY range TO 'v2.parquet' (PARQUET_VERSION v2); -- 610 MB, ~2.63 s
这个序列使用 BYTE_STREAM_SPLIT
可以很好地压缩。它小 ~90%,写入速度快 ~31%。真实世界的数据通常没有如此易于压缩的模式。尽管如此,仍然存在模式,这些编码将利用这些模式。
如果您使用的查询引擎支持读取它们,您可以在 DuckDB 1.2.0 发布后开始使用这些编码!
浪费的比特
虽然很难获得准确的数字,但可以肯定的是,每天都有许多 TB 的数据以 Parquet 格式写入。写入的很大一部分比特被浪费了,因为查询引擎尚未实现这些较新的编码。这个问题的解决方案出奇地容易。无需发明任何新事物来停止浪费所有这些空间。只需阅读 Parquet 编码规范,并实现它们。其中一些“较新”的编码现在已经有近 10 年的历史了!
通过减小 Parquet 文件的大小,我们可以减少数据中心中存储的数据量。即使稍微减少我们存储的数据量也会产生很大的影响,因为它最终可以减少建造新数据中心的需求。这并不是说数据中心是邪恶的;我们将来肯定需要更多的数据中心。但是,充分利用我们已经拥有的数据中心不会伤害任何人。
结论
Parquet 目前是行业标准的表格数据格式。由于它也用作数据交换格式,因此 Parquet 功能的有效性取决于使用它的查询引擎。如果一些主流查询引擎(你们知道自己是谁)拒绝实现这些功能,我们所有人都会遭受损失。这并不是说所有查询引擎都必须处于 Parquet 的前沿,DuckDB 肯定不是。但是,查询引擎开发人员有共同的责任,即让 Parquet 更有用。
我们希望更多的查询引擎能够实现这些较新的编码。然后,更多的查询引擎可以默认写入它们,并停止浪费如此多的比特。