# install.packages("pak")
pak::pak("duckdb")はじめに
Rで大量のデータを扱う際、メモリ制限やパフォーマンスの問題に直面することがあります。そんなとき、軽量で高速な組み込み型のデータベースであるDuckDBを利用することで、効率的にデータ操作が可能になります。本記事では、RでDuckDBを使用する方法について解説します。
DuckDBとは
DuckDBは、組み込み型のSQLデータベースであり、特に分析ワークロードに最適化されています。SQLiteのように軽量でありながら、大規模なデータセットを効率的に処理できる点が特徴です。DuckDBは、列指向ストレージを採用しており、分析クエリのパフォーマンスが向上します。
ちなみに組み込み型データベースとは、アプリケーションに組み込まれて動作するデータベースのことを指します。サーバー型のデータベースとは異なり、外部のデータベースサーバーを必要とせず、アプリケーション内で直接データベース操作が可能です。すなわちR上で完結するということです。
列指向については、ブログでparquetを扱った際に少し触れています。以下もご覧ください。簡潔に言うと、列指向データベースは、データを列ごとに格納するため、特定の列に対するクエリが高速に処理されるという特徴があります。
DuckDBのインストール
DuckDBをRで使用するには、duckdbパッケージをインストールします。
もしくは
install.packages("duckdb")DuckDBの基本的な使い方
データベースへの接続
まずは、DuckDBデータベースに接続します。以下のコードでは、メモリ内データベースに接続していますが、ファイルベースのデータベースを使用することも可能です。
メモリ内データベースはRセッションが終了すると消えてしまいますが、ファイルベースのデータベースは永続的に保存されます。
ファイルベースのデータベースではディスクにデータが保存されるため、Rセッションを終了してもデータが保持されます。一方、メモリ内データベースはRセッションが終了するとデータが失われてしまいますが、読み書きの速度が速いという利点があります。
library(duckdb)# メモリ内データベースに接続
con <- dbConnect(duckdb(), dbdir = ":memory:")ファイルベースのデータベースに接続する場合は、以下のようにします。
# ファイルベースのデータベースに接続
con <- dbConnect(duckdb(), dbdir = here::here("data/iris.duckdb"))この方法だと、dataフォルダにiris.duckdbという名前でデータベースファイルが作成されます。.duckdbファイルを置くパスは自由に変更してください。
データの読み込み
SQLを使う場合
DuckDBは様々なデータ形式をサポートしています。ここでは、CSVファイルを読み込む例を示します。
# CSVファイルを読み込む
dbExecute(con, "CREATE TABLE iris AS SELECT * FROM read_csv('data/iris.csv')")さて、ここで突然SQL文が出てきましたが、なんとなく単語を見てもらえればわかる通り、read_csv('data/iris.csv')から全ての列を選択(*は全ての列を意味します)して、irisという名前のテーブルを作成(CREATE TABLE iris AS)しています。
ちなみに、僕はSQLはあまりわかりません。いちいち調べないと先ほどのコードも書けないレベルなのですが、この方法をとるメリットを先に説明しておきます。
この方法のメリットは、Rに一度もデータを読み込まずに、DuckDBが直接CSVファイルを処理できる点です。 そのため、大規模なCSVでもメモリを無駄に消費せず、高速にテーブルを作成できます。
また、CREATE TABLE ... AS SELECT ...という形で書けば、SQLの標準的な構文で柔軟に前処理(列の選択やフィルタリングなど)を行いながら、テーブルを作成できます。
Rを使う場合
とはいえ新しくSQLを覚えるのは面倒だしRで書けたらうれしいという僕なので、ここでRを使うことの意義を示します。
それは、今提示したコードを皆さんおなじみdplyrを使って実行できる、ということです。
それはすなわちdplyrを使って書いたコードをSQLに変換して実行してくれるということを意味しています。
データベースやSQLはなんだかよくわからないけれども、dplyrが使えるならうれしいですよね1。
その場合のコードを見てみましょう。
library(tidyverse) # `dplyr`だけでもOKです。iris_data <- read_csv(here::here("data/iris.csv"))
duckdb_register(con, iris, "iris_data")まずはデータベースに入れたいデータをread_csv()で読み込みます。ここではdataフォルダにあるiris.csvを読み込んでいます。
続いてduckdb_register()を使って、iris_dataをirisという名前でデータベースに登録しています。これでデータベース上にirisテーブルが作成されました。
ちなみにファイルベースのデータベースを使いたい場合は、duckdb_register()の代わりにdbWriteTable()を使います。
dbWriteTable(con, "iris", iris_data)これで.duckdbファイルにirisテーブルが保存されます。
ちなみに、データベースにはテーブルを複数作成できるので、iris以外のデータもも好きな名前でテーブルを追加できます2。
データの操作
ところで、データベースを使うメリットのひとつに、遅延評価(lazy evaluation) があります。これは、実際に結果を取り出すまでSQLが実行されない仕組みのことです。
通常のデータ処理は
データの読み込み → 書いた順に処理を実行 → 結果を返す
という流れですが、遅延評価では
データの読み込み → 書いた順に処理を記録 → 最後にまとめて実行(SQL発行) → 結果を返す
という流れになります。
このおかげで、大規模なデータセットでも不要な計算が省かれ、効率的に処理できます。
SQLを使う場合
SQLを使ってデータを操作する場合、以下のようにクエリを実行します。
# データをクエリする
result <- dbGetQuery(con, "SELECT * FROM iris WHERE Species = 'setosa'")この例では、irisテーブルからSpeciesがsetosaの行を選択しています。
dplyrを使う場合
dplyrを使ってデータを操作する場合、以下のように記述します(おなじみの感じですね)。
result <- tbl(con, "iris") |>
filter(Species == "setosa") |>
collect()tbl(con, "iris")でデータベース上のirisテーブルを参照し、filter()で条件を指定しています。最後にcollect()を使って結果をRのデータフレームとして取得します。
ここで重要なのは、collect()を呼び出すまではSQLは実行されないという点です。filter()などの処理を追加するたびに、裏でSQLクエリが少しずつ組み立てられていき、最後にcollect()を呼び出した時点でまとめて実行されます。
つまり、collect()を呼び出すまではデータはRに読み込まれず、操作はあくまで「SQLを組み立てているだけ」です。collect()を実行した瞬間に初めてSQLが発行され、結果がRにデータフレームとして取り込まれます。
データベースになじみがない方は違和感があるかもしれませんが(かく言う僕もですが)、con自体はデータそのものではなく、DuckDBデータベースへの接続(コネクション)を表すオブジェクトです。
普段のRではデータフレームを直接操作しますが、ここではまず接続オブジェクトconを通して「このデータベースの中にあるテーブルを参照しますよ」という指示を出します。
つまり、conはデータの実体ではなく、「データベースへの窓口」や「リモコン」のようなものと考えると分かりやすいです(ChatGPT曰く)。
とりあえず、collect()の前までは通常のデータ処理と同じようにdplyrの関数を使って操作できるので、慣れ親しんだ方法でデータを扱うことができます。
テーブルの削除
追加したテーブルを削除する場合は、以下のようにします。
dbExecute(con, "DROP TABLE iris")これでirisテーブルがデータベースから削除されます。
もし安全に削除したい場合は、
dbExecute(con, "DROP TABLE IF EXISTS iris")とすることで、テーブルが存在しない場合でもエラーにならずに処理を続行できます。
データベースからの切断
データベースの操作が終わったら、接続を切断します。
dbDisconnect(con, shutdown = TRUE)shutdown = TRUEを指定すると、メモリ内データベースの場合はデータが消去され、ファイルベースのデータベースの場合は接続が閉じられます。
{duckplyr}を使う
duckplyrパッケージを使うと、DuckDBとdplyrの連携がさらに便利になります。duckplyrは、DuckDB専用のdplyr拡張パッケージであり、DuckDBの機能をより活用できます。
duckplyrとは
duckplyrは、dplyrのドロップイン置き換えとして機能するパッケージです。既存のdplyrコードをそのまま使いながら、裏側でDuckDBの高速な分析エンジンを活用できます。
先述のアプローチとの違い
これまで紹介してきたduckdbパッケージを使った方法とduckplyrを使った方法の違いを整理します。
duckdb × dbplyr(データベース操作)
library(dplyr)
library(DBI)
# データベースに接続
con <- dbConnect(duckdb::duckdb())
# データベース上のテーブルを参照
tbl(con, "my_table") |>
filter(value > 100) |>
summarise(total = sum(value))- データベースに接続する
- dplyrの構文をSQLに翻訳して実行
- データは基本的にDB上に存在
collect()で初めてRのメモリに読み込まれる
duckplyr(メモリ内データの高速化)
library(duckplyr)
df <- data.frame(x = 1:1000000, y = rnorm(1000000))
df |>
filter(y > 0) |>
summarise(mean_x = mean(x))- データベース接続不要
- Rのメモリ上のデータをDuckDBエンジンで高速処理
- SQLへ翻訳はされない(データはR上に存在)
- 既存のdplyrコードがそのまま高速化される
duckplyrの特徴
- ドロップイン置き換え
library(duckplyr)するだけで、多くのdplyr動詞が高速化- 既存のコードを書き換える必要がほとんどない
- 自動フォールバック
- duckplyrで対応できない操作は、自動的に通常のdplyrにフォールバック
- エラーで止まることなく処理が継続されます
- 大規模データに最適
- DuckDBの並列処理エンジンを活用
- メモリ効率の良い処理が可能
- 互換性
- dplyrの多くの関数に対応
- tidyverseエコシステムとシームレスに連携
簡単な使用例
library(tidyverse)
library(duckplyr)
#> Loading required package: dplyr
#> ✔ Overwriting dplyr methods with duckplyr methods.
#> ℹ Turn off with `duckplyr::methods_restore()`.
# 通常のdplyr構文がそのまま使えます
mtcars |>
group_by(cyl) |>
summarise(
mean_mpg = mean(mpg),
mean_hp = mean(hp),
.groups = "drop"
)# A tibble: 3 × 3
cyl mean_mpg mean_hp
<dbl> <dbl> <dbl>
1 4 26.7 82.6
2 6 19.7 122.
3 8 15.1 209.
注意点として、読み込み順序が重要です。library(tidyverse)の後にlibrary(duckplyr)を読み込むことで、dplyrのメソッドがduckplyrで上書きされます。逆の順序で読み込むと、通常のdplyrが優先されてしまいます。
小さいデータだとあまり速さを実感できないかもしれませんが、大きいデータを扱うときに効果を発揮します。
まとめ
今回は、RでDuckDBを使用する方法についてまとめました。
DuckDBは軽量で高速な組み込み型データベースであり、大規模なデータセットを効率的に処理できます。SQLを直接使う方法と、dplyrを使う方法の両方が利用可能であり、遅延評価によりパフォーマンスが向上します。
また、duckplyrパッケージを使うことで、既存のdplyrコードをほぼそのまま高速化できるため、データ処理の効率化が図れます。
最近は仕事ででかいデータを扱うことが増えてきたので、今後もDuckDBを活用していきたいと思います(というか活用し始めています)。ぜひお試しあれ。