暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

DuckDB 性能揭秘:用纽约出租车数据集挑战 CSV 读取极限

alitrack 2024-10-31
717

DuckDB 性能揭秘:用纽约出租车数据集挑战 CSV 读取极限

作者: Pedro Holanda

原文: https://duckdb.org/2024/10/16/driving-csv-performance-benchmarking-duckdb-with-the-nyc-taxi-dataset.html

image

纽约出租车数据集[1] 记录了纽约市多年出租车行程,是一个在数据库基准测试[2]机器学习[3]数据可视化[4]等领域广泛使用的重要数据集。

2022 年,数据提供方决定将数据以 Parquet 文件代替 CSV 文件发布。就性能而言,这一决定非常明智。Parquet 文件更小,并且其列式存储格式支持高效读取。然而,这使得很多系统无法直接加载这些文件。

Redshift 上的十亿出租车行程[5]的博客中,提出了一种新的数据库基准测试,用以评估在出租车数据集上执行聚合操作的性能。数据集还与天气、出租车类型以及上下车位置等其他数据集进行了连接,并且被存储为多个 gzip 压缩的 CSV 文件,每个文件约含 2000 万行数据。

作为 CSV 文件的出租车数据集

DuckDB 以其出色的 CSV 读取性能[6]闻名,我们希望通过这个基准测试的加载过程,探究是否能找到我们 CSV 读取器中的性能瓶颈。于是,我们着手生成这些数据集,并在 DuckDB 中进行性能分析。根据 AWS Redshift 的一项研究,CSV 文件是 S3 中使用最广泛的外部数据源[7],而且其中 99% 的文件都是 gzip 压缩的。因此,基准测试采用拆分后的 gzip 文件这一点引起了我们的兴趣。

本文将为您详细介绍如何在 DuckDB 中运行该基准测试,并总结我们从中学到的一些经验与未来可能的改进方向。该基准测试使用的数据集是公开提供的[8]。这个数据集被分成 65 个 gzip 压缩的 CSV 文件,每个文件包含 2000 万行数据,单个文件大小约为 1.8 GB。整个数据集压缩后为 111 GB,解压后为 518 GB。我们还提供了数据集生成的更多细节,并指出了我们分发的数据集与Redshift 上的十亿出租车行程[9]描述的原始数据集之间的差异。

复现基准测试

进行公平的基准测试是个相当复杂的问题[10],尤其是当基准测试所需的数据、查询和结果难以获取或运行时。为了简化操作,我们在 taxi-benchmark GitHub 仓库[11] 中提供了相关脚本。

仓库包含三个主要的 Python 脚本:

  1. 1. generate_prepare_data.py
    :下载所需文件并准备基准测试数据。

  2. 2. benchmark.py
    :执行基准测试并验证结果。

  3. 3. analyse.py
    :分析基准测试结果,并生成本文中讨论的部分见解。

这个基准测试并非完全完美——实际上,任何基准测试都不可能完美。但我们相信分享这些脚本是个积极的开端,我们也欢迎任何对脚本进行优化的贡献。

仓库中还包含详细的 README 文件,说明如何使用这些脚本。
这个仓库将成为本文实验的基础。

准备数据集

首先,您需要通过执行 python generate_prepare_data.py[12] 下载并准备所有文件。脚本会将 65 个文件下载到 ./data
 文件夹中,并将它们解压缩合并为一个大文件。

最终,./data
 文件夹中将有 65 个 gzip 压缩的 CSV 文件(文件名从 trips_xaa.csv.gz
 到 trips_xcm.csv.gz
)和一个包含完整数据的未压缩 CSV 文件(文件名为 decompressed.csv
)。

我们将基准测试分为以下两种场景进行:

  1. 1. 加载 65 个压缩文件。

  2. 2. 加载一个未压缩的大文件。

当文件准备就绪后,您可以通过运行 python benchmark.py[13] 来启动基准测试。

加载过程

基准测试的加载阶段会针对每个场景运行六次。前五次运行中,我们记录并取其加载时间的中位数。在第六次运行时,收集资源使用数据(例如 CPU 使用率、磁盘读写情况)。

