CSV 文件:Parquet 终极存储文件格式的挑战者——亦或不然?

Author Avatar
Pedro Holanda
2024-12-05 · 13 分钟

总结来说:数据分析主要使用两种类型的存储格式文件:人类可读的文本文件(如 CSV)和性能驱动的二进制文件(如 Parquet)。这篇博文将在这两种格式的性能和灵活性方面进行终极对决,只有一方能胜出。

文件格式

CSV 文件

数据最常存储在人类可读的文件格式中,例如 JSON 或 CSV 文件。这些文件格式易于操作,因为任何使用文本编辑器的人都可以轻松打开、修改和理解它们。

多年来,CSV 文件因其操作缓慢和繁琐而声名狼藉。实际上,如果您想使用您喜欢的数据库系统操作 CSV 文件,您必须遵循以下步骤:

  1. 通过在文本编辑器中打开文件来手动发现其模式。
  2. 使用给定模式创建一个表。
  3. 手动确定文件的格式(例如,哪个字符用作引号?)
  4. 使用 COPY 语句并将格式设置为指定值,将文件加载到表中。
  5. 开始查询它。

这个过程不仅繁琐,而且并行化 CSV 文件读取器远非易事。这意味着大多数系统要么单线程处理,要么采用两遍方法。

此外,CSV 文件是“狂野”的:尽管存在作为 CSV 标准的 RFC-4180,但它通常被忽略。因此,系统必须足够健壮,才能处理这些文件,就像它们直接来自狂野西部一样。

最后但同样重要的是,CSV 文件是浪费的:数据总是以字符串形式布局。例如,如果数值 1000000000 存储为 int32,它将占用 10 字节而不是 4 字节。此外,由于数据布局是行式的,应用轻量级列式压缩的机会就丧失了。

Parquet 文件

由于这些缺点,Parquet 等性能驱动的文件格式近年来获得了显著的普及。Parquet 文件无法通过通用文本编辑器打开,不易编辑,并且具有严格的模式。然而,它们以列形式存储数据,应用各种压缩技术,将数据分区到行组中,维护这些行组的统计信息,并直接在文件中定义其模式。

这些特性使 Parquet 成为一种一体式的文件格式——高度不灵活但高效且快速。由于模式定义良好,从 Parquet 文件读取数据很容易。并行化扫描器也很简单,因为每个线程都可以独立处理一个行组。谓词下推也易于实现,因为每个行组都包含统计元数据,并且文件大小非常小。

结论应该很简单:如果您的文件很小并且需要灵活性,那么 CSV 文件就足够了。然而,对于数据分析,人们应该转向 Parquet 文件,对吗?嗯,这个转向可能不再是硬性要求了——继续阅读以了解原因!

在 DuckDB 中读取 CSV 文件

在过去的几个版本中,DuckDB 致力于提供不仅易于使用而且性能卓越的 CSV 扫描器。该扫描器具有其自定义的CSV 嗅探器、并行化算法、缓冲区管理器、类型转换机制和基于状态机的解析器。

为了提高可用性,以前手动发现模式和创建表的范式已经改变。现在,DuckDB 使用 CSV 嗅探器,类似于 Pandas 等数据帧库中发现的嗅探器。这使得查询 CSV 文件变得非常容易,例如:

FROM 'path/to/file.csv';

或者从 CSV 文件创建表,无需预先定义任何模式,例如:

CREATE TABLE t AS FROM 'path/to/file.csv';

此外,该读取器成为分析系统中速度最快的 CSV 读取器之一,这可以从 最新迭代ClickBench最新迭代的加载时间所显示的那样。在此基准测试中,数据从一个82 GB 未压缩 CSV 文件加载到数据库表中。

Image showing the ClickBench result 2024-12-05
ClickBench CSV 加载时间 (2024-12-05)

比较 CSV 和 Parquet

随着 CSV 读取器在可用性和性能方面的显著提升,人们可能会问:将 CSV 文件加载到表中与加载 Parquet 文件相比,实际性能差异是什么?此外,直接对这些文件运行查询时,这两种格式有何不同?

为了找出答案,我们将使用包含 TPC-H 数据的 CSV 和 Parquet 文件运行几个示例,以阐明它们的差异。用于生成本博文基准测试的所有脚本都可以在一个存储库中找到。

可用性

在可用性方面,扫描 CSV 文件和 Parquet 文件可能存在显著差异。

在简单情况下,如果 DuckDB 正确检测到所有选项,则可以直接对 CSV 或 Parquet 文件运行查询。

FROM 'path/to/file.csv';
FROM 'path/to/file.parquet';

