DuckDBが思った以上に便利だった話

この記事は CARTA TECH BLOG アドベントカレンダー2024 の12/24の記事です。こんにちは、新卒入社したCARTA MARKETING FIRMのebittです。入社以来、広告ドメインとデータ基盤に跨って大量のデータを扱う業務の多い1年でした。色々壁にぶち当たり、悩み、正直まだまだ学びしかありません。さて、この記事では大量のデータを素早く分析する処理に長けたDuckDBを最近お試しした話を書いていこうと思います。

DuckDBとは?

DuckDBはOLAP用途での利用を想定された無料・オープンソースの組み込みRDBMSです。 S3などのリモートやローカルからCSVやJSONを読み込んで、テーブルを作成してSQLでクエリできます。最近社内で「DuckDBを使ってCSVにパッとクエリできて良かった」「便利だった」などと利用事例をちょくちょく目にするようになり気になったので、試しに手元で動かしてみることにしました。

DuckDBの強み

SQLが使える

独自言語を使わないツールは覚えることがそれだけで1つ少ないので嬉しいですね。普段から業務でSQLを書くので個人的にめちゃめちゃありがたかったです。

移植性が高い

Linux,Mac,Windowsなどの主要なOSとx86,ARMなどの主要なCPUアーキテクチャでは一通り動作するようです。Wasm版のDuckDB-Wasmも出ているのでブラウザ上でも動作してくれます。

拡張性が高い

DuckDBは導入当初ではhttpやhttpsプロトコルなどの利用はできないのですが、拡張によってそれらの利用が可能になります。これによってローカルにあるファイルはもちろん、GitHubなどのパブリックなホストやS3などの各種クラウドストレージに加えRDSなどのリモートにあるRDBにも横断的にアクセスできます。また、CSV,JSON,Parquetなどの主要なファイルフォーマットに対応しているのも嬉しいポイントです。

アプリケーションのリソースを使ってクエリを実行できる

これはDuckDBが組み込みである点によるものですが、PostgreSQLのようなサーバ・クライアント型のRDBMSと違いDuckDBはクエリの実行をアプリケーションのプロセス内で行います。つまり比較的重めなクエリであってもアプリケーションのリソースを使って実行されるので、既にデータベースが存在している場合には導入時のデータベースのサーバコストが抑えられるというメリットがあります。

無料!

最高!!

参考:Why DuckDB

環境セットアップ

筆者の手元の環境はMacOS Sonoma Ver14.4です。 今回はbrew install duckdbで導入しました。

DuckDBの公式ドキュメントにインストール手順が載っているので実際に手元に導入してお試ししてみたい方はぜひそちらへどうぞ。

また、この後の作業でGitHub上のCSVファイルを取得するために、httpfsという拡張を導入しています。

基本的な操作

GitHub上のCSVにクエリする

データの操作には標準SQLをそのまま使えます。覚えることが少なくてGOODです! 基本的な操作を見ていくためにDuckDB in Actionの2章を参考に、GitHub上にある各国の人口データを示したサンプルのCSVデータにDuckDBを用いてクエリしてみます。このようにリモートのURLを指定してあげることで、自動でテーブルを作成してくれているのが分かります。また、CSVのカラムの推論も自動でやってくれるのでデータさえあればあとはSELECTするだけでアクセスできます。

D SELECT count(*)
  FROM 'https://github.com/bnokoro/Data-Science/raw/master/countries%20of%20the%20world.csv';
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│         2099 │
└──────────────┘
D SELECT Country FROM 'https://github.com/bnokoro/Data-Science/raw/master/countries%20of%20the%20world.csv';
┌───────────────────────┐
│        Country        │
│        varchar        │
├───────────────────────┤
│ Afghanistan           │
│ Albania               │
│ Algeria               │
│ American Samoa        │
(以下省略)

ただし対応していない拡張子の場合は要注意

ちなみに、調査でログファイルにクエリしたい時に拡張子が.logとか.txtなんだけど実は中身はCSVファイルなんだよなってことありませんか?DuckDBはその辺もいい感じにやってくれます。

まず以下のように先ほどクエリしたGitHub上のCSVファイルをbit.lyにかけてURLを変換します。

https://bit.ly/49B3XDQ

そして、先ほどと同様に今度はこのbit.lyのURLのCSVファイルに対して行数を取得するようクエリするとどうなるでしょう?

SELECT count(*)
  FROM 'https://bit.ly/49B3XDQ';
Catalog Error: Table with name https://bit.ly/49B3XDQ does not exist!
Did you mean "pg_attribute"?
LINE 2: FROM 'https://bit.ly/49B3XDQ';

エラーになりました。なぜかというとDuckDBは明示的にcsvと拡張子の付いていないファイルはcsvとして認識してくれないからです。

