Rで解析:高速な分析用SQLを実行できる「duckdb」パッケージの紹介
大量のレコードを含むデータを集計する場面では、データ全体をメモリへ読み込んでから処理を進めることが一般的です。しかし、ファイルサイズが大きくなるほど読み込みに時間を要し、集計の条件を変えながら試行錯誤を繰り返すには手間がかかります。
本パッケージは、分析処理に特化した組み込み型のデータベース管理システムDuckDBと、データベースに接続するためのインターフェースを収録しています。メモリ上またはファイル上のデータベースへ接続し、SQLによる問い合わせの実行が可能です。CSVファイルをそのままテーブルとして取り込む機能や、Rのデータフレームを複製せずに仮想テーブルとして参照する機能も収録されています。
また、dbplyrパッケージを介したdplyr構文での操作や、ファイルを直接参照する遅延テーブルの作成も可能です。本パッケージの利用で、Rで大規模なデータの集計を進められるのではないかと考えます。
パッケージバージョンは1.5.5。Windows 11 x64 (build 26200)のR version 4.6.1で確認しています。
パッケージのインストール
下記コマンドを実行してください。
# パッケージのインストール
install.packages("duckdb")
# パッケージの読み込み
library("duckdb")
# コマンド例の実行に必要なパッケージのインストール
# install.packages(c("dplyr", "dbplyr"))
# コマンド例の実行に必要なパッケージの読み込み
library("dplyr")
library("dbplyr")コマンド例
詳細はコメント、パッケージのヘルプを確認してください。
SQLは、データベースを操作するための標準的な問い合わせ言語です。DuckDBは、集計や結合といった分析処理に特化した組み込み型のデータベース管理システムで、サーバーを別途起動することなくR上のプロセス内で動作します。
以降のコマンド例では、北海道の農産物直売所を題材とした架空のデータを使用します。はじめに3つのデータフレームを作成します。
# 直売所マスタのデータフレームを作成
shop_master <- data.frame(
shop_id = c("S01", "S02", "S03", "S04", "S05"),
shop_name = c("恵庭ふれあい市場", "千歳みのり館", "苫小牧うみかぜ市場",
"富良野あさひ市場", "美瑛おかの市場"),
city = c("恵庭市", "千歳市", "苫小牧市", "富良野市", "美瑛町"))
# 直売所ごとの販売実績のデータフレームを作成
sales <- data.frame(
sale_date = as.Date(c("2026-07-01", "2026-07-01", "2026-07-02", "2026-07-02",
"2026-07-03", "2026-07-03", "2026-07-04", "2026-07-04",
"2026-07-05", "2026-07-05")),
shop_id = c("S01", "S02", "S01", "S03", "S04", "S05", "S02", "S04", "S03", "S05"),
item = c("かぼちゃ", "とうもろこし", "アスパラ", "じゃがいも", "メロン",
"とうもろこし", "かぼちゃ", "アスパラ", "とうもろこし", "じゃがいも"),
quantity = c(24L, 150L, 40L, 90L, 18L, 210L, 33L, 55L, 130L, 75L),
unit_price = c(320L, 120L, 480L, 180L, 1600L, 130L, 300L, 520L, 110L, 190L))
# CSVの読み込み例で使用する出荷記録のデータフレームを作成
shipment <- data.frame(
ship_date = as.Date(c("2026-07-01", "2026-07-02", "2026-07-03",
"2026-07-04", "2026-07-05", "2026-07-06")),
shop_id = c("S01", "S02", "S03", "S04", "S05", "S01"),
item = c("かぼちゃ", "とうもろこし", "じゃがいも", "メロン", "アスパラ", "とうもろこし"),
quantity = c(24L, 150L, 90L, 18L, 55L, 130L),
unit_price = c(320L, 120L, 180L, 1600L, 520L, 110L))データベースインスタンスの作成:duckdbコマンド
DuckDBのデータベースインスタンスを作成し、接続の土台となるドライバーオブジェクトを返します。dbdirを省略した場合は、データの全てがメモリ上に保持されます。
| オプション | 意味 | 初期値 |
|---|---|---|
| dbdir | データベースファイルを配置する既存ディレクトリのパス | DBDIR_MEMORY |
| read_only | 読み取り専用で動作させるかの指定 | FALSE |
| bigint | 64ビット整数の返し方(”numeric”または”integer64″を指定) | “numeric” |
| config | DuckDBの設定フラグを格納した名前付きリスト | list() |
| … | 将来の拡張用の引数(指定しない) | なし |
| home | 拡張機能と保存済みシークレットを置くルートディレクトリ | NULL |
| shared_home | 共有ディレクトリ「~/.duckdb」を使用するかの指定(NULL、TRUE、FALSEを指定) | NULL |
| allow_extensions | DuckDB拡張機能の読み込みを許可するかの指定(NULL、TRUE、FALSEを指定) | NULL |
| environment_scan | 呼び出し元環境のデータフレームをテーブルとして扱うかの指定 | FALSE |
# メモリ上にデータベースインスタンスを作成
drv <- duckdb(dbdir = ":memory:", bigint = "numeric", shared_home = FALSE)
# 作成したドライバーを指定して接続を確立
con <- dbConnect(drv)
# 直売所マスタをテーブルとしてデータベースへ書き込み
dbWriteTable(con, "shop_master", shop_master)
# データベース内のテーブル一覧を確認
dbListTables(con)
[1] "shop_master"
# SQLで直売所名と所在市町を取得
dbGetQuery(con, "SELECT shop_name, city FROM shop_master ORDER BY shop_id")
shop_name city
1 恵庭ふれあい市場 恵庭市
2 千歳みのり館 千歳市
3 苫小牧うみかぜ市場 苫小牧市
4 富良野あさひ市場 富良野市
5 美瑛おかの市場 美瑛町
# 接続を切断
dbDisconnect(con)
# ドライバーを停止してデータベースインスタンスを解放
duckdb_shutdown(drv)データベースへの接続:dbConnectコマンド
ドライバーオブジェクトに対して接続を開きます。Rへ値を返す際のタイムゾーンや、64ビット整数、配列型、ジオメトリ列の受け取り方を接続単位で指定できます。
| オプション | 意味 | 初期値 |
|---|---|---|
| drv | duckdb()が返したドライバーオブジェクト | なし |
| dbdir | データベースファイルを配置する既存ディレクトリのパス | DBDIR_MEMORY |
| … | 将来の拡張用の引数(指定しない) | なし |
| debug | 実行したクエリなどのデバッグ情報を表示するかの指定 | getOption(“duckdb.debug”, FALSE) |
| read_only | 読み取り専用で動作させるかの指定 | FALSE |
| timezone_out | Rへ返す際のタイムゾーン(ローカル時刻はSys.timezone()または””を指定) | “UTC” |
| tz_out_convert | タイムスタンプ列の変換方法(”with”または”force”を指定) | c(“with”, “force”) |
| config | DuckDBの設定フラグを格納した名前付きリスト | list() |
| bigint | 64ビット整数の返し方(”numeric”または”integer64″を指定) | “numeric” |
| array | 配列型の返し方(”none”または”matrix”を指定) | “none” |
| geometry | ジオメトリ列の返し方(”blob”または”wk”を指定) | “blob” |
| map | MAP型の列の返し方(”data.frame”または”list_of”を指定) | “data.frame” |
# ドライバーオブジェクトを作成
drv <- duckdb(shared_home = FALSE)
# 値の受け取り方を指定して接続を確立
con <- dbConnect(drv, timezone_out = "UTC", bigint = "numeric",
array = "none", debug = FALSE)
# 日付と桁の大きい整数を含む問い合わせを実行
res <- dbGetQuery(con, "SELECT DATE '2026-07-01' AS ship_date, 12345678901 AS total_yen")
# 取得した結果を表示
res
ship_date total_yen
1 2026-07-01 12345678901
# 各列がRのどのクラスへ変換されたかを確認
sapply(res, class)
ship_date total_yen
"Date" "numeric"
# 接続を切断
dbDisconnect(con)CSVファイルの直接読み込み:duckdb_read_csvコマンド
CSVファイルをRへ読み込むことなく、DuckDBのテーブルとして直接取り込みます。col.typesに名前付きベクトルだと、列ごとのデータ型を明示が可能です。
| オプション | 意味 | 初期値 |
|---|---|---|
| conn | dbConnect()で作成したDuckDBの接続 | なし |
| name | 作成するテーブルの名前 | なし |
| files | 構造が同一である1つ以上のCSVファイル名 | なし |
| … | 将来の拡張用の引数(指定しない) | なし |
| header | 1行目をヘッダー行として扱うかの指定 | TRUE |
| na.strings | NULLとみなす文字列(長さ1のみ指定可) | “” |
| nrow.check | データ型の判定に用いる読み込み行数 | 500 |
| delim | フィールドの区切り文字 | “,” |
| quote | 列の値を囲む引用符 | “\”” |
| col.names | 検出または生成された列名を上書きする文字ベクトル | NULL |
| col.types | 列のDuckDBデータ型を指定する文字ベクトル | NULL |
| lower.case.names | 列名を小文字へ変換するかの指定 | FALSE |
| sep | 互換性のために用意されたdelimの別名 | delim |
| transaction | 処理全体を1つのトランザクションで実行するかの指定 | TRUE |
| temporary | 一時テーブルとして作成するかの指定 | FALSE |
# 出荷記録を一時ファイルへCSVとして書き出し
csv_path <- file.path(tempdir(), "shipment.csv")
write.csv(shipment, csv_path, row.names = FALSE, fileEncoding = "UTF-8")
# DuckDBへ接続
con <- dbConnect(duckdb(shared_home = FALSE))
# 列ごとのデータ型を明示してCSVをテーブルとして読み込み
duckdb_read_csv(con, "shipment", csv_path,
header = TRUE,
na.strings = "",
delim = ",",
col.types = c(
ship_date = "DATE",
shop_id = "VARCHAR",
item = "VARCHAR",
quantity = "INTEGER",
unit_price = "INTEGER"
))
# 読み込んだテーブルの内容を確認
dbReadTable(con, "shipment")
ship_date shop_id item quantity unit_price
1 2026-07-01 S01 かぼちゃ 24 320
2 2026-07-02 S02 とうもろこし 150 120
3 2026-07-03 S03 じゃがいも 90 180
4 2026-07-04 S04 メロン 18 1600
5 2026-07-05 S05 アスパラ 55 520
6 2026-07-06 S01 とうもろこし 130 110
7 2026-07-01 S01 かぼちゃ 24 320
8 2026-07-02 S02 とうもろこし 150 120
9 2026-07-03 S03 じゃがいも 90 180
10 2026-07-04 S04 メロン 18 1600
11 2026-07-05 S05 アスパラ 55 520
12 2026-07-06 S01 とうもろこし 130 110
# 品目ごとの売上金額を集計
dbGetQuery(con, "
SELECT item, SUM(quantity * unit_price) AS sales_yen
FROM shipment
GROUP BY item
ORDER BY sales_yen DESC")
item sales_yen
1 とうもろこし 64600
2 メロン 57600
3 アスパラ 57200
4 じゃがいも 32400
5 かぼちゃ 15360
# 接続を切断
dbDisconnect(con)
データフレームの仮想テーブル登録:duckdb_registerコマンド
Rのデータフレームを仮想テーブル(ビュー)として登録し、SQLから参照できるようにします。登録の際にデータの複製は行われません。
| オプション | 意味 | 初期値 |
|---|---|---|
| conn | dbConnect()で作成したDuckDBの接続 | なし |
| name | 登録する仮想テーブルの名前 | なし |
| df | 仮想テーブルの元となるデータフレーム | なし |
| overwrite | 同名の登録がある場合に上書きするかの指定 | FALSE |
| experimental | 実験的な最適化を有効にするかの指定 | FALSE |
# DuckDBへ接続
con <- dbConnect(duckdb(shared_home = FALSE))
# 直売所マスタを仮想テーブルとして登録
duckdb_register(con, "shop_master", shop_master)
# 販売実績を仮想テーブルとして登録(同名の登録があれば上書き)
duckdb_register(con, "sales", sales, overwrite = TRUE)
# 2つの仮想テーブルを結合して所在市町ごとの売上金額を集計
dbGetQuery(con, "
SELECT m.city, SUM(s.quantity * s.unit_price) AS sales_yen, COUNT(*) AS records
FROM sales AS s
INNER JOIN shop_master AS m ON s.shop_id = m.shop_id
GROUP BY m.city
ORDER BY sales_yen DESC")
city sales_yen records
1 富良野市 57400 2
2 美瑛町 41550 2
3 苫小牧市 30500 2
4 千歳市 27900 2
5 恵庭市 26880 2
# 接続を切断
dbDisconnect(con)仮想テーブルの登録解除:duckdb_unregisterコマンド
duckdb_register()で登録した仮想テーブルを解除します。解除後は、名前でのSQLからの参照ができなくなります。
| オプション | 意味 | 初期値 |
|---|---|---|
| conn | dbConnect()で作成したDuckDBの接続 | なし |
| name | 登録を解除する仮想テーブルの名前 | なし |
# DuckDBへ接続
con <- dbConnect(duckdb(shared_home = FALSE))
# 2つのデータフレームを仮想テーブルとして登録
duckdb_register(con, "shop_master", shop_master)
duckdb_register(con, "sales", sales)
# 登録済みのテーブル一覧を確認
dbListTables(con)
[1] "sales" "shop_master"
# 販売実績の仮想テーブルの登録を解除
duckdb_unregister(con, "sales")
# 解除後のテーブル一覧を確認
dbListTables(con)
[1] "shop_master"
# 接続を切断
dbDisconnect(con)SQL文の実行:sql_execコマンド
接続の管理を行うことなく、テーブルの作成やレコードの追加といった副作用を伴うSQL文を実行し、影響した行数を返します。接続を省略した場合はdefault_conn()が使用されます。
| オプション | 意味 | 初期値 |
|---|---|---|
| sql | 実行するSQL文の文字列 | なし |
| conn | 使用する接続オブジェクト | default_conn() |
# 産地ごとの収穫量を格納するテーブルを作成
sql_exec("CREATE OR REPLACE TABLE harvest (item VARCHAR, area VARCHAR, ton INTEGER)")
[1] 0
# 作成したテーブルへ4件のレコードを追加
sql_exec("
INSERT INTO harvest VALUES
('かぼちゃ', '和寒町', 320),
('とうもろこし', '芽室町', 980),
('じゃがいも', '幕別町', 1540),
('メロン', '富良野市', 260)")
[1] 4 SQLによる問い合わせ:sql_queryコマンド
SQLの問い合わせを実行し、結果をデータフレームとして返します。default_conn()はenvironment_scanによる接続のため、呼び出し元のデータフレームが自動的に参照されます。
| オプション | 意味 | 初期値 |
|---|---|---|
| sql | 実行するSQL文の文字列 | なし |
| conn | 使用する接続オブジェクト | default_conn() |
# sql_execで作成済みのテーブルへ問い合わせ
sql_query("SELECT * FROM harvest ORDER BY ton DESC")
item area ton
1 じゃがいも 幕別町 1540
2 とうもろこし 芽室町 980
3 かぼちゃ 和寒町 320
4 メロン 富良野市 260
# 登録操作を行わずにデータフレームsalesをそのまま集計
sql_query("
SELECT item, SUM(quantity * unit_price) AS sales_yen
FROM sales
GROUP BY item
ORDER BY sales_yen DESC")
item sales_yen
1 とうもろこし 59600
2 アスパラ 47800
3 じゃがいも 30450
4 メロン 28800
5 かぼちゃ 17580
既定の接続の取得:default_connコマンド
パッケージが内部で保持している既定の接続を取得します。
# 既定の接続オブジェクトを取得
conn <- default_conn()
# 取得した接続オブジェクトのクラスを確認
class(conn)
[1] "duckdb_connection"
attr(,"package")
[1] "duckdb"
# 取得した接続を明示して問い合わせを実行
sql_query("SELECT '恵庭市' AS city, 44880 AS sales_yen", conn = conn)
city sales_yen
1 恵庭市 44880保存先ディレクトリの確認:duckdb_storage_statusコマンド
ダウンロードした拡張機能と保存済みシークレットの配置先などを表示します。ディレクトリの作成や問い合わせは実施しません。
# 拡張機能とシークレットの保存先を確認
duckdb_storage_status()ファイルを参照する遅延テーブルの作成:tbl_fileコマンド
ディスク上のParquet、CSV、JSONファイルを直接参照する遅延テーブルを作成します。dplyrの構文で記述した処理はSQLへ変換され、collect()を実行した時点でDuckDB側で評価されます。
| オプション | 意味 | 初期値 |
|---|---|---|
| src | DuckDBの接続オブジェクト(省略時はdefault_conn()) | NULL |
| path | 既存のParquet、CSV、JSONファイルのパス | なし |
| … | 参照先へ引き渡す引数 | なし |
| cache | Parquetファイルのオブジェクトキャッシュを有効にするかの指定 | FALSE |
# 出荷記録を一時ファイルへCSVとして書き出し
csv_path <- file.path(tempdir(), "shipment.csv")
write.csv(shipment, csv_path, row.names = FALSE, fileEncoding = "UTF-8")
# DuckDBへ接続
con <- dbConnect(duckdb(shared_home = FALSE))
# CSVファイルを直接参照する遅延テーブルを作成
tbl_shipment <- tbl_file(con, csv_path)
# 出荷数量が100以上の記録を抽出して結果を取り込み
tbl_shipment %>%
filter(quantity >= 100) %>%
select(ship_date, item, quantity) %>%
collect() %>%
as.data.frame()
ship_date item quantity
1 2026-07-02 とうもろこし 150
2 2026-07-06 とうもろこし 130
# 接続切断
dbDisconnect(con)関数を参照する遅延テーブルの作成:tbl_functionコマンド
read_csv_auto()のようなテーブルを生成するDuckDBの関数から、遅延テーブルを作成します。標準的でない形式のCSVを読み込む場合などに使用します。queryにはFROM句を除いたSQLを指定します。
| オプション | 意味 | 初期値 |
|---|---|---|
| src | DuckDBの接続オブジェクト | なし |
| query | FROM句を除いたSQLの文字列 | なし |
| … | 参照先へ引き渡す引数 | なし |
| cache | Parquetファイルのオブジェクトキャッシュを有効にするかの指定 | FALSE |
# 出荷記録を一時ファイルへCSVとして書き出し
csv_path <- file.path(tempdir(), "shipment.csv")
write.csv(shipment, csv_path, row.names = FALSE, fileEncoding = "UTF-8")
# SQLへ埋め込むためにパスの区切り文字をスラッシュへ変換
csv_sql_path <- normalizePath(csv_path, winslash = "/", mustWork = FALSE)
# DuckDBへ接続
con <- dbConnect(duckdb(shared_home = FALSE))
# read_csv_auto関数を経由してCSVを参照する遅延テーブルを作成
tbl_auto <- tbl_function(con, paste0("read_csv_auto('", csv_sql_path, "')"))
# 直売所ごとの記録件数を集計して結果をRへ取り込み
tbl_auto %>%
count(shop_id) %>%
arrange(shop_id) %>%
collect() %>%
as.data.frame()
shop_id n
1 S01 2
2 S02 1
3 S03 1
4 S04 1
5 S05 1
# 接続を切断
dbDisconnect(con)
この記事が誰かの役に立ちますように。