对于那些“狂野”、不按规则出牌的,类似于亚瑟·摩根的 CSV 文件,情况可能会大相径庭。这从每个扫描器可以设置的参数数量中可见一斑。Parquet 扫描器总共有六个参数可以改变文件读取方式。在大多数情况下,用户永远不需要手动调整其中任何一个。

另一方面,CSV 读取器依赖于嗅探器能够自动检测许多不同的配置选项。例如:分隔符是什么?文件顶部应该跳过多少行?是否有注释?等等。这导致用户可能需要手动调整超过30 个配置选项才能正确解析其 CSV 文件。同样,由于缺乏广泛采用的标准,需要如此多的选项。然而,在大多数情况下,用户可以依靠嗅探器,或者最多更改一两个选项。

CSV 读取器还拥有广泛的错误处理系统,并且在出现问题时总是会提供要检查的选项建议。

为了向您展示 DuckDB 错误报告系统的工作原理,请考虑以下 CSV 文件:

Clint Eastwood;94
Samuel L. Jackson

在此文件中,第二行缺少第二列的值。

Invalid Input Error: CSV Error on Line: 2
Original Line: Samuel L. Jackson
Expected Number of Columns: 2 Found: 1
Possible fixes:
* Enable null padding (null_padding=true) to replace missing values with NULL
* Enable ignore errors (ignore_errors=true) to skip this row

  file = western_actors.csv
  delimiter = , (Auto-Detected)
  quote = " (Auto-Detected)
  escape = " (Auto-Detected)
  new_line = \n (Auto-Detected)
  header = false (Auto-Detected)
  skip_rows = 0 (Auto-Detected)
  comment = \0 (Auto-Detected)
  date_format =  (Auto-Detected)
  timestamp_format =  (Auto-Detected)
  null_padding = 0
  sample_size = 20480
  ignore_errors = false
  all_varchar = 0

DuckDB 提供有关遇到的任何错误的详细信息。它会突出显示出现问题的 CSV 文件行,显示原始行,并建议可能的错误修复方案,例如忽略有问题的行或用 NULL 填充缺失值。它还会显示用于扫描文件的完整配置,并指示选项是自动检测的还是手动设置的。

这里的底线是,即使 CSV 用法取得了进步,Parquet 文件的严格性也使其操作起来更加容易。

当然,如果您需要在文本编辑器或 Excel 中打开文件,则需要将数据保存为 CSV 格式。请注意,Parquet 文件确实有一些可视化工具,例如 TAD

性能

使用 DuckDB 操作文件主要有两种方式:

  1. 用户从文件创建 DuckDB 表,并在未来的查询中使用该表。这是一个加载过程,通常用于将数据存储为 DuckDB 表或对它们运行大量查询的情况。此外,请注意,对于大多数数据库系统(例如 Oracle、SQL Server、PostgreSQL、SQLite 等),这些是唯一可能的情况。

  2. 可以直接在文件扫描器上运行查询,而无需创建表。这对于用户在内存和磁盘空间方面存在限制,或者对这些文件的查询只执行一次的场景非常有用。请注意,这种情况通常不受数据库系统支持,但对于数据帧库(例如 Pandas)来说很常见。

为了公平比较这些扫描器,我们预先提供了表模式,确保扫描器生成完全相同的数据类型。我们还设置了 preserve_insertion_order = false,因为这可能会影响两个扫描器的并行化,并设置 max_temp_directory_size = '0GB' 以确保没有数据溢出到磁盘,所有实验都在内存中完全运行。

我们对 CSV 文件和 Parquet(使用默认的 Snappy 压缩)都使用了默认写入器,并且还运行了一个 Parquet 变体,其中包含 CODEC 'zstd', COMPRESSION_LEVEL 1,因为这可以加快查询/加载时间。

对于所有实验,我们使用了一台配备 64 GB RAM 的 Apple M1 Max 电脑。我们使用 TPC-H 比例因子 20,并报告 5 次运行的中位数时间。

创建表

为了创建表,我们重点关注 lineitem 表。

定义模式后,两个文件都可以通过简单的 COPY 语句加载,无需设置任何额外参数。请注意,即使定义了模式,CSV 嗅探器仍将执行以确定方言(例如,引用字符、分隔符等)并匹配类型和名称。

名称 时间 (秒) 大小 (GB)
CSV 11.76 15.95
Parquet Snappy 5.21 3.78
Parquet ZSTD 5.52 3.22

我们可以看到 Parquet 文件确实更小。大约比 CSV 文件小 5 倍,但性能差异并不剧烈。

CSV 扫描器仅比 Parquet 扫描器慢约 2 倍。值得注意的是,这些操作相关的一些成本(约 1-2 秒)与插入 DuckDB 表有关,而不是扫描器本身。