所有加载操作都在内存中的 DuckDB 实例中执行,数据不会被持久存储到 DuckDB,而是仅在连接期间存在。由于数据集无法完全装入内存,部分数据会被写入磁盘上的临时空间。由于未持久化存储,加载过程会明显更快,但查询速度可能略慢,因为 DuckDB 使用了未压缩的格式[14]。在此次基准测试中,我们主要关注 CSV 读取器的性能,而非查询性能,因此选择不对数据进行持久化。

表的模式定义在 schema.sql[15] 文件中。

schema.sql。

CREATE TABLE trips (
    trip_id                 BIGINT,
    vendor_id               VARCHAR,
    pickup_datetime         TIMESTAMP,
    dropoff_datetime        TIMESTAMP,
    store_and_fwd_flag      VARCHAR,
    rate_code_id            BIGINT,
    pickup_longitude        DOUBLE,
    pickup_latitude         DOUBLE,
    dropoff_longitude       DOUBLE,
    dropoff_latitude        DOUBLE,
    passenger_count         BIGINT,
    trip_distance           DOUBLE,
    fare_amount             DOUBLE,
    extra                   DOUBLE,
    mta_tax                 DOUBLE,
    tip_amount              DOUBLE,
    tolls_amount            DOUBLE,
    ehail_fee               DOUBLE,
    improvement_surcharge   DOUBLE,
    total_amount            DOUBLE,
    payment_type            VARCHAR,
    trip_type               VARCHAR,
    pickup                  VARCHAR,
    dropoff                 VARCHAR,
    cab_type                VARCHAR,
    precipitation           BIGINT,
    snow_depth              BIGINT,
    snowfall                BIGINT,
    max_temperature         BIGINT,
    min_temperature         BIGINT,
    average_wind_speed      BIGINT,
    pickup_nyct2010_gid     BIGINT,
    pickup_ctlabel          VARCHAR,
    pickup_borocode         BIGINT,
    pickup_boroname         VARCHAR,
    pickup_ct2010           VARCHAR,
    pickup_boroct2010       BIGINT,
    pickup_cdeligibil       VARCHAR,
    pickup_ntacode          VARCHAR,
    pickup_ntaname          VARCHAR,
    pickup_puma             VARCHAR,
    dropoff_nyct2010_gid    BIGINT,
    dropoff_ctlabel         VARCHAR,
    dropoff_borocode        BIGINT,
    dropoff_boroname        VARCHAR,
    dropoff_ct2010          VARCHAR,
    dropoff_boroct2010      BIGINT,
    dropoff_cdeligibil      VARCHAR,
    dropoff_ntacode         VARCHAR,
    dropoff_ntaname         VARCHAR,
    dropoff_puma            VARCHAR);

65 个文件的加载器使用以下 SQL 语句:

COPY trips FROM 'data/trips_*.csv.gz' (HEADER false);

未压缩的单个文件使用以下 SQL 语句加载:

COPY trips FROM 'data/decompressed.csv' (HEADER false);

查询

数据加载完成后,基准测试脚本会运行每个基准查询[16]五次,并记录其执行时间。此外,查询结果会与相应的参考答案[17]进行验证,以确保测试结果的准确性。所有查询与原始十亿出租车行程[18]基准测试中使用的查询完全一致。

结果

加载时间

尽管我们处理的是一个包含 51 列的大型 CSV 数据集,DuckDB 仍能相当快速地完成数据加载。

需要注意的是,DuckDB 默认保留数据的插入顺序,这对性能有一定影响。以下结果中的所有数据集加载操作都将此选项设置为 false

SET preserve_insertion_order = false;

所有实验均在 Apple M1 Max(64 GB RAM)上运行,我们比较了单个未压缩 CSV 文件和 65 个压缩 CSV 文件的加载时间。

名称时间(分钟)CPU 使用率与 100% 的平均偏差
单文件——未压缩11:5231.57
多文件——压缩13:5227.13

从多个压缩文件加载数据比从单个未压缩文件加载更具 CPU 效率。多个压缩文件的 CPU 使用率偏差较小,表明 CPU 空转时间更少。主要有两个原因:(1)压缩文件比未压缩文件小约 8 倍,大大减少了从磁盘加载的数据量,从而减少了 CPU 等待数据处理的停顿时间;(2)并行化多个文件的加载比单个文件更容易,因为每个线程都可以独立处理一个文件。

