本文へスキップ
からだにいいもの

Rのトピックスを中心に『まだ、まだ、知らない、役に立つ情報?』を発信します。

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
bigint64ビット整数の返し方(”numeric”または”integer64″を指定)“numeric”
configDuckDBの設定フラグを格納した名前付きリストlist()
将来の拡張用の引数(指定しない)なし
home拡張機能と保存済みシークレットを置くルートディレクトリNULL
shared_home共有ディレクトリ「~/.duckdb」を使用するかの指定(NULL、TRUE、FALSEを指定)NULL
allow_extensionsDuckDB拡張機能の読み込みを許可するかの指定(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ビット整数、配列型、ジオメトリ列の受け取り方を接続単位で指定できます。

オプション意味初期値
drvduckdb()が返したドライバーオブジェクトなし
dbdirデータベースファイルを配置する既存ディレクトリのパスDBDIR_MEMORY
将来の拡張用の引数(指定しない)なし
debug実行したクエリなどのデバッグ情報を表示するかの指定getOption(“duckdb.debug”, FALSE)
read_only読み取り専用で動作させるかの指定FALSE
timezone_outRへ返す際のタイムゾーン(ローカル時刻はSys.timezone()または””を指定)“UTC”
tz_out_convertタイムスタンプ列の変換方法(”with”または”force”を指定)c(“with”, “force”)
configDuckDBの設定フラグを格納した名前付きリストlist()
bigint64ビット整数の返し方(”numeric”または”integer64″を指定)“numeric”
array配列型の返し方(”none”または”matrix”を指定)“none”
geometryジオメトリ列の返し方(”blob”または”wk”を指定)“blob”
mapMAP型の列の返し方(”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に名前付きベクトルだと、列ごとのデータ型を明示が可能です。

オプション意味初期値
conndbConnect()で作成したDuckDBの接続なし
name作成するテーブルの名前なし
files構造が同一である1つ以上のCSVファイル名なし
将来の拡張用の引数(指定しない)なし
header1行目をヘッダー行として扱うかの指定TRUE
na.stringsNULLとみなす文字列(長さ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から参照できるようにします。登録の際にデータの複製は行われません。

オプション意味初期値
conndbConnect()で作成した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からの参照ができなくなります。

オプション意味初期値
conndbConnect()で作成した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側で評価されます。

オプション意味初期値
srcDuckDBの接続オブジェクト(省略時はdefault_conn())NULL
path既存のParquet、CSV、JSONファイルのパスなし
参照先へ引き渡す引数なし
cacheParquetファイルのオブジェクトキャッシュを有効にするかの指定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を指定します。

オプション意味初期値
srcDuckDBの接続オブジェクトなし
queryFROM句を除いたSQLの文字列なし
参照先へ引き渡す引数なし
cacheParquetファイルのオブジェクトキャッシュを有効にするかの指定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)

この記事が誰かの役に立ちますように。

スポンサーリンク
価格および配送状況は変更される場合があります。購入時は商品ページをご確認ください。
当サイトに表示されている商品情報はAmazonから提供されたものであり、更新または削除される場合があります。
karada-goodはAmazonアソシエイトとして、適格販売により収入を得ています。