DuckDBによるアドホック分析:数千のCSVをダッシュボードにまとめる
Amazon Seller Centralは、多くのアナリストのツールでは処理が追いつかないペースでデータを生成します。トラフィックレポート、コンバージョンデータ、在庫状況のサマリー、事業実績の出力、広告キャンペーンの内訳が、それぞれCSVまたはTSVで提供されます。数か月、数年と蓄積すると、ディレクトリには数千のファイルが並びます。通常はすべてをデータベースに取り込みます。しかし、読み取りが中心でスキーマが変わりやすい分析処理のために、PostgreSQLインスタンスを構築・維持し、クエリを実行するのは、課題の規模に見合わない運用負荷を生みます。
当社は欧州の5つのマーケットプレイスで販売するAmazon出品者向けに、レポートパイプラインを構築しました。元データは6年分のSeller Central出力、4,200ファイルで、ビジネスレポート、広告実績、在庫スナップショット、FBA手数料台帳を含みます。最初に採用したPostgreSQLのスキーマとETLスクリプトは、安定するまでに3週間かかりました。AmazonがAPIのバージョンごとに列名やレポート構造を改定するたび、継続的な対応が必要でした。クエリは高速でしたが、保守には時間がかかりました。
DuckDBなら、この判断が変わります。組み込み型の分析データベースで、分析コードと同じプロセス内で動作し、サーバー、デーモン、接続文字列を必要とせず、ディスク上のファイルを直接読み取ります。標準SQLでクエリを実行でき、大規模な集計に最適化した列指向エンジンの性能を備えています。このような処理では、単に使えるだけでなく、適切なツールです。
DuckDBの仕組み
最も重要なのは、列指向と行指向の違いです。PostgreSQLは行単位でデータを配置し、1つのレコードの全フィールドをディスク上に連続して保存します。SELECT revenue, units_sold FROM orders WHERE marketplace = 'DE'を実行すると、シーケンシャルスキャンでは走査する各行の全フィールドを読み、不要なものを除いて、指定した2つを返します。インデックスやインデックスオンリースキャンによって読み取り範囲を絞れる場合もありますが、テーブルの大部分を対象とする広範な集計ではあまり役立ちません。個々のレコードを挿入・更新・取得するトランザクション処理には効率的ですが、数列だけを使って数百万行を集計する分析クエリにはあまり適していません。
DuckDBは列単位でデータを配置します。revenueの値をまとめ、units_soldの値もまとめて保存します。同じクエリでは、必要な3列(結果として返す2列と、フィルター用のmarketplace)だけを読み、残りをすべて省略できます。40列のデータセットでは小さな最適化ではなく、少数の列だけを使うクエリではI/Oを約90%以上削減できる場合があります。さらに、1行ずつではなくSIMDのCPU命令でデフォルトで2,048個の値をまとめて処理するベクトル化実行を組み合わせることで、特にParquetのような列指向形式では、同じハードウェア上の十分に調整されたPostgreSQLインスタンスを上回る分析性能を実現できる場合があります。
組み込み型の構成も同じくらい重要です。ネットワークの往復や認証の追加処理がなく、接続プールを管理する必要もありません。DuckDBはファイルやディレクトリを開き、クエリプランを作って実行します。バイナリは100MB未満です。ノートパソコン、Lambda関数、cronジョブから呼ぶPythonスクリプトでも動きます。セルフホスト型の分析パイプラインでは、運用管理の対象をほぼゼロにできます。
ファイルに直接SQLを実行する
Seller Centralデータに特に有効なのが、グロブパターンによるファイルの一括読み取りです。1つのSQL文で数千のCSVを同時に検索できます。
SELECT
date_trunc('month', "Date") AS month,
SUM("Units Ordered") AS units,
SUM("Ordered Product Sales") AS revenue
FROM read_csv_auto('/data/seller-central/business-reports/*.csv')
GROUP BY 1
ORDER BY 1;
read_csv_autoはファイルの内容からスキーマを推論します。行をサンプリングして列の型を検出し、異なる区切り文字に対応し、union_by_name=trueを指定すると、列構成が違うファイル間で列名に基づいて列を統合します(デフォルトでは最初のファイルのスキーマを使い、後続のファイルを列の位置で対応付けます)。名前が変更された列を自動で対応付けることはできず、クエリ内でエイリアスやCOALESCEを使う必要があります。過去3年間でAmazonが少なくとも4回列名を改定したSeller Centralデータでは、これが重要です。実行時にディレクトリにあるファイルを直接検索するため、取り込みもスキーマ移行も不要で、古いキャッシュを無効化する必要もありません。
広告コンソールから出力したTSVにも、形式を明示して同じ方法を使えます。
FROM read_csv('/data/advertising/*.tsv', delim=' ', header=true)
単なる便利機能ではなく、ファイルベースのデータの扱い方そのものが変わります。ファイルシステムがデータベースとなり、スキーマは宣言ではなく推論で決まります。前処理なしで、複数のファイルをまたぐクエリを組み立てられます。ETLパイプラインの更新が追いつかないほど分析したい問いが変わるアドホック分析では、この構成を基本の選択にできます。
パフォーマンス特性
当社の検証用データセットは4,200ファイル、未圧縮で約2.8GBです。一般的なノートパソコン(Apple M2、16GB RAM)では、マーケットプレイス、ASIN、月ごとの売上集計が4.1秒で完了します。取り込みとインデックス作成を済ませたPostgreSQLで同等のクエリを実行すると、2.8秒です。ただし、この比較には初回取り込みの40分、Amazonの列名変更に伴うスキーマ移行、継続的なETLの保守コストが含まれません。この処理の総運用コストでは、DuckDBが大きく勝ります。
通常は複数テラバイト規模でクエリ性能がボトルネックになった場合、DuckDBはParquetを直接読み取れます。既存のCSVからParquetへの変換は1回で済みます。
COPY (FROM read_csv_auto('/data/**/*.csv'))
TO '/data/consolidated.parquet'
(FORMAT PARQUET);
その後のParquetへのクエリは、元のCSVより8〜12倍高速です。Parquetは行グループごとの最小値・最大値という列の統計情報を保存します。DuckDBはこれを使い、データの区画を丸ごと読み飛ばします。ソート済みのParquetファイルでmarketplace = 'DE'を指定すると、ディスク上のデータの10%未満を読むだけで済む場合があります。この述語プッシュダウンは、通常のCSVファイルには使えません。
Chart.jsで単体のダッシュボードを構築する
DuckDBのクエリから得られるのはデータです。実務上の課題は、その活用方法です。中小規模のAmazon販売業務の社内レポートには、Chart.jsを使った単体のHTMLファイルが、サーバー上のダッシュボードより役立つことがよくあります。サーバーを動かしたり、認証情報を管理したり、SupersetやMetabaseのインスタンスを維持したりする必要がありません。
パイプラインは3段階です。まず、DuckDBでクエリ結果をJSONに出力します。
COPY (
SELECT
strftime(date_trunc('week', "Date"), '%Y-%m-%d') AS week,
SUM("Units Ordered") AS units,
SUM("Ordered Product Sales") AS revenue
FROM read_csv_auto('/data/**/*.csv')
GROUP BY 1 ORDER BY 1
) TO 'weekly.json' (FORMAT JSON, ARRAY true);
次に、PythonスクリプトでJSONファイルを読み、CDN経由でChart.jsを参照するHTMLテンプレートにデータを直接埋め込みます。最後に、Webサーバーを使わず、そのHTMLファイルをブラウザーで開きます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Seller Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<canvas id="revenue"></canvas>
<script>
const rows = /* JSON_DATA_HERE */;
new Chart(document.getElementById('revenue'), {
type: 'line',
data: {
labels: rows.map(r => r.week),
datasets: [{ label: 'Revenue', data: rows.map(r => r.revenue), tension: 0.3 }]
}
});
</script>
</body>
</html>
生成されるのは単一のHTMLファイルです(Chart.jsをCDNから読み込むため、ライブラリをファイル内に埋め込まない限り、閲覧にはインターネット接続が必要です)。このファイルは、メールで共有し、非公開のS3バケットに置き、ローカルディレクトリから直接開くことができます。常時稼働するダッシュボードの運用負荷を避けて週次レポートを作りたいチームには、実用的で保守しやすい方法です。シェルスクリプトを実行すれば、DuckDBがCSVを検索し、PythonがHTMLを作成して、ダッシュボードを再生成できます。サーバーインフラも継続的なホスティング費用も不要で、保守対象はクエリとテンプレートスクリプトに限られます。
DuckDBがPostgreSQLより適している場合
DuckDBはPostgreSQLを置き換えるものではありません。比較が意味を持つのは、大規模な読み取り、集計、グループ化といった分析処理が中心で、書き込みより読み取りが主な場合です。同時書き込み、参照整合性、行レベルのアクセス制御、本番アプリケーション全体の運用要件では、PostgreSQLが大きく勝ります。アプリケーションがレコードを挿入・更新・検索する複数ユーザー向けシステムには、PostgreSQLが適切です。
DuckDBが適している条件は次のとおりです。
- データがすでにディスク上のファイルとして存在し、インポートステップが計画されていない
- スキーマが事前に完全に分かっていない、またはよく変更される
- クエリがアドホックでアクセスパターンが事前に予測できない
- プロジェクトの期間や規模に、データベースサーバーの運用負荷が見合わない
Amazon Seller Centralのデータは、この4条件をすべて満たします。スキーマはSeller Centralのリリースごとに変わり、出力したファイルはすでにローカルにあります。分析したい業務上の問いは、最適化の対象に応じて毎週変わります。代替手段であるPostgreSQLのETLパイプラインの保守には、DuckDBなら半日で解決できる問題に、数週間の開発時間を費やすことになります。
実務上の目安は、普段ならExcelやJupyterノートブックで分析するデータに、DuckDBを使ってみることです。SQLはより柔軟に処理を記述でき、数千行を超えるデータでは通常は性能が大幅に高く、ファイルベースなのでデータを元の場所に置いたまま分析できます。