Melih Birim

csvql 在 8 GB 檔案上查詢原始 CSV 的速度比 DuckDB 快約 2.8 倍,記憶體使用量少約 6 倍,且無需額外磁碟空間——而證明這一點的基準測試也揭露了兩個我原本永遠不會發現的真正錯誤。

設定:同一台機器上查詢相同的原始 CSV

DuckDB 發布了它自己的紐約計程車 CSV 基準測試。這使得它成為完美的標竿:當我使用競爭對手自己的資料集和查詢來執行競爭對手的基準測試時,我不會被指控配置錯誤。

該資料集位於 DuckDB 自己的 blob 儲存空間(blobs.duckdb.org/data/nyc-taxi-dataset)。每個檔案有 2,000 萬列,51 欄,壓縮後約 8 GB。四個查詢是典型的「十億計程車乘次」聚合——按計程車類型、乘客數量、年份和四捨五入的行程距離進行 GROUP BY 計數和平均值計算。

讓這種基準測試成敗的唯一規則:兩個引擎必須執行相同的工作。DuckDB 的標題數字有兩種形式——「有儲存」(將 CSV 載入 DuckDB 的原生列式格式後)和「無儲存」(每次查詢原始 CSV)。csvql 始終直接查詢原始 CSV;它沒有原生儲存。因此,公平的競爭是兩個引擎每次都讀取原始 CSV,冷啟動

csvql:  csvql "SELECT ... FROM 'trips.csv' ..."
duckdb: SELECT ... FROM read_csv_auto('trips.csv') ...   -- NOT a preloaded table

Enter fullscreen mode Exit fullscreen mode

將 csvql-on-raw-CSV 與 DuckDB-on-preloaded-store 進行比較是不誠實的,任何打開腳本的人都會這麼說。直接對直接是唯一能經得起檢驗的比較。

機器: Apple Silicon,macOS。DuckDB: v1.4.2。方法: 每個查詢執行 5 次取最佳結果,暖 OS 頁面快取(兩個引擎均等)。


速度:在 8 GB 上約 2.8 倍

查詢 csvql DuckDB 加速比
Q01 COUNT(*) GROUP BY cab_type 1.29 s 3.55 s 2.8x
Q02 AVG(total_amount) GROUP BY passenger_count 1.41 s 3.81 s 2.7x
Q03 COUNT(*) GROUP BY passenger_count, year 1.36 s 4.01 s 2.9x
Q04 GROUP BY passenger_count, year, ROUND(distance) 1.38 s 3.99 s 2.9x

查詢延遲,越低越好:

        0s        1s        2s        3s        4s
Q01 csvql  ██████▌ 1.29
    duck   ██████████████████ 3.55
Q02 csvql  ███████ 1.41
    duck   ███████████████████ 3.81
Q03 csvql  ██████▊ 1.36
    duck   ████████████████████ 4.01
Q04 csvql  ██████▉ 1.38
    duck   ████████████████████ 3.99

Enter fullscreen mode Exit fullscreen mode

這是反直覺的部分:隨著檔案變大,差距會縮小。在 417 MB / 100 萬列的樣本上,csvql 快約 10 倍。在 8 GB 上,約 2.8 倍。為什麼?在小檔案上,DuckDB 的程序啟動和 CSV 讀取器初始化主導了執行時間,而 csvql 的精簡啟動贏得很大。在大檔案上,實際的解析和聚合吞吐量主導,DuckDB 的並行 CSV 讀取器迎頭趕上,而 csvql 穩定在約 2.8 倍。兩個數字都是真實的;誠實的做法是發布附加檔案大小的完整曲線,而不是挑選 10 倍的數據。


記憶體:少約 6 倍

同樣的 8 GB 檔案,每個查詢的峰值記憶體佔用:

查詢 csvql DuckDB
Q01 29 MB 178 MB
Q02 30 MB 210 MB
Q03 34 MB 208 MB
Q04 38 MB 219 MB
Peak memory (MB), 8 GB file
csvql   █▍                          ~30 MB
duckdb  ██████████████████████████  ~200 MB

Enter fullscreen mode Exit fullscreen mode

csvql 通過記憶體映射掃描流式處理檔案,只將 group-by 雜湊表保留在記憶體中——幾十個群組。它一次最多只保留 8 GB 的一小部分。這就是為什麼 30 MB 的佔用可以處理 8 GB 檔案的原因。


儲存:真正重要的數字

這就是「原始 CSV,無儲存」不再是公平性警告而成為全部重點的地方。

Extra disk required to answer these queries on the 8 GB file:

csvql   0 bytes        — queries the CSV in place, no ingest
duckdb  2.1 GB + 21.7s — must build a native store for its fast path

Enter fullscreen mode Exit fullscreen mode

DuckDB 可以比這些原始 CSV 數字更快——但只能通過其「有儲存」路徑,該路徑首先將 CSV 載入 2.1 GB 原生儲存,單個 8 GB 檔案的一次性成本約 22 秒(完整多檔案資料集需要數分鐘)。csvql 達到其數字需要零額外磁碟和零載入。對於對五分鐘前出現在磁碟上的 CSV 進行臨時查詢,「無載入」是立即得到答案與喝咖啡休息後得到答案之間的差別。