そういう時は、以下のようにread_csv_auto関数が使えます。拡張子に関わらずファイルの中身がCSV形式でさえあれば読み込んでくれるようになるのです。これで拡張子を直すためにいちいちダウンロードしてファイル名を変更して...なんてことをやる必要はありませんね。

SELECT count(*)
  FROM read_csv_auto('https://bit.ly/49B3XDQ');
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│         2099 │
└──────────────┘

もっと大規模なデータを読み込ませてみる

DuckDBは自身が達成しようとしたゴールを述べたWhy DuckDBの中にFastと掲げており、以下のように続けています。

DuckDB is designed to support analytical query workloads, also known as online analytical processing (OLAP). These workloads are characterized by complex, relatively long-running queries that process significant portions of the stored dataset, for example aggregations over entire tables or joins between several large tables.

大規模なデータを処理する複雑で重めなクエリ、つまり分析クエリのワークロードをサポートするように設計されていると書かれてあります。そしてそれがFastという見出しで書かれているので、分析クエリがどれくらい速いか確かめてみることにしました。

今回はモバイル端末が発するアプリ起動などの各種イベントを通知してくれる、Mobile Measurement Partnerという事業者からのイベントログを22GBほどローカルに用意しました。ちなみにCARTA MARKETING FIRMで行っている広告配信とは関係のないイベントログを今回は使っているのでご了承ください。

~/test_duckdb 
❯ du -sh .          
 22G    .

まずテーブルを作成してみます。20秒かからないくらいで終わりました。22GBというファイルサイズを踏まえると速く感じられますね。

D select * from read_json("./raw-*");
100% ▕████████████████████████████████████████████████████████████▏ 
┌──────────────────────┬─────────┬───────────┬───┬──────────────────────┬──────────────────────┬──────────────────────┐
│        log_id        │  event  │  channel  │ … │ receiver_time_unix…  │       log_name       │   context_metadata   │
│         uuid         │ varchar │  varchar  │   │        int64         │       varchar        │ struct("***.******…  │
├──────────────────────┼─────────┼───────────┼───┼──────────────────────┼──────────────────────┼──────────────────────┤
│ f4eef3dc-479c-47ca…  │ *****   │ ***** │ … │           ********** │ raw-******-***-***…  │ {'***.******': **…  │

行数は860万行余りあります。

D select count(*) as count from read_json("raw-*");
100% ▕████████████████████████████████████████████████████████████▏ 
┌─────────┐
│  count  │
│  int64  │
├─────────┤
│ 8651143 │
└─────────┘

今度はイベントログの中のjsonを深掘りして特定のフィールドの値を使って集計してみます。イベントログに紐づく都市名ごとにイベント数上位10位までを出力するクエリです。今回も20秒かからないくらいで終わってくれています。ローカルでこれだけサクサク動いてくれたら文句ないですね。

D select request_info.parameters['city'][1] as city, count(*) as count from read_json("raw-*") group by city order by count desc limit 10;
100% ▕████████████████████████████████████████████████████████████▏ 
┌──────────────┬─────────┐
│     city     │  count  │
│  varchar[]   │  int64  │
├──────────────┼─────────┤
│              │ 1496334 │
│ [Tokyo]      │ 1431314 │
│ [Osaka]      │  395700 │
│ [Jakarta]    │  247852 │
│ [Roppongi]   │  188235 │
│ [Nagoya]     │   76004 │
│ [Kyoto]      │   74277 │
│ [Phnom Penh] │   67319 │
│ [Yokohama]   │   66449 │
│ [Nakano]     │   60098 │
├──────────────┴─────────┤
│ 10 rows      2 columns │
└────────────────────────┘

感想とまとめ

開発者がすぐに取り入れられそうな利用シーンとしてはこれまでローカルでjqを使っていた部分になりそうです。「Better jq」的なイメージで、調査・集計を行いたい場合には大規模なデータセットであっても高速に動作してくれてとても便利でした。

特にjqと比べて、「SQLが使える点」「csv,json,parquetなどの複数のファイルフォーマットを扱える点」「クラウドストレージへ直接クエリできる点」が比較優位にあると思います。例えば「RDSにある表とS3上にあるjsonをjoinして調査したい」というような場面でも、DuckDBならローカルで、かつワンストップで対応できるでしょう。

また、pythonのAPIも用意されているのでStreamlitと組み合わせてデータ分析・可視化ツールを手早く作るといったユースケースにも良いのではと感じました。wasm版ですが、ブラウザ上でS3へクエリを投げるサンプルが@voluntasさんから公開されているので使用感を掴みたい方は触ってみてください。

DuckDB-Wasm + Parquet + S3-compatible object storage + OPFS

最後に、Mobile Measurement Partnerからのイベント数の多い上位10都市は日本の都市ばかりでしたが、海外の都市が少し混じっていたのが気になったので個人的に深掘ってみたいと思います。

それでは皆様、Merry Christmas🎄