CPU 效率差异也体现在执行时间上:从单个未压缩文件加载数据比从多个压缩文件快 2 分钟。这主要是由于我们当前的解压算法设计存在局限性。读取压缩文件包括三项操作:(1)将数据从磁盘加载到压缩缓冲区;(2)解压数据至解压缓冲区;(3)处理解压缓冲区。当前实现中,任务 1 和任务 2 被合并为一个操作,导致在当前缓冲区完全解压之前无法继续读取,进而导致 CPU 空转时间增加。

内部工作机制

我们可以进一步查看内部数据处理的情况,以验证加载时间的差异。

下图展示了 “单文件——未压缩” 运行时 CPU 和磁盘的利用率快照。可以看到,达到 100% CPU 利用率具有挑战性,由于数据被写入磁盘表,我们经常遇到停顿现象。另一个关键点是,CPU 利用率与磁盘读取高度相关,说明线程经常在处理数据之前等待数据加载。如果为 CSV 读取器/写入器引入异步 IO 技术(在后台进行输入/输出操作),并行处理性能将显著提升,因为单线程就能高效处理大部分磁盘 IO 操作,同时不影响 CPU 利用率。

未压缩加载统计

下图展示了 65 个压缩文件加载时的类似快照。虽然我们仍然在数据写入时遇到停顿,但由于压缩文件的数据体积较小,CPU 利用率显著提高。在这种情况下,数据的并行化处理更为简单。与未压缩文件类似,CPU 利用率中的空隙可以通过引入异步 IO 以及优化的解压算法来缓解。

压缩加载统计

查询时间

为了更加全面,我们还展示了在配备 M1 Pro CPU 的 MacBook Pro 上四个查询的执行时间。此对比展示了使用纯内存连接(无存储)与数据加载并持久化到数据库后的查询时间差异。

名称无存储时间(秒)有存储时间(秒)
Q 012.451.45
Q 023.890.80
Q 035.212.20
Q 0411.23.12

最大的差异在于,当 DuckDB 使用持久化存储时,数据被高效压缩[19],因此查询数据集的速度快得多[20]。相比之下,当我们不使用持久化存储时,内存数据库会临时将数据存储在未压缩的 .tmp
 文件中,以便处理内存溢出,这增加了磁盘 IO 负担,从而导致查询时间更长。这一发现引发了我们一个新的探索方向:是否可以通过对临时数据进行压缩来提升性能。

数据集生成过程

原始博客文章基于纽约市出租车和豪华轿车委员会发布的 CSV 文件生成了这个数据集。最初,这些文件包括精确的上车和下车经纬度坐标。然而,从 2016 年中期开始,为了保护隐私,这些精确坐标被模糊化为上车和下车位置的几何对象(甚至有故事说通过检查出租车的目的地导致婚姻破裂)。此外,近年来 TLC 决定将数据重新分发为 Parquet 文件,并且完全匿名化了所有经纬度数据,包括 2016 年中期之前的数据。

这为基准测试带来了问题,因为Redshift 上的十亿出租车行程[21]博客依赖于这些详细的位置信息。让我们来看一下以下的数据片段:

649084905,VTS,2012-08-31 22:00:00,2012-08-31 22:07:00,0,1,-

73.993908,40.741383000000006,-73.989915,40.75273800000001,1,1.32,6.1,0.5,0.5,0,0,0,0,7.1,CSH,0,0101000020E6100000E6CE4C309C7F52C0BA675DA3E55E4440,0101000020E610000078B471C45A7F52C06D3A02B859604440,yellow,0.00,0.0,0.0,91,69,4.70,142,54,1,Manhattan,005400,1005400,I,MN13,Hudson Yards-Chelsea-Flatiron-Union Square,3807,132,109,1,Manhattan,010900,1010900,I,MN17,Midtown-Midtown South,3807

我们看到精确的经纬度坐标:-73.993908,40.741383000000006,-73.989915,40.75273800000001
,以及根据这些经纬度生成的 PostGIS 几何十六进制 blob:0101000020E6100000E6CE4C309C7F52C0BA675DA3E55E4440,0101000020E610000078B471C45A7F52C06D3A02B859604440
(使用 ST_SetSRID(ST_Point(longitude, latitude), 4326)
 生成)。

由于这些详细信息对数据集非常重要,因此无法再像原博客中描述的那样生成文件,因为这些详细位置信息已经缺失。然而,互联网从不遗忘。通过多方查找,我们找到了各种来源分发的原始数据集,例如 [1][22][2][23],和 [3][24]。借助这些资源,我们将原始 CSV 文件与博客[25]中提到的天气信息相结合,生成了与原始博客类似的数据集。

