notebook

都内でWEB系エンジニアやってます。

DuckDBに入門する

最近よく見かけるので気になってきたため、ちょっと触ってみた

その際のメモ

DuckDB

OLAP+インプロセスのRDB

分析用途で用いる、さらっと読んだ感じだとローカルでいろんな形式のデータを組み合わせて分析などを行う際に便利というイメージだった

データ基盤などで、どこにどんなデータがあるからこうしようみたいな調査にも使えそう

インストール

DuckDB Installation – DuckDB

ドキュメント見るとさまざまなパターンでのインストール方法がある

今回はWSLなのでLinux

$ curl -LO https://github.com/duckdb/duckdb/releases/download/v1.0.0/duckdb_cli-linux-amd64.zip

$ unzip duckdb_cli-linux-amd64.zip
Archive: duckdb_cli-linux-amd64.zip  
inflating: duckdb

$ sudo mv duckdb /usr/local/bin/

$ duckdb sample.duckdb  
v1.0.0 1f98600c2c  
Enter ".help" for usage hints.  
D
D .database
sample: sample.duckdb

SQLiteと同様データファイルを指定してコンソールを起動させてみた

$ duckdb  
v1.0.0 1f98600c2c  
Enter ".help" for usage hints.  
Connected to a transient in-memory database.  
Use ".open FILENAME" to reopen on a persistent database.  
D .database  
memory:

何もファイルを指定しない場合はmemory上で処理してくれるっぽい

インポート色々

CSVはもちろん他にもさまざまな種類のデータを読み込める

さらに、アクセスキーを登録するコマンドも用意されていてあらかじめアクセスキーを用意すればGCSやS3のデータも読める(今回はやってないので次回以降で…)

いくつか試してみた

JSON (Over HTTP)

OVER Httpで試してみた、ローカルファイルももちろん読み込める

ネストしている場合はstructとして読み込んでくれて気が利く

D SELECT id,name,username,company FROM read_json_auto('https://jsonplaceholder.typicode.com/users') LIMIT 10;
┌───────┬──────────────────────────┬──────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│  id   │           name           │     username     │                                                           company                                                            │
│ int64 │         varcharvarchar      │                                   struct("name" varchar, catchphrase varchar, bs varchar)                                    │
├───────┼──────────────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│     1 │ Leanne Graham            │ Bret             │ {'name': Romaguera-Crona, 'catchPhrase': Multi-layered client-server neural-net, 'bs': harness real-time e-markets}          │
│     2 │ Ervin Howell             │ Antonette        │ {'name': Deckow-Crist, 'catchPhrase': Proactive didactic contingency, 'bs': synergize scalable supply-chains}                │
│     3 │ Clementine Bauch         │ Samantha         │ {'name': Romaguera-Jacobson, 'catchPhrase': Face to face bifurcated interface, 'bs': e-enable strategic applications}        │
│     4 │ Patricia Lebsack         │ Karianne         │ {'name': Robel-Corkery, 'catchPhrase': Multi-tiered zero tolerance productivity, 'bs': transition cutting-edge web services} │
│     5 │ Chelsey Dietrich         │ Kamren           │ {'name': Keebler LLC, 'catchPhrase': User-centric fault-tolerant solution, 'bs': revolutionize end-to-end systems}           │
│     6 │ Mrs. Dennis Schulist     │ Leopoldo_Corkery │ {'name': Considine-Lockman, 'catchPhrase': Synchronised bottom-line interface, 'bs': e-enable innovative applications}       │
│     7 │ Kurtis Weissnat          │ Elwyn.Skiles     │ {'name': Johns Group, 'catchPhrase': Configurable multimedia task-force, 'bs': generate enterprise e-tailers}                │
│     8 │ Nicholas Runolfsdottir V │ Maxime_Nienow    │ {'name': Abernathy Group, 'catchPhrase': Implemented secondary concept, 'bs': e-enable extensible e-tailers}                 │
│     9 │ Glenna Reichert          │ Delphine         │ {'name': Yost and Sons, 'catchPhrase': Switchable contextually-based project, 'bs': aggregate real-time technologies}        │
│    10 │ Clementina DuBuque       │ Moriah.Stanton   │ {'name': Hoeger LLC, 'catchPhrase': Centralized empowering task-force, 'bs': target end-to-end models}                       │
├───────┴──────────────────────────┴──────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 10 rows                                                                                                                                                                  4 columns │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

ndjson

さらに、ndjsonでも配列の状態でもよしなに読み込んでくれるよう

  • ndjson
{"a": 1}
{"a": 2}
{"a": 3}
  • json array
[
  {"a": 1},
  {"a": 2},
  {"a": 3}
]

両方結果は下記

D SELECT *
  FROM read_json_auto('/home/user/sample.json');
┌───────┐
│   a   │
│ int64 │
├───────┤
│     1 │
│     2 │
│     3 │
└───────┘

複数データを読み込んでJOINしてみる

CTASでusers,postsテーブルを作成

