2023-06-10 第106回R勉強会@東京
@eitsupi
https://duckdblabs.com/news/2021/07/14/spin-off-company-DuckDB-Labs.html
.files add
コマンドでローカルのファイルをアップロードしてduckdb -c 'from foo.csv' --markdown >foo.md
のようにMarkdownへの変換もおまかせ!サンプルとしてdplyr
に含まれているstarwars
データの元になっているCSVファイルをダウンロードします。
curl -sL https://raw.githubusercontent.com/tidyverse/dplyr/v1.1.2/data-raw/starwars.csv -o starwars.csv
read_csv_auto
関数でデータを読み込みます。
┌────────────────┬────────┬────────┬───┬──────────────────────┬──────────────────────┐
│ name │ height │ mass │ … │ vehicles │ starships │
│ varchar │ int64 │ double │ │ varchar │ varchar │
├────────────────┼────────┼────────┼───┼──────────────────────┼──────────────────────┤
│ Luke Skywalker │ 172 │ 77.0 │ … │ Snowspeeder, Imper… │ X-wing, Imperial s… │
│ C-3PO │ 167 │ 75.0 │ … │ │ │
│ R2-D2 │ 96 │ 32.0 │ … │ │ │
├────────────────┴────────┴────────┴───┴──────────────────────┴──────────────────────┤
│ 3 rows 14 columns (5 shown) │
└────────────────────────────────────────────────────────────────────────────────────┘
FROM
句で開始するクエリDuckDBではSELECT
句ではなくFROM句
でクエリを開始できます。
dplyr
だとtidyselect
で↓のように書ける
「列名のパターンで複数列を選択する」という処理
DuckDBはCOLUMNS
を使ってこう書けます。
┌────────┬─────────┬───┬──────────────────────┬──────────────────────┐
│ mass │ species │ … │ vehicles │ starships │
│ double │ varchar │ │ varchar │ varchar │
├────────┼─────────┼───┼──────────────────────┼──────────────────────┤
│ 77.0 │ Human │ … │ Snowspeeder, Imper… │ X-wing, Imperial s… │
│ 75.0 │ Droid │ … │ │ │
│ 32.0 │ Droid │ … │ │ │
│ 136.0 │ Human │ … │ │ TIE Advanced x1 │
│ 49.0 │ Human │ … │ Imperial Speeder B… │ │
├────────┴─────────┴───┴──────────────────────┴──────────────────────┤
│ 5 rows 5 columns (4 shown) │
└────────────────────────────────────────────────────────────────────┘
dplyr
だと↓のように書ける
「特定の列を置換する」という処理
DuckDBではREPLACE
を使ってこう書けます。
FROM read_csv_auto('starwars.csv', header=true, nullstr='NA')
SELECT * REPLACE (height * 1000 AS height)
LIMIT 5
┌────────────────┬────────┬────────┬───┬──────────────────────┬──────────────────────┐
│ name │ height │ mass │ … │ vehicles │ starships │
│ varchar │ int64 │ double │ │ varchar │ varchar │
├────────────────┼────────┼────────┼───┼──────────────────────┼──────────────────────┤
│ Luke Skywalker │ 172000 │ 77.0 │ … │ Snowspeeder, Imper… │ X-wing, Imperial s… │
│ C-3PO │ 167000 │ 75.0 │ … │ │ │
│ R2-D2 │ 96000 │ 32.0 │ … │ │ │
│ Darth Vader │ 202000 │ 136.0 │ … │ │ TIE Advanced x1 │
│ Leia Organa │ 150000 │ 49.0 │ … │ Imperial Speeder B… │ │
├────────────────┴────────┴────────┴───┴──────────────────────┴──────────────────────┤
│ 5 rows 14 columns (5 shown) │
└────────────────────────────────────────────────────────────────────────────────────┘
DISTINCT ON
dplyr
だと↓のように書ける
「グループ毎に最大の値を持つ行だけを残す」という処理
DISTINCT ON
DuckDBはDISTINCT ON
でこう書けます。
FROM read_csv_auto('starwars.csv', header=true, nullstr='NA')
SELECT DISTINCT ON (sex)
sex, name, mass
ORDER BY mass DESC
┌────────────────┬───────────────────────┬────────┐
│ sex │ name │ mass │
│ varchar │ varchar │ double │
├────────────────┼───────────────────────┼────────┤
│ hermaphroditic │ Jabba Desilijic Tiure │ 1358.0 │
│ male │ Grievous │ 159.0 │
│ none │ IG-88 │ 140.0 │
│ female │ Beru Whitesun lars │ 75.0 │
│ │ Sly Moore │ 48.0 │
└────────────────┴───────────────────────┴────────┘
QUALIFY
句dplyr
だと↓のように書ける
「グループ毎に上位n行を残す」という処理
QUALIFY
句DuckDBはQUALIFY
句でこう書けます。
FROM read_csv_auto('starwars.csv', header=true, nullstr='NA')
SELECT
sex, name, mass
QUALIFY
row_number() over (partition by sex order by mass desc) <= 2
┌────────────────┬───────────────────────┬────────┐
│ sex │ name │ mass │
│ varchar │ varchar │ double │
├────────────────┼───────────────────────┼────────┤
│ female │ Beru Whitesun lars │ 75.0 │
│ female │ Shaak Ti │ 57.0 │
│ │ Sly Moore │ 48.0 │
│ │ Ric Olié │ │
│ hermaphroditic │ Jabba Desilijic Tiure │ 1358.0 │
│ male │ Grievous │ 159.0 │
│ male │ Darth Vader │ 136.0 │
│ none │ IG-88 │ 140.0 │
│ none │ C-3PO │ 75.0 │
└────────────────┴───────────────────────┴────────┘
duckdb
Rパッケージの速度を測ってみましょう。
DuckDBのリポジトリに置かれているいつものParquet3をダウンロードします。
curl::curl_download(
"https://github.com/duckdb/duckdb-data/releases/download/v1.0/lineitemsf1.snappy.parquet",
"lineitemsf1.snappy.parquet"
)
サクッと時間を計りたいのでtictoc
パッケージをロードします。
DuckDBとSQLiteのインメモリデータベーを作成しておきます。
事前にParquetからデータを読み込んでおきます。(duckdb
使用)
22.707 sec elapsed
tic()
df_lineitemsf1 |>
dtplyr::lazy_dt() |>
dplyr::filter(
l_shipdate >= "1994-01-01", l_shipdate < "1995-01-01",
l_discount >= 0.05, l_discount < 0.07,
l_quantity < 24
) |>
dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
dplyr::collect()
# A tibble: 1 × 1
revenue
<dbl>
1 75207768.
5.58 sec elapsed
Parquetデータセットに対して直接クエリを実行できます。
tic()
arrow::open_dataset("lineitemsf1.snappy.parquet") |>
dplyr::filter(
l_shipdate >= "1994-01-01", l_shipdate < "1995-01-01",
l_discount >= 0.05, l_discount < 0.07,
l_quantity < 24
) |>
dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
dplyr::collect()
# A tibble: 1 × 1
revenue
<dbl>
1 75207768.
1.175 sec elapsed
Parquetデータセットに対して直接クエリを実行できます。(Hive風パーティションは未対応)
tic()
polars::scan_parquet("lineitemsf1.snappy.parquet")$filter(
polars::pl$col("l_shipdate") >= "1994-01-01" & polars::pl$col("l_shipdate") < "1995-01-01" &
polars::pl$col("l_discount") >= 0.05 & polars::pl$col("l_discount") < 0.07 &
polars::pl$col("l_quantity") < 24
)$select(
(polars::pl$col("l_extendedprice") * polars::pl$col("l_discount"))$sum()$alias("revenue")
) |>
as.data.frame()
revenue
1 75207768
0.855 sec elapsed
データをインメモリテーブルに書き込む必要があります。
tic()
dplyr::tbl(con_sqlite, "lineitemsf1") |>
dplyr::filter(
l_shipdate >= "1994-01-01", l_shipdate < "1995-01-01",
l_discount >= 0.05, l_discount < 0.07,
l_quantity < 24
) |>
dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
dplyr::collect()
# A tibble: 1 × 1
revenue
<dbl>
1 75207768.
3.191 sec elapsed
データフレームをDB内にコピーする必要はありません。
tic()
# 仮想テーブルとしてR DataFrameをDBに登録
duckdb::duckdb_register(con, "lineitemsf1", df_lineitemsf1, overwrite = TRUE)
duckdb:::sql("
FROM lineitemsf1
SELECT
sum(l_extendedprice * l_discount) AS revenue
WHERE
l_shipdate >= '1994-01-01' AND l_shipdate < '1995-01-01'
AND l_discount >= 0.05 AND l_discount < 0.07
AND l_quantity < 24
", con
)
revenue
1 75207768
0.442 sec elapsed
tic()
dplyr::tbl(con, "lineitemsf1.snappy.parquet") |>
dplyr::filter(
l_shipdate >= "1994-01-01", l_shipdate < "1995-01-01",
l_discount >= 0.05, l_discount < 0.07,
l_quantity < 24
) |>
dplyr::summarise(
revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)
) |>
dplyr::collect()
# A tibble: 1 × 1
revenue
<dbl>
1 75207768.
0.581 sec elapsed
tic()
# 仮想テーブルとしてR DataFrameをDBに登録
duckdb::duckdb_register(con, "lineitemsf1", df_lineitemsf1, overwrite = TRUE)
dplyr::tbl(con, "lineitemsf1") |>
dplyr::filter(
l_shipdate >= "1994-01-01", l_shipdate < "1995-01-01",
l_discount >= 0.05, l_discount < 0.07,
l_quantity < 24
) |>
dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
dplyr::collect()
# A tibble: 1 × 1
revenue
<dbl>
1 75207768.
1.222 sec elapsed
duckdblabs組織下で活発に開発されている謎のRパッケージ。
dbplyr
がdplyr
をSQLに翻訳するのに対し、duckplyr
はDuckDBのリレーショナルAPI(?)に翻訳しクエリを実行する。
materializing:
---------------------
--- Relation Tree ---
---------------------
Distinct
Aggregate [sum(*(l_extendedprice, l_discount))]
Filter [(>=(l_shipdate, '1994-01-01') AND <(l_shipdate, '1995-01-01') AND >=(l_discount, 0.05) AND <(l_discount, 0.07) AND <(l_quantity, 24.0))]
r_dataframe_scan(0x560b29736908)
---------------------
-- Result Columns --
---------------------
- revenue (DOUBLE)
# A tibble: 1 × 1
revenue
<dbl>
1 75207768.
8.893 sec elapsed
Enjoy!
#Tokyo.R
しかし現在最新の0.8.0ではオートコンプリートが壊れているようです。 (duckdb/duckdb#7564)
DuckDB quacks Arrow: A zero-copy data integration between Apache Arrow and DuckDB