はじめに
私は機械学習エンジニアをしており、現在はLLMを扱うことが多く、RAGシステムの構築などを行っています。
バリバリのバックエンドエンジニアというわけではないので、SQLを日常的に使う機会はそれほど多くありません。
ただ、こういった場面ではDBを使用したくなることがあります。
- RAGシステムでドキュメントのメタデータ(ファイル名・チャンクID・埋め込みベクトルのインデックス等)を管理したい
- データの結合・加工を行いたい
2つ目に関して、なんのこっちゃと思う方もいるかもしれませんが、複数の文書間でIDなどのデータで紐づいていたり、特定のフラグや文言が存在するデータのみを抽出して学習データに使用したい、といった場面がたびたびあります。
フォーマットがバラバラな文書をPandasでちまちま加工して必要なデータを成形していると、コードも複雑化してマジックストリング(ハードコードされたカラム名)だらけ、ぱっと見すごくわかりにくく使い回せないスクリプトが出来上がるわけです。
そういった場合に、ある程度成形した文書ごとのデータをDBにとりあえず入れておき、SQL操作で必要なデータを取り出したほうが直感的でわかりやすく、あとから必要なデータの要件を変えやすいため、活用することがあります。
このようなユースケースでSQLiteを使っていたのですが、最近DuckDBを使ってみたところPandasとの相性が良く使いやすかったので、記事にまとめてみました。
DuckDBとは?
DuckDBは、組み込み型の分析用RDBMSです。
SQLiteと同様にサーバーレスで動作し、単一ファイルに全データを保存できます。大きな違いは、SQLiteが行指向(OLTP向け)なのに対し、DuckDBは列指向(OLAP向け)であることです。
特徴をひと言でまとめると、「Pandasと融合できるSQLite」 という感じです。
SQLite vs DuckDB 比較表
| 項目 | SQLite | DuckDB |
|---|---|---|
| アーキテクチャ | 行指向(OLTP) | 列指向(OLAP) |
| 得意な処理 | 小〜中規模のトランザクション処理 | 大量データの集計・分析クエリ |
| サーバー | 不要(組み込み) | 不要(組み込み) |
| ファイル形式 |
.db / .sqlite
|
.duckdb / .db
|
| Pandas連携 |
pd.read_sql() で読み取り可能 |
DataFrameを直接クエリ・挿入可能 |
| 並列クエリ | 非対応 | 対応(マルチスレッド) |
| 対応言語 | Python, C/C++, Java, etc. | Python, R, Java, Node.js, etc. |
| インメモリモード | 非対応 | 対応(:memory: で起動) |
| CSVやParquetの直接読み込み | 不可(要インポート) | SQL上で直接クエリ可能 |
| Pythonパッケージ |
sqlite3(標準ライブラリ) |
duckdb(pip install) |
| ライセンス | Public Domain | MIT |
SQLiteはPythonのSTLに含まれておりインストール不要という強みがありますが、DuckDBはpip install duckdbだけで使えるので導入コストはほぼ変わりません。
インストール
pip install duckdb
基本的な使い方
DBの作成・接続
import duckdb
# ファイルに保存するDB(永続化)
conn = duckdb.connect("my_database.duckdb")
# インメモリDB(セッション終了で消える)
conn = duckdb.connect(":memory:")
テーブルの作成・データ挿入
conn.execute("""
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY,
filename VARCHAR,
chunk_id INTEGER,
content TEXT,
created_at TIMESTAMP DEFAULT current_timestamp
)
""")
conn.execute("""
INSERT INTO documents (id, filename, chunk_id, content)
VALUES (1, 'report_2026.pdf', 0, 'DuckDBはとても便利なデータベースです。')
""")
クエリ
result = conn.execute("SELECT * FROM documents").fetchall()
print(result)
# [(1, 'report_2026.pdf', 0, 'DuckDBはとても便利なデータベースです。', datetime.datetime(...))]
PandasとのシームレスなDataFrame連携
ここがDuckDBの最大の魅力です。
DataFrameを直接SQLでクエリする
import pandas as pd
import duckdb
df = pd.DataFrame({
"filename": ["doc_a.txt", "doc_b.txt", "doc_c.txt"],
"chunk_id": [0, 0, 1],
"token_count": [512, 128, 300],
"source": ["web", "local", "web"],
})
# DataFrameをそのままSQLでクエリ
result = duckdb.sql("SELECT filename, token_count FROM df WHERE source = 'web'").df()
print(result)
filename token_count
0 doc_a.txt 512
2 doc_c.txt 300
dfという変数名をSQL内でそのままテーブル名として使えます。pd.read_sql()のような変換処理は不要です。
クエリ結果をDataFrameとして受け取る
conn = duckdb.connect("my_database.duckdb")
conn.execute("""
CREATE TABLE IF NOT EXISTS logs (
run_id VARCHAR,
metric VARCHAR,
value FLOAT
)
""")
conn.execute("INSERT INTO logs VALUES ('run_001', 'accuracy', 0.92)")
conn.execute("INSERT INTO logs VALUES ('run_001', 'f1_score', 0.89)")
conn.execute("INSERT INTO logs VALUES ('run_002', 'accuracy', 0.95)")
# .df() でそのままDataFrameに変換
df_result = conn.execute("""
SELECT run_id, AVG(value) as avg_metric
FROM logs
GROUP BY run_id
""").df()
print(df_result)
run_id avg_metric
0 run_001 0.905
1 run_002 0.950
DataFrameをそのままINSERT
import pandas as pd
import duckdb
df_new = pd.DataFrame({
"run_id": ["run_003", "run_003"],
"metric": ["accuracy", "f1_score"],
"value": [0.91, 0.88],
})
conn = duckdb.connect("my_database.duckdb")
# DataFrameをテーブルに直接INSERT
conn.register("tmp_df", df_new)
conn.execute("INSERT INTO logs SELECT * FROM tmp_df")
conn.unregister("tmp_df")
SQLiteではdf.to_sql()を使いますが、DuckDBではconn.register()で一時的にDataFrameをテーブルとして登録し、そのままINSERTできます。
CSVやParquetを直接クエリできる
DuckDBのもう一つの強みは、外部ファイルをインポートせずに直接SQLで読めることです。
import duckdb
# CSVを直接クエリ
result = duckdb.sql("SELECT * FROM read_csv_auto('data.csv') LIMIT 5").df()
# Parquetも同様
result = duckdb.sql("SELECT COUNT(*) FROM read_parquet('embeddings.parquet')").df()
# 複数ファイルをまとめてクエリ(glob対応)
result = duckdb.sql("SELECT * FROM read_csv_auto('logs/*.csv')").df()
ETLの前処理など、「とりあえず中身を確認したい」という場面で非常に便利です。
実用例:複数Excelを結合して学習データを作る
私が実際にDuckDBを使っていたユースケースをもとに実用例を作ってみました。
複数のExcelファイルをそれぞれDBに取り込み、IDで結合・フィルタリングして必要なデータを取り出すという処理です。
まず、各Excelを読み込んでDBに登録します。
import duckdb
import pandas as pd
import glob
conn = duckdb.connect("project.duckdb")
# メインデータ(Excel)をDBに登録
df_main = pd.read_excel("main_data.xlsx")
df_main.columns = ["id", "title", "description", "file_name", "path", "use_flag"]
conn.execute("""
CREATE TABLE IF NOT EXISTS main_data (
id INTEGER,
title VARCHAR,
description VARCHAR,
file_name VARCHAR,
path VARCHAR,
use_flag VARCHAR -- 学習対象かどうかのフラグ(例: '〇')
)
""")
conn.register("tmp", df_main)
conn.execute("INSERT INTO main_data SELECT * FROM tmp")
conn.unregister("tmp")
# 別ソースのメタデータ(複数Excel)をまとめてDBに登録
dfs = [pd.read_excel(f) for f in glob.glob("metadata/*.xlsx")]
df_meta = pd.concat(dfs, ignore_index=True)
df_meta.columns = ["item_id", "category", "summary"]
conn.execute("""
CREATE TABLE IF NOT EXISTS metadata (
item_id VARCHAR,
category VARCHAR,
summary VARCHAR
)
""")
conn.register("tmp", df_meta)
conn.execute("INSERT INTO metadata SELECT * FROM tmp")
conn.unregister("tmp")
次に、SQLで結合・フィルタリングして必要なデータだけを取り出します。
# use_flagが'〇'のデータに絞り、item_idで結合して学習データを作成
df_train = conn.execute("""
SELECT
d.id,
d.title,
d.description,
d.file_name,
m.category,
m.summary
FROM main_data d
JOIN metadata m
ON d.id = m.item_id
WHERE d.use_flag = '〇'
AND d.file_name IS NOT NULL
ORDER BY d.id
""").df()
print(df_train.shape)
df_train.to_csv("train_data.csv", index=False)
この程度の処理であればPandasでも問題ないですが、より複雑な条件で紐づけや絞り込みをやろうと思うとコードがかなり散らばってしまいます。
SQLで書くと処理の意図がひと目でわかりますし、あとから「このフラグも追加で絞りたい」といった要件変更にも WHERE 句を足すなどで簡単に対応できるのが便利です。
まとめ
| ユースケース | おすすめ |
|---|---|
| Webアプリのデータ永続化(トランザクション重視) | SQLite |
| データ分析・集計・Pandas連携 | DuckDB |
| CSVやParquetの探索的分析 | DuckDB |
| RAGや実験ログの管理 | DuckDB |
| Pythonだけで完結させたい(pip不要) | SQLite |
MLやAI開発の文脈では、Pandasとの相性の良さやCSVの直接クエリなど、DuckDBが活きる場面がかなり多いと感じています。
「SQLiteは知ってるけど重い処理だとちょっと辛い」「DataFrameとDBをいったりきたりするのが面倒」という方は、ぜひDuckDBを試してみてください。