誠實的總結:相同的答案,約快 2.8 倍,記憶體少約 6 倍,零額外磁碟,零載入。


無法執行的測試——以及為什麼這是誠實的部分

你會注意到我沒有將 csvql 的數字與 DuckDB 的已發布數據進行比較(他們的部落格報告 Q01–Q04 在無儲存情況下為 2.45 / 3.89 / 5.21 / 11.2 秒)。這看起來會很不錯——csvql 的約 1.3 秒與 Q04 上的 11.2 秒相比是令人興奮的 8 倍。這也會是垃圾。

兩個原因:

  1. 資料集大小不同。DuckDB 發布的查詢數字是在完整約 11 億列的資料集上(所有 65 個檔案,約 500 GB 未壓縮 CSV)。我的是在單個 2,000 萬列檔案上。這是 55 倍的列數差異。將它們放在同一張表中相當於將 csvql-on-20M 與 DuckDB-on-1.1B 進行比較——基準測試等同於固定比賽。(你可以在他們自己的 Q04 中看到大小差距:1.1B 列上的 11.2 秒與我的本地 DuckDB 在 20M 上的約 4 秒。)

  2. 硬體不同。他們的數字來自 M1 Pro;我的是來自不同的 Apple Silicon 機器。借用在其他矽片上測量的競爭對手數字並除以你的數字不是基準測試,而是願望。

合法地引用 DuckDB 的已發布數據,csvql 必須執行相同的約 11 億列資料集——這意味著磁碟上約 500 GB 未壓縮 CSV。我的機器有 275 GB 可用空間。它實際上裝不下。因此,我沒有在硬體和資料大小之間借用數字,而是在同一台機器上使用相同的 OS 快取狀態,在相同的 2,000 萬列上執行兩個引擎。這控制了跨硬體比較無法控制的一切。這是一個更小、更不華麗的結果——但也是一個更值得信賴的結果。

如果有人有 1 TB 磁碟想要執行完整的 500 GB 並報告 csvql 與 DuckDB 的確切已發布表格相比的結果,腳本只需一個標誌即可(bench_taxi.sh 65)。我很想看到它。


情節轉折:基準測試在我的引擎中找到了兩個錯誤

我建立基準測試來測量速度。最終它成為 csvql 曾經擁有的最佳測試套件,因為在真實資料上執行真實查詢立即打破了其中兩個:

錯誤 #1 — GROUP BY ROUND(trip_distance) 崩潰並顯示 ColumnNotFound字串生成函數(STRFTIME)分組可以正常工作;按數值生成函數(ROUND)分組則不行——群組鍵解析器根本沒有處理這種情況的案例。Q04 在我新增一個案例之前根本無法執行。

錯誤 #2 — ORDER BY COUNT(*) DESC 靜默返回未排序的列。沒有錯誤,沒有崩潰——只是錯誤的輸出。GROUP BY 代碼路徑按內部雜湊鍵對結果進行排序,完全忽略 ORDER BY 子句。這是最可怕的一類錯誤:返回自信但錯誤答案的那種。只檢查時序的基準測試會直接忽略它;將輸出與 DuckDB 進行比較會在一次差異比較中捕獲它。

兩個錯誤現在都已修復,每個都有回歸測試,四個標準查詢產生與 DuckDB 位元相同的結果(11.0 浮點格式除外)。道德:針對受信任的預言機檢查的基準測試是戴著秒錶的正確性測試。如果我只對 csvql 本身執行,兩個錯誤都會仍然存在。


自己重現

一切都在倉庫中。不需要 500 GB——樣本模式拉取約 417 MB 並在幾秒內執行:

git clone https://github.com/melihbirim/csvql
cd csvql && zig build -Doptimize=ReleaseFast

# quick: 1M-row sample (~417 MB)
./bench/bench_taxi.sh --sample

# full: 20M rows (~8 GB download from DuckDB's blobs)
./bench/bench_taxi.sh 1

# memory / CPU / storage instead of speed
./bench/bench_taxi.sh --resources 1

Enter fullscreen mode Exit fullscreen mode

腳本下載 DuckDB 自己的資料集,在原始 CSV 上執行兩個引擎,並打印上面的表格。它是約 130 行的 bash——在你信任它之前閱讀它。


要點

  • 直接對直接進行基準測試,否則不要麻煩。整個練習中最重要的行是拒絕將原始 CSV csvql 與預載入的 DuckDB 進行比較。
  • 發布整個曲線。在 400 MB 上約 10 倍,在 8 GB 上約 2.8 倍都是真實的。隱藏一個以標題另一個是基準測試獲得壞名聲的方式。
  • 有趣的數字不總是速度。對於臨時 CSV 工作,少約 6 倍記憶體和零載入比節省兩秒更重要。
  • 將你的基準測試指向受信任的預言機。它兼作你懶得編寫的正確性測試。它發現了兩個我本會發布的錯誤。

csvql 是開源的(Zig,單個靜態二進位檔案,github.com/melihbirim/csvql)。如果你執行完整資料集,請告訴我你得到了什麼。

數字:Apple Silicon M2Pro,macOS,DuckDB v1.4.2,5 次取最佳,暖快取。資料集和查詢:DuckDB 的紐約計程車基準測試