DuckDBとは
DuckDBは組み込み型の列指向OLAP(Online Analytical Processing)データベースです。SQLiteのように依存関係なしで動作しながら、分析クエリに特化した設計により、数十億行のデータを瞬時に処理できます。2025年、DuckDBはデータサイエンティストやアナリストの必須ツールとして確立されました。
なぜDuckDBが注目されるのか
従来の分析環境の課題
従来のワークフロー:
ローカルCSV → pandasで読み込み → メモリ不足
または
ローカルCSV → クラウドDWHにアップロード → クエリ実行 → 結果ダウンロード
DuckDBのワークフロー:
ローカルCSV/Parquet → DuckDBで直接クエリ → 即座に結果
従来、大規模データの分析にはBigQueryやSnowflakeなどのクラウドDWHが必要でした。しかし、以下の課題がありました:
- コスト: クラウドの従量課金
- レイテンシ: データのアップロード/ダウンロード時間
- 複雑性: 環境セットアップとネットワーク依存
- プライバシー: 機密データのクラウド転送
DuckDBはこれらの課題をローカル実行で解決します。
DuckDBの主要特徴
1. 組み込み型アーキテクチャ
# サーバー不要、インストールも簡単
pip install duckdb
import duckdb
# インメモリデータベース(デフォルト)
conn = duckdb.connect()
# 永続化データベース
conn = duckdb.connect('my_analysis.duckdb')
# 即座にクエリ実行可能
result = conn.execute("SELECT 42 AS answer").fetchall()
print(result) # [(42,)]
特徴:
- サーバープロセス不要
- シングルファイルで完結
- ゼロ設定で起動
- クロスプラットフォーム対応
2. 列指向ストレージ
行指向(従来のRDBMS):
┌─────┬────────┬─────────┬──────────┐
│ ID │ Name │ Amount │ Date │
├─────┼────────┼─────────┼──────────┤
│ 1 │ Alice │ 1000 │ 2025-01 │ → ディスク読み込み
│ 2 │ Bob │ 2000 │ 2025-02 │ → ディスク読み込み
│ 3 │ Carol │ 1500 │ 2025-03 │ → ディスク読み込み
└─────┴────────┴─────────┴──────────┘
列指向(DuckDB):
┌─────────────────────────────────┐
│ ID: [1, 2, 3] │ → 必要な列だけ読み込み
│ Name: [Alice, Bob, Carol] │
│ Amount: [1000, 2000, 1500] │ ← SUM(Amount)は
│ Date: [2025-01, 02, 03] │ この列だけでOK
└─────────────────────────────────┘
分析クエリでの利点:
- 集計関数(SUM, AVG, COUNT)が高速
- 必要な列のみをスキャン
- 圧縮効率が高い(同じ型のデータが連続)
3. ベクトル化実行エンジン
-- DuckDBは内部でベクトル演算を使用
-- 1行ずつではなく、数千行を一度に処理
SELECT
product_category,
SUM(sales_amount) as total_sales,
AVG(quantity) as avg_quantity
FROM sales_data
WHERE sale_date >= '2025-01-01'
GROUP BY product_category
ORDER BY total_sales DESC;
-- ベクトル化により:
-- - CPUキャッシュを効率的に活用
-- - SIMD命令による並列演算
-- - 分岐予測の最適化
pandas/Polars統合
pandasとの連携
import duckdb
import pandas as pd
# pandasのDataFrameを直接クエリ
df = pd.DataFrame({
'customer_id': range(1, 1000001),
'amount': [i * 100 for i in range(1, 1000001)],
'category': ['A', 'B', 'C'] * 333333 + ['A']
})
# pandasより10-100倍高速なクエリ
result = duckdb.query("""
SELECT
category,
COUNT(*) as count,
SUM(amount) as total,
AVG(amount) as average
FROM df
GROUP BY category
ORDER BY total DESC
""").df()
print(result)
# category count total average
# 0 A 333334 16666850000 50000.25
# 1 C 333333 16666650000 50000.00
# 2 B 333333 16666550000 49999.50
大規模DataFrameの処理
# pandasでは困難な大規模データも処理可能
# DuckDBはディスクスピルを自動で処理
# 10GB以上のCSVを直接クエリ
result = duckdb.query("""
SELECT
date_trunc('month', sale_date) as month,
region,
SUM(revenue) as monthly_revenue
FROM 'sales_2025_*.csv' -- ワイルドカードで複数ファイル
GROUP BY 1, 2
ORDER BY 1, monthly_revenue DESC
""").df()
Polarsとの連携
import duckdb
import polars as pl
# PolarsのLazyFrameをDuckDBでクエリ
lf = pl.scan_parquet("large_dataset/*.parquet")
# DuckDBがPolarsのLazyFrameを認識
result = duckdb.query("""
SELECT
product_id,
SUM(quantity) as total_quantity
FROM lf
WHERE price > 100
GROUP BY product_id
HAVING SUM(quantity) > 1000
""").pl() # Polars DataFrameとして返却
# または相互変換
duckdb_result = duckdb.query("SELECT * FROM lf LIMIT 10")
polars_df = duckdb_result.pl()
Parquet/CSV直接クエリ
ファイル直接クエリの威力
import duckdb
# CSVを直接クエリ(ロード不要)
result = duckdb.query("""
SELECT
city,
COUNT(*) as transactions,
SUM(amount) as total_amount
FROM 'transactions.csv'
GROUP BY city
ORDER BY total_amount DESC
LIMIT 10
""").df()
# Parquetファイルも同様
result = duckdb.query("""
SELECT * FROM 'data/sales_2025.parquet'
WHERE region = 'APAC'
""").df()
# 複数ファイルをワイルドカードで指定
result = duckdb.query("""
SELECT
filename,
COUNT(*) as row_count
FROM 'data/logs_*.parquet'
GROUP BY filename
""").df()
# GCSやS3のファイルも直接クエリ可能
result = duckdb.query("""
SELECT * FROM 's3://my-bucket/data/*.parquet'
WHERE event_type = 'purchase'
""").df()
高度なファイル操作
# JSONファイルのクエリ
result = duckdb.query("""
SELECT
json_extract_string(data, '$.user.name') as user_name,
json_extract(data, '$.items') as items
FROM read_json_auto('events.json')
""").df()
# Excel ファイルの読み込み
duckdb.install_extension('spatial')
duckdb.load_extension('spatial')
result = duckdb.query("""
SELECT * FROM st_read('data.xlsx', layer='Sheet1')
""").df()
# 異なるフォーマット間の変換
duckdb.query("""
COPY (SELECT * FROM 'input.csv' WHERE amount > 1000)
TO 'output.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)
""")
MotherDuckクラウドサービス
MotherDuckとは
MotherDuckは、DuckDBをクラウドで利用できるサーバーレス分析プラットフォームです。ローカルDuckDBの利便性を維持しながら、チーム共有と大規模データ処理を実現します。
import duckdb
# MotherDuckへの接続
conn = duckdb.connect("md:my_database?motherduck_token=YOUR_TOKEN")
# ローカルとクラウドのハイブリッドクエリ
result = conn.execute("""
SELECT
local_table.id,
cloud_table.name,
local_table.value
FROM local_data.csv AS local_table
JOIN my_database.customers AS cloud_table
ON local_table.customer_id = cloud_table.id
""").fetchdf()
MotherDuckの主要機能
機能:
共有データベース:
- チームでのデータ共有
- アクセス制御
- バージョニング
ハイブリッドクエリ:
- ローカルデータ + クラウドデータ
- 自動的なワークロード分散
- ネットワーク転送の最小化
スケーラビリティ:
- サーバーレスで自動スケール
- TB級のデータ処理
- 従量課金制
料金体系(2025年現在):
無料枠: 10GB/月
Pro: $0.50/GB scanned
Enterprise: カスタム
コラボレーション機能
# データの共有
conn.execute("""
CREATE OR REPLACE SHARE my_analytics_share AS
SELECT * FROM processed_sales
WHERE date >= '2025-01-01'
""")
# 共有データへのアクセス
other_conn = duckdb.connect("md:?motherduck_token=OTHER_USER_TOKEN")
result = other_conn.execute("""
SELECT * FROM attach_share('organization/my_analytics_share')
""").fetchdf()
実践的なコード例
データパイプライン構築
import duckdb
def create_analytics_pipeline():
conn = duckdb.connect('analytics.duckdb')
# ステージングテーブルの作成
conn.execute("""
CREATE OR REPLACE TABLE stg_orders AS
SELECT
order_id,
customer_id,
CAST(order_date AS DATE) as order_date,
CAST(amount AS DECIMAL(10,2)) as amount,
status
FROM 'raw_data/orders_*.csv'
WHERE order_date IS NOT NULL
""")
# 集計テーブルの作成
conn.execute("""
CREATE OR REPLACE TABLE daily_sales AS
SELECT
order_date,
COUNT(DISTINCT order_id) as orders,
COUNT(DISTINCT customer_id) as customers,
SUM(amount) as revenue,
AVG(amount) as avg_order_value
FROM stg_orders
WHERE status = 'completed'
GROUP BY order_date
""")
# 移動平均の計算
conn.execute("""
CREATE OR REPLACE TABLE sales_trends AS
SELECT
order_date,
revenue,
AVG(revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) as revenue_7day_ma,
AVG(revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
) as revenue_30day_ma
FROM daily_sales
ORDER BY order_date
""")
return conn
# パイプライン実行
conn = create_analytics_pipeline()
trends = conn.execute("SELECT * FROM sales_trends").df()
高度な分析クエリ
-- コホート分析
WITH first_purchase AS (
SELECT
customer_id,
DATE_TRUNC('month', MIN(order_date)) as cohort_month
FROM orders
GROUP BY customer_id
),
monthly_activity AS (
SELECT
o.customer_id,
fp.cohort_month,
DATE_TRUNC('month', o.order_date) as activity_month,
DATE_DIFF('month', fp.cohort_month, DATE_TRUNC('month', o.order_date)) as months_since_first
FROM orders o
JOIN first_purchase fp ON o.customer_id = fp.customer_id
)
SELECT
cohort_month,
months_since_first,
COUNT(DISTINCT customer_id) as active_customers,
ROUND(COUNT(DISTINCT customer_id) * 100.0 /
FIRST_VALUE(COUNT(DISTINCT customer_id)) OVER (
PARTITION BY cohort_month
ORDER BY months_since_first
), 1) as retention_rate
FROM monthly_activity
GROUP BY cohort_month, months_since_first
ORDER BY cohort_month, months_since_first;
-- RFM分析
WITH customer_rfm AS (
SELECT
customer_id,
DATE_DIFF('day', MAX(order_date), CURRENT_DATE) as recency,
COUNT(DISTINCT order_id) as frequency,
SUM(amount) as monetary
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '1 year'
GROUP BY customer_id
),
rfm_scores AS (
SELECT
customer_id,
recency,
frequency,
monetary,
NTILE(5) OVER (ORDER BY recency DESC) as r_score,
NTILE(5) OVER (ORDER BY frequency) as f_score,
NTILE(5) OVER (ORDER BY monetary) as m_score
FROM customer_rfm
)
SELECT
customer_id,
recency,
frequency,
monetary,
r_score || f_score || m_score as rfm_segment,
CASE
WHEN r_score >= 4 AND f_score >= 4 THEN 'Champions'
WHEN r_score >= 4 AND f_score <= 2 THEN 'New Customers'
WHEN r_score <= 2 AND f_score >= 4 THEN 'At Risk'
WHEN r_score <= 2 AND f_score <= 2 THEN 'Lost'
ELSE 'Regular'
END as customer_segment
FROM rfm_scores
ORDER BY monetary DESC;
Jupyterでの活用
# Jupyter Notebook での使用例
import duckdb
import matplotlib.pyplot as plt
# マジックコマンドの設定
%load_ext duckdb_magic
# セルでSQLを直接実行
%%duckdb
SELECT
DATE_TRUNC('week', order_date) as week,
SUM(amount) as weekly_revenue
FROM 'sales.parquet'
GROUP BY 1
ORDER BY 1
# 結果を変数に格納
%%duckdb result_df <<
SELECT
category,
COUNT(*) as count,
SUM(revenue) as total_revenue
FROM sales
GROUP BY category
# 可視化
result_df.plot(kind='bar', x='category', y='total_revenue')
plt.title('Revenue by Category')
plt.show()
BigQuery/Snowflakeとの使い分け
比較表
| 観点 | DuckDB | BigQuery | Snowflake |
|------|--------|----------|-----------|
| デプロイ | 組み込み | クラウド | クラウド |
| コスト | 無料 | 従量課金 | 従量課金 |
| セットアップ | 数秒 | 数分 | 数分-数時間 |
| データサイズ | GB-数百GB | TB-PB | TB-PB |
| 同時ユーザー | シングル | 数千 | 数千 |
| レイテンシ | ミリ秒 | 秒-分 | 秒-分 |
| ユースケース | ローカル分析、開発 | 本番DWH | 本番DWH |
使い分けガイド
DuckDBが最適な場面:
- ローカルでの探索的データ分析
- データサイエンスのプロトタイピング
- CI/CDパイプラインでのデータテスト
- 機密データの分析(クラウド転送不要)
- コスト最適化(小-中規模データ)
- 組み込みアプリケーション
BigQuery/Snowflakeが最適な場面:
- TB/PB級の大規模データ
- 複数チームでの同時アクセス
- 本番環境のダッシュボード
- リアルタイムストリーミング
- エンタープライズガバナンス
ハイブリッドアプローチ:
開発/テスト: DuckDB
↓
本番/共有: BigQuery/Snowflake
↓
ローカル分析: DuckDB(サンプルデータ)
ハイブリッド実装例
# 開発時はDuckDB、本番はBigQueryへのシームレスな移行
class DataWarehouse:
def __init__(self, environment='dev'):
self.environment = environment
if environment == 'dev':
self.conn = duckdb.connect()
else:
from google.cloud import bigquery
self.conn = bigquery.Client()
def query(self, sql):
if self.environment == 'dev':
# DuckDBでローカル実行
return self.conn.execute(sql).df()
else:
# BigQueryで実行
return self.conn.query(sql).to_dataframe()
# 使用例
dw = DataWarehouse(environment='dev') # 開発時
# dw = DataWarehouse(environment='prod') # 本番時
result = dw.query("""
SELECT region, SUM(sales) as total_sales
FROM sales_data
GROUP BY region
""")
2025年の採用状況
急成長するエコシステム
統計(2025年):
GitHub Stars: 25,000+
月間ダウンロード: 15M+
企業採用: Fortune 500の30%以上
主要な採用企業:
- Netflix: ログ分析
- Stripe: 財務分析
- Uber: 地理空間データ処理
- Meta: データサイエンスワークフロー
統合ツール:
- dbt: ネイティブサポート
- Apache Superset: コネクタ提供
- Metabase: ネイティブドライバー
- Streamlit: 直接統合
コミュニティの拡大
2025年のマイルストーン:
- DuckCon 2025: 初の大規模カンファレンス開催
- エクステンションエコシステム: 50+ 公式/コミュニティ拡張
- 日本語ドキュメント: コミュニティ翻訳プロジェクト完了
- 教育機関: 大学のデータサイエンスコースで採用増加
拡張機能(Extensions)
import duckdb
# 利用可能な拡張機能を確認
duckdb.query("SELECT * FROM duckdb_extensions()").df()
# 拡張機能のインストールと読み込み
duckdb.install_extension("spatial")
duckdb.load_extension("spatial")
# 空間データ分析
result = duckdb.query("""
SELECT
ST_AsText(geometry) as wkt,
ST_Area(geometry) as area
FROM 'buildings.geojson'
WHERE ST_Within(geometry, ST_GeomFromText('POLYGON(...)'))
""").df()
# その他の主要拡張機能
# - httpfs: HTTP/S3/GCS からのファイル読み込み
# - json: 高度なJSON処理
# - parquet: Parquet最適化
# - sqlite_scanner: SQLiteファイルの読み込み
# - postgres_scanner: PostgreSQLへの直接接続
パフォーマンス最適化
ベストプラクティス
import duckdb
# 1. 並列処理の設定
conn = duckdb.connect()
conn.execute("SET threads TO 8") # CPUコア数に応じて設定
conn.execute("SET memory_limit = '8GB'") # メモリ制限
# 2. Parquet形式の活用
conn.execute("""
COPY (SELECT * FROM large_csv.csv)
TO 'optimized.parquet' (
FORMAT PARQUET,
COMPRESSION ZSTD,
ROW_GROUP_SIZE 100000
)
""")
# 3. パーティショニング
conn.execute("""
COPY (SELECT * FROM sales WHERE year = 2025)
TO 'sales' (
FORMAT PARQUET,
PARTITION_BY (month, region),
OVERWRITE_OR_IGNORE
)
""")
# 4. インデックスの活用(永続データベース)
conn.execute("""
CREATE INDEX idx_customer ON orders(customer_id)
""")
クエリ最適化
-- EXPLAIN ANALYZEでクエリプランを確認
EXPLAIN ANALYZE
SELECT
customer_id,
SUM(amount) as total
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY customer_id;
-- フィルタプッシュダウンの活用
-- Parquetの列プルーニングが自動適用される
SELECT customer_id, amount
FROM 'orders.parquet'
WHERE status = 'completed'; -- フィルタがスキャン時に適用
まとめ
DuckDBは2025年、ローカル分析のデファクトスタンダードとして確立しました。組み込み型OLAPの特性を活かし、以下のような変革をもたらしています:
技術的優位性:
- 列指向ストレージによる高速分析
- pandas/Polarsとのシームレスな統合
- Parquet/CSV直接クエリ
- ゼロセットアップの利便性
実用的メリット:
- クラウドコストの削減
- データプライバシーの確保
- 開発サイクルの高速化
- 学習コストの低さ
エコシステムの成熟:
- MotherDuckによるクラウド拡張
- 豊富な拡張機能
- 主要ツールとの統合
データ分析の第一歩として、まずはDuckDBでローカル分析を始め、必要に応じてクラウドDWHと組み合わせるハイブリッドアプローチが2025年のベストプラクティスです。
← 一覧に戻る参考: DuckDB公式ドキュメント