CREATE TABLE users AS SELECT * FROM read_json_auto('https://jsonplaceholder.typicode.com/users');
CREATE TABLE posts AS SELECT * FROM read_json_auto('https://jsonplaceholder.typicode.com/posts');
D select p.userId,u.id,p.id,u.name,p.title from users u left join posts p on(u.id = p.userId) LIMIT 20;
┌────────┬───────┬───────┬───────────────┬────────────────────────────────────────────────────────────────────────────┐
│ userId │  id   │  id   │     name      │                                   title                                    │
│ int64  │ int64 │ int64 │    varcharvarchar                                   │
├────────┼───────┼───────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│      111 │ Leanne Graham │ sunt aut facere repellat provident occaecati excepturi optio reprehenderit │
│      112 │ Leanne Graham │ qui est esse                                                               │
│      113 │ Leanne Graham │ ea molestias quasi exercitationem repellat qui ipsa sit aut                │
│      114 │ Leanne Graham │ eum et est occaecati                                                       │
│      115 │ Leanne Graham │ nesciunt quas odio                                                         │
│      116 │ Leanne Graham │ dolorem eum magni eos aperiam quia                                         │
│      117 │ Leanne Graham │ magnam facilis autem                                                       │
│      118 │ Leanne Graham │ dolorem dolore est ipsam                                                   │
│      119 │ Leanne Graham │ nesciunt iure omnis dolorem tempora et accusantium                         │
│      1110 │ Leanne Graham │ optio molestias id quia eum                                                │
│      2211 │ Ervin Howell  │ et ea vero quia laudantium autem                                           │
│      2212 │ Ervin Howell  │ in quibusdam tempore odit est dolorem                                      │
│      2213 │ Ervin Howell  │ dolorum ut in voluptas mollitia et saepe quo animi                         │
│      2214 │ Ervin Howell  │ voluptatem eligendi optio                                                  │
│      2215 │ Ervin Howell  │ eveniet quod temporibus                                                    │
│      2216 │ Ervin Howell  │ sint suscipit perspiciatis velit dolorum rerum ipsa laboriosam odio        │
│      2217 │ Ervin Howell  │ fugit voluptas sed molestias voluptatem provident                          │
│      2218 │ Ervin Howell  │ voluptate et itaque vero tempora molestiae                                 │
│      2219 │ Ervin Howell  │ adipisci placeat illum aut reiciendis qui                                  │
│      2220 │ Ervin Howell  │ doloribus ad provident suscipit at                                         │
├────────┴───────┴───────┴───────────────┴────────────────────────────────────────────────────────────────────────────┤
│ 20 rows                                                                                                   5 columns │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

EXCEL

Excel Import – DuckDB

SELECT * FROM st_read('/mnt/c/Users/user/sample.xlsx', layer='sheet1');  
Catalog Error: Table Function with name "st_read" is not in the catalog, but it exists in the spatial extension.  
  
Please try installing and loading the spatial extension:  
INSTALL spatial;  
LOAD spatial;

EXCELファイルの読み込みはプラグインが必要

インストールしたら読み込めた

Parquet

サンプル用にCSV→Parquetの簡単な変換用コードを用意して試した

  • csv2parquet.py
import pandas as pd
import sys

def csv_to_parquet(input_file, output_file):
    # CSVファイルの読み込み
    df = pd.read_csv(input_file)
    # Parquetファイルへの書き込み
    df.to_parquet(output_file, engine='pyarrow')

def main():
    if len(sys.argv) != 3:
        print("Usage: python convert.py <input_file> <output_file>")
        sys.exit(1)
    
    input_file = sys.argv[1]
    output_file = sys.argv[2]
    
    csv_to_parquet(input_file, output_file)

if __name__ == "__main__":
    main()
  • jsonからCSV、CSVからParquetへ
$ python csv2parquet.py users_1.csv users_1.parquet
  • 読み込み
D SELECT * FROM read_parquet('/home/user/users_1.parquet');  
┌───────┬─────────┬───────┬─────────────────────┐  
│ id │ name │ age │ email │  
│ int64 │ varchar │ int64 │ varchar │  
├───────┼─────────┼───────┼─────────────────────┤  
│ 1 │ Alice │ 30 │ alice@example.com │  
│ 2 │ Bob │ 25 │ bob@example.com │  
│ 3 │ Charlie │ 35 │ charlie@example.com │  
└───────┴─────────┴───────┴─────────────────────┘

複数ファイル指定

*で複数の複数のファイルを対象に読み込むことも可能

