DuckDB-WASMを使ってみる
本記事は「 TTDC Advent Calendar 2024 」 25 日目の記事です。
目的
弊社ではデータ解析を行う商品・サービスを提供しています。昨今、ユーザーが利用するデータ量の増加、ユーザービリティの要求変化から、"技術的な興味で"フロントエンドでデータ解析ができないか模索しています。Node.jsでデータ解析ロジックをゴリゴリ書いてもいいですが、DuckDB-WASMを試しているテックブログ(参考[1],[2],[3]など)を多く見かけるようになってきたので、一度試してみようと思ったのがきっかけです。
今回は、他のOSSと比較することはせず、実際に動かしてみて、パフォーマンスを測定するなどして開発者目線の"感触を得る"ことを目的とします。
用語
WASM
Microsoft Copilotが生成した要約
WASMとは
WebAssembly(WASM)とは、ウェブブラウザ内で実行可能なバイナリ命令形式のプログラムです。以下はWASMの主な特徴です:
高速な実行:ネイティブコードに近い速度で実行されるため、パフォーマンスが非常に高いです。
言語サポート:C、C++、Rustなどのさまざまなプログラミング言語からWASMバイナリを生成できます。
安全性:サンドボックス内で実行されるため、ブラウザのセキュリティモデルに従って安全に実行されます。
互換性:主要なウェブブラウザ(Chrome、Firefox、Safari、Edgeなど)でサポートされています。
WASMに期待している点は、JavaScript/TypeScript言語のみではなく他の言語が利用可能な点です。
DuckDB
Microsoft Copilotが生成した要約
DuckDBは、組み込み型分析SQLデータベースで、データサイエンスや分析ワークロードに特化しています。主な特徴は以下の通りです:
組み込み型:軽量で、他のプログラムに組み込んで使用できます。例えば、PythonやRのスクリプト内で直接利用可能です。
高パフォーマンス:カラムストア形式と高度なクエリ最適化を活用し、非常に高速なクエリ実行が可能です。
柔軟なファイルサポート:CSV、Parquet、Arrowなど、複数のファイル形式をサポートしています。
マルチプラットフォーム:Windows、macOS、Linuxで動作し、さまざまなシステムに簡単に統合できます。
シンプルなAPI:シンプルかつ直感的なAPIを提供しており、使いやすいです。
DuckDBに期待している点は、CSVやParquetファイルが扱える点、組み込み型分析SQLが扱える点です。
DuckDB-WASM
Microsoft Copilotが生成した要約
DuckDB-WASMは、メモリ内分析データベースのDuckDBをWebAssembly(WASM)を使用してウェブに持ち込むプロジェクトです。これにより、DuckDBをブラウザ内で直接実行できるため、サーバーサイドのインフラストラクチャを必要とせずに、強力なデータ分析と処理が可能になります。特にリアルタイムのデータ分析や可視化を必要とするウェブアプリケーションに役立ちます。
この要約通り、データ分析の利点が本当か、実際に触っていきます。
環境
普段から開発はWindowsで行っています。今回もWindows環境に環境を構築します。
- Windows11 Pro
- Visual Studio Code 1.96.0 (以下、VSCode)
- Vite
- Node Version Manager(以下、nvm)
- Node.js(以下、node)
下記のコマンドは、すべてVS Codeのターミナルウィンドウ上でシェルはPowerShellを利用しています。
nodeのバージョンを確認したところ、古かったので最新へアップデートします。私は、nvmを使って複数のバージョンを切り替えている環境のため、nvmを利用します。
nvm list available
nvm install 22.12.0
nvm use 22.12.0
一応、バージョンを確認しておきます
node -v
v22.12.0
これで環境は整いました。
コード環境の構築
初めに適切なディレクトリで、create viteのコマンドを実行し基本となるプロジェクトを作成します。
今回はduckdb_wasm_sample
というプロジェクト名、フレームワークはReact、言語はTypeScriptで準備します。
npm create vite@latest
次にモジュールをインストールします。
npm install @duckdb/duckdb-wasm
npm install @duckdb/duckdb-wasm-shell
次にsrc/App.tsx
にVite向けのサンプルコードを追記します。この状態で、ビルドエラーなどがないか確認するため、サンプルを立ち上げます。
npm run dev
この状態だと、画面はまだ標準のままなので、デバッグコンソールでモジュールがロードされているか確認します。
その後、App.tsx
にViteのサンプルコードを貼り付けて準備完了としました。
いろいろ実行してみる!
今回のデータは、私がテキトーに作ったダミーデータ生成ツールで、ざっくり生成したものを利用します。ファイルは.parquet
ファイル、カラムはTime, channel1, channel2, ..., channel18
です。Timeはサンプリング時間、channel1からchannel18はノイズが乗ったsin波、周波数が違うsin波でデータ型はすべてDouble。レコード数は120,000、カラム数は19としています。弊社では、より多くのレコード数、カラム数のデータを常に相手にしています。今回は比較的”優しい”部類のデータ量です。今回、処理結果はconsole
を使って出力し、各処理時間を出力しています。処理時間は少数2桁を四捨五入した値でまとめていきます。
テーブル作成
console.time('CREATE TABLE')
const create_table_result = await connection.query(`
CREATE TABLE direct AS
SELECT * FROM 'sample.parquet'
`);
console.timeEnd('CREATE TABLE')
処理時間は599.9[ms]でした。
カラム情報取得
console.time('GetAllColumnInfo')
const get_all_cplumn_info_result = await connection.query(`
SELECT
column_name,
data_type
FROM information_schema.columns
WHERE table_name = 'direct';
`);
console.timeEnd('GetAllColumnInfo')
処理時間は60.4[ms]でした。
レコード数取得
console.time('SELECT COUNT')
const select_count = await connection.query(`
SELECT COUNT(Time) FROM direct
`);
console.timeEnd('SELECT COUNT')
処理時間は16.2[ms]でした。
平均値取得
console.time('SELECT AVG')
const select_avg = await connection.query(`
SELECT AVG(Time) as average_time FROM direct
`);
console.timeEnd('SELECT AVG')
処理時間は2.5[ms]でした。
合計値取得
console.time('SELECT SUM')
const select_sum = await connection.query(`
SELECT SUM(Time) as sum_time FROM direct
`);
console.timeEnd('SELECT SUM')
処理時間は1.5[ms]でした。
カラム同士の足し算
console.time('SELECT add')
const select_add = await connection.query(`
SELECT
channel3 + channel4 as total_channels
FROM direct
`);
console.timeEnd('SELECT add')
処理時間は6.7[ms]でした。
閾値判定
sin波は-1から1までの幅で値を出力するため、0.5を閾値としました。
console.time('Judge threshold')
const select_judge_threshold = await connection.query(`
WITH prev_values AS (
SELECT
Time,
channel3,
LAG(channel3) OVER (ORDER BY Time) as prev_channel3
FROM direct
)
SELECT *
FROM prev_values
WHERE (channel3 >= 0.5 AND prev_channel3 < 0.5)
OR (channel3 < 0.5 AND prev_channel3 >= 0.5)
ORDER BY Time;
`);
console.timeEnd('Judge threshold')
処理時間は59.1[ms]でした。
移動平均(4ポイント)
console.time('MovingAverage')
const sma_result = await connection.query(`
SELECT
AVG(channel4) OVER (
ORDER BY Time
ROWS BETWEEN 3 PRECEDING AND 1 FOLLOWING
) as sma_4point
FROM direct
ORDER BY Time
LIMIT 120000;
`);
console.timeEnd('MovingAverage')
処理時間は85.4[ms]でした。
まとめ
各処理内容を下表にまとめます。
タスク | 処理時間[ms] |
---|---|
テーブル作成 | 599.9 |
カラム情報取得 | 60.4 |
レコード数取得 | 16.2 |
平均値取得 | 2.5 |
合計値取得 | 1.5 |
カラム同士の足し算 | 6.7 |
閾値判定 | 59.1 |
移動平均(4ポイント) | 85.4 |
今回の例では、一番最初に全データをロードしている事もあり、実際の運用に即しているとは言えませんが、個人的に処理速度は申し分なし!という印象です。ここまでのデータ処理がフロントエンドで完結できるのは大きなメリットだと思いました。
今後はもう少し実運用に近いデータ量やインフラ環境で確認していこうと思います。