此数据集与原始数据集有何不同?

我们的数据集与 “Redshift 上的十亿出租车行程” 博客中的数据集存在两个显著差异:

  1. 1. 我们的数据集包含截至 2016 年 6 月 30 日的精确经纬度信息,而原始文章仅使用了截至 2015 年底的数据(因为该文章发表于 2016 年 2 月)。

  2. 2. 我们的数据集包含了 Uber 行程数据,而原始数据集中未包含。

如果您希望使用与原始数据集更接近的数据进行基准测试,可以通过筛选掉额外数据生成新表。例如:

CREATE TABLE trips_og AS
    FROM trips
    WHERE pickup_datetime < '2016-01-01'
      AND cab_type != 'uber';

结论

本文介绍了如何在 DuckDB 上运行出租车数据基准测试。我们提供了完整的脚本,便于您在喜欢的系统上进行基准测试。我们还展示了如何利用这个基准测试来评估 DuckDB 的性能,并从中获得对未来改进方向的深入见解。

引用链接

[1]
 纽约出租车数据集: https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page
[2]
 数据库基准测试: https://tech.marksblogg.com/benchmarks.html
[3]
 机器学习: https://www.r-bloggers.com/2018/01/new-york-city-taxi-limousine-commission-tlc-trip-data-analysis-using-sparklyr-and-google-bigquery-2/
[4]
 数据可视化: https://www.kdnuggets.com/2017/02/data-science-nyc-taxi-trips.html
[5]
 Redshift 上的十亿出租车行程: https://tech.marksblogg.com/billion-nyc-taxi-rides-redshift.html
[6]
 出色的 CSV 读取性能: https://x.com/jmduke/status/1820593783005667459
[7]
 CSV 文件是 S3 中使用最广泛的外部数据源: https://assets.amazon.science/24/3b/04b31ef64c83acf98fe3fdca9107/why-tpc-is-not-enough-an-analysis-of-the-amazon-redshift-fleet.pdf
[8]
 公开提供的: https://github.com/pdet/taxi-benchmark/blob/0.1/files.txt
[9]
 Redshift 上的十亿出租车行程: https://tech.marksblogg.com/billion-nyc-taxi-rides-redshift.html
[10]
 相当复杂的问题: https://pdet.github.io/assets/papers/benchmarking.pdf
[11]
 taxi-benchmark GitHub 仓库: https://github.com/pdet/taxi-benchmark
[12]
 python generate_prepare_data.py: https://github.com/pdet/taxi-benchmark/blob/0.1/generate_prepare_data.py
[13]
 python benchmark.py: https://github.com/pdet/taxi-benchmark/blob/0.1/benchmark.py
[14]
 DuckDB 使用了未压缩的格式: https://duckdb.org/docs/guides/performance/how_to_tune_workloads#persistent-vs-in-memory-tables
[15]
 `schema.sql`: https://github.com/pdet/taxi-benchmark/blob/0.1/sql/schema.sql
[16]
 基准查询: https://github.com/pdet/taxi-benchmark/tree/0.1/sql/queries
[17]
 参考答案: https://github.com/pdet/taxi-benchmark/tree/0.1/sql/answers
[18]
 十亿出租车行程: https://tech.marksblogg.com/benchmarks.html
[19]
 高效压缩: https://duckdb.org/2022/10/28/lightweight-compression.html
[20]
 查询数据集的速度快得多: https://duckdb.org/docs/guides/performance/how_to_tune_workloads%7D#persistent-vs-in-memory-tables
[21]
 Redshift 上的十亿出租车行程: https://tech.marksblogg.com/billion-nyc-taxi-rides-redshift.html
[22]
 [1]: https://arrow.apache.org/docs/6.0/r/articles/dataset.html
[23]
 [2]: https://catalog.data.gov/dataset/?q=Yellow+Taxi+Trip+Data&sort=views_recent+desc&publisher=data.cityofnewyork.us&organization=city-of-new-york&ext_location=&ext_bbox=&ext_prev_extent=
[24]
 [3]: https://datasets.clickhouse.com/trips_mergetree/partitions/trips_mergetree.tar
[25]
 博客: https://github.com/toddwschneider/nyc-taxi-data


文章转载自alitrack,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论