D SELECT * FROM read_json('/home/user/users_1.json');
┌───────┬─────────┬───────┬─────────────────────┐
│  id   │  name   │  age  │        email        │
│ int64 │ varchar │ int64 │       varchar       │
├───────┼─────────┼───────┼─────────────────────┤
│     1 │ Alice   │    30 │ alice@example.com   │
│     2 │ Bob     │    25 │ bob@example.com     │
│     3 │ Charlie │    35 │ charlie@example.com │
└───────┴─────────┴───────┴─────────────────────┘
D SELECT * FROM read_json('/home/user/users_2.json');
┌───────┬─────────┬───────┬───────────────────┐
│  id   │  name   │  age  │       email       │
│ int64 │ varchar │ int64 │      varchar      │
├───────┼─────────┼───────┼───────────────────┤
│   101 │ David   │    40 │ david@example.com │
│   102 │ Eva     │    28 │ eva@example.com   │
│   103 │ Frank   │    33 │ frank@example.com │
└───────┴─────────┴───────┴───────────────────┘
D SELECT * FROM read_json('/home/user/users*.json');
┌───────┬─────────┬───────┬─────────────────────┐
│  id   │  name   │  age  │        email        │
│ int64 │ varchar │ int64 │       varchar       │
├───────┼─────────┼───────┼─────────────────────┤
│     1 │ Alice   │    30 │ alice@example.com   │
│     2 │ Bob     │    25 │ bob@example.com     │
│     3 │ Charlie │    35 │ charlie@example.com │
│   101 │ David   │    40 │ david@example.com   │
│   102 │ Eva     │    28 │ eva@example.com     │
│   103 │ Frank   │    33 │ frank@example.com   │
└───────┴─────────┴───────┴─────────────────────┘

これかなり便利だよなー

CLIでの利用

$ cat users_1.json | duckdb -json -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true) WHERE age >= 30"  
[{"id":1,"name":"Alice","age":30,"email":"alice@example.com"},  
{"id":3,"name":"Charlie","age":35,"email":"charlie@example.com"}]

-json で出力フォーマットにJSONを指定

/dev/stdinで標準入力から読み込むことを指示

あとは普通のSQL

ちょっと凝ったフィルタとかは完全にこの手法が楽、jqでやりきるのは結構時間掛かる…

環境変数の値をクエリに含めてみた

$ export AGE=30
$ cat users_1.json | duckdb -echo -line -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true) WHERE age >= $AGE+1"  
SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true) WHERE age >= 30+1  
id = 3  
name = Charlie  
age = 35  
email = charlie@example.com

-echoで、どのようなSQLを実行しているのかっていうのがログに残せる

色々気遣いがあって良き

出力フォーマット

結構種類あった

$ cat users_1.json | duckdb  -ascii -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
idnameageemail1Alice30alice@example.com2Bob25bob@example.com3Charlie35charlie@example.com

$  cat users_1.json | duckdb  -box -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
┌────┬─────────┬─────┬─────────────────────┐
│ id │  name   │ age │        email        │
├────┼─────────┼─────┼─────────────────────┤
│ 1  │ Alice   │ 30  │ alice@example.com   │
│ 2  │ Bob     │ 25  │ bob@example.com     │
│ 3  │ Charlie │ 35  │ charlie@example.com │
└────┴─────────┴─────┴─────────────────────┘

$ cat users_1.json | duckdb  -column -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
id  name     age  email
--  -------  ---  -------------------
1   Alice    30   alice@example.com
2   Bob      25   bob@example.com
3   Charlie  35   charlie@example.com

$ cat users_1.json | duckdb  -csv -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
id,name,age,email
1,Alice,30,alice@example.com
2,Bob,25,bob@example.com
3,Charlie,35,charlie@example.com

$ cat users_1.json | duckdb  -json -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
[{"id":1,"name":"Alice","age":30,"email":"alice@example.com"},
{"id":2,"name":"Bob","age":25,"email":"bob@example.com"},
{"id":3,"name":"Charlie","age":35,"email":"charlie@example.com"}]

$ cat users_1.json | duckdb  -line -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
   id = 1
 name = Alice
  age = 30
email = alice@example.com

   id = 2
 name = Bob
  age = 25
email = bob@example.com

   id = 3
 name = Charlie
  age = 35
email = charlie@example.com

$ cat users_1.json | duckdb  -list -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
id|name|age|email
1|Alice|30|alice@example.com
2|Bob|25|bob@example.com
3|Charlie|35|charlie@example.com

$ cat users_1.json | duckdb  -markdown -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
| id |  name   | age |        email        |
|---:|---------|----:|---------------------|
| 1  | Alice   | 30  | alice@example.com   |
| 2  | Bob     | 25  | bob@example.com     |
| 3  | Charlie | 35  | charlie@example.com |

$ cat users_1.json | duckdb  -quote -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
'id'|'name'|'age'|'email'
1|'Alice'|30|'alice@example.com'
2|'Bob'|25|'bob@example.com'
3|'Charlie'|35|'charlie@example.com'

$ cat users_1.json | duckdb  -html -c "SELECT * FROM read_json_auto('/dev/stdin', ignore_errors=true)"
<TR><TH>id</TH>
<TH>name</TH>
<TH>age</TH>
<TH>email</TH>
</TR>
<TR><TD>1</TD>
<TD>Alice</TD>
<TD>30</TD>
<TD>alice@example.com</TD>
</TR>
<TR><TD>2</TD>
<TD>Bob</TD>
<TD>25</TD>
<TD>bob@example.com</TD>
</TR>
<TR><TD>3</TD>
<TD>Charlie</TD>
<TD>35</TD>
<TD>charlie@example.com</TD>
</TR>

感想

個人的には大昔にawkqで感動してたときのことを思い出した

何かしら便利ツール作りたくなってくるやつだなーと思いました