然而,在比较中考虑这一点仍然很重要。实际上,原始 CSV 扫描器比 Parquet 扫描器慢约 3 倍,这是一个显著的差异,但比人们最初想象的要小得多。

直接查询文件

我们将在文件上运行两个不同的 TPC-H 查询。

查询 01. 首先,我们运行 TPC-H Q01。此查询仅对 lineitem 表进行操作,执行带有过滤器的聚合和分组。它在一个列上进行过滤,并从 lineitem 的 16 列中投影 7 列。

因此,此查询将强调谓词下推(Parquet 读取器支持但 CSV 读取器不支持)和投影下推(两者都支持)。

SELECT
    l_returnflag,
    l_linestatus,
    sum(l_quantity) AS sum_qty,
    sum(l_extendedprice) AS sum_base_price,
    sum(l_extendedprice * (1 - l_discount)) AS sum_disc_price,
    sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) AS sum_charge,
    avg(l_quantity) AS avg_qty,
    avg(l_extendedprice) AS avg_price,
    avg(l_discount) AS avg_disc,
    count(*) AS count_order
FROM
    lineitem
WHERE
    l_shipdate <= CAST('1996-09-02' AS date)
GROUP BY
    l_returnflag,
    l_linestatus
ORDER BY
    l_returnflag,
    l_linestatus;
名称 时间 (秒)
CSV 6.72
Parquet Snappy 0.88
Parquet ZSTD 0.95

我们可以看到,直接在文件上运行此查询与简单地将数据加载到表中相比,性能差距约为 7 倍。在 Parquet 文件中,我们可以直接跳过不符合我们过滤器 l_shipdate <= CAST('1996-09-02' AS date) 的行组。请注意,此过滤器大约删除了 30% 的数据。不仅如此,我们还可以跳过不符合过滤器的单个行。此外,由于 Parquet 格式是列式存储的,我们可以完全跳过对未投影列的任何计算。

不幸的是,CSV 读取器无法从这些过滤器中受益。由于它缺乏分区,因此无法有效跳过部分数据。理论上,CSV 扫描器可以跳过不符合过滤器的行的计算,但目前尚未实现。

此外,CSV 投影会跳过列上的大部分计算(例如,它不会转换或复制值),但它仍然必须解析该值才能跳过它。

查询 21. 查询 21 不仅严重依赖谓词和投影下推,而且还显著依赖基于统计信息的连接顺序以获得良好性能。在此查询中,使用了四个不同的文件并将其连接在一起。

SELECT
    s_name,
    count(*) AS numwait
FROM
    supplier,
    lineitem l1,
    orders,
    nation
WHERE
    s_suppkey = l1.l_suppkey
    AND o_orderkey = l1.l_orderkey
    AND o_orderstatus = 'F'
    AND l1.l_receiptdate > l1.l_commitdate
    AND EXISTS (
        SELECT
            *
        FROM
            lineitem l2
        WHERE
            l2.l_orderkey = l1.l_orderkey
            AND l2.l_suppkey <> l1.l_suppkey)
    AND NOT EXISTS (
        SELECT
            *
        FROM
            lineitem l3
        WHERE
            l3.l_orderkey = l1.l_orderkey
            AND l3.l_suppkey <> l1.l_suppkey
            AND l3.l_receiptdate > l3.l_commitdate)
    AND s_nationkey = n_nationkey
    AND n_name = 'SAUDI ARABIA'
GROUP BY
    s_name
ORDER BY
    numwait DESC,
    s_name
LIMIT 100;
名称 时间 (秒)
CSV 19.95
Parquet Snappy 2.08
Parquet ZSTD 2.12

我们可以看到,这个查询现在有大约 10 倍的性能差异。我们观察到与查询 01 类似的效果,但现在我们还承担了为 CSV 文件执行无统计信息连接顺序的额外成本。

结论

毫无疑问,CSV 文件扫描的性能在过去几年中急剧提升。如果我们猜测几年前创建表时的性能差异,答案可能至少是一个数量级。

这非常出色,因为它允许从不支持性能驱动文件格式的遗留系统中导出数据。

但请注意,不要被那些超级方便和快速的 CSV 读取器所蒙蔽。您的数据最好还是保存在 Parquet 等自描述、列式二进制压缩格式中——当然也可以是 DuckDB 文件格式!它们体积更小,一致性更强。此外,由于高效的投影/谓词下推和可用的统计信息,直接在 Parquet 文件上运行查询会更有益。

需要注意的是,目前存在大量关于索引 CSV 文件(即以某种方式构建统计信息)的工作,以加速未来的查询并启用谓词下推。然而,DuckDB 尚未执行这些操作。

总结:在大多数场景下,Parquet 仍然是无可争议的冠军,但我们将继续努力,尽可能缩小这一差距。