DuckDBのおもしろ仕様メモ
FROM-First Syntax
-
FROM
から書き始めることで、テーブルエイリアスを決めた後でSELECT
句をエイリアス付きでかけて便利。 - だけど、
Linq
ライクにFROM
-WHERE
-SELECT
と書くとエラーにされる。 - 正しくは、
FROM
-SELECT
-WHERE
と書く必要があり不満。
ケツカンマ
- ドキュメントに明示的な記述はなく、
syntax
で判断できる程度ではあるが、SELECT
の最終要素の最後にカンマおいても怒られない。 - 先頭カンマかケツカンマかで第三次世界大戦起きそうな勢いあるので、この仕様は
QOL
高い。
/* これOK */
select 1, 2, 3,
スカラ問い合わせ(SELECT区でのサブクエリ)
以下のように記述した場合、普通(?)のRDB
であれば、サブクエリが複数行返すためエラーにされる。
/* generate_seriesは引数で指定した範囲の数値を返す表関数 */
select (select * from generate_series(1, 10))
一方、DuckDB
はサブクエリの先頭行を返してくるという困ったちゃん。
> select (select * from generate_series(1, 10));
┌────────────────────────────────────────┐
│ (SELECT * FROM generate_series(1, 10)) │
│ int64 │
├────────────────────────────────────────┤
│ 1 │
└────────────────────────────────────────┘
本来必要だったWHERE
を描き忘れてた場合にバグだと気付けなくなる困ったちゃん。
せめて、Nested-Array
で返してくれれば、理解もできるのだけど・・・。
って思ってたんだけど、以下のようにすれば、Nested-Array
で返してくれるのでQOL
をなんとか維持できた。[1]
/* サブクエリのselect句で集約関数使いまとめる */
> select (select list(id) from generate_series(1, 10) t(id));
┌────────────────────────────────────────────────────────┐
│ (SELECT list(id) FROM generate_series(1, 10) AS t(id)) │
│ int64[] │
├────────────────────────────────────────────────────────┤
│ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] │
└────────────────────────────────────────────────────────┘
ちなみにFROM
句ではなくSELECT
句でgenerate_series
を使った場合はNested-Array
を返すのでご心配なく。
> select (select generate_series(1, 10));
┌─────────────────────────────────┐
│ (SELECT generate_series(1, 10)) │
│ int64[] │
├─────────────────────────────────┤
│ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] │
└─────────────────────────────────┘
-
バグの存在に気づけなくなってしまう問題からは目を背けつつ・・・ ↩︎
INSERT文のBY NAMEオプション
普通(?)のRDB
で以下のように書く場合、列の位置の型を合わせないとエラーになる。
insert into foo
select x, y, z from bar
なので、INSERT
には列名を明示しましょうってなる。
insert into foo (x, y, z)
select x, y, z from bar
一方DuckDB
ではBY NAME
オプション使うことで、SELECT
句で指定した名前で自動マッピングしてくれる。
くれるのだが・・・、エイリアス付けると、エイリアス付きの名前でのマッピングを行っちゃう困ったちゃん。[1]
/* FOOテーブルのb.x, b.y, b.zという列名でのマッピングを試みてしまう */
insert into foo by name
select b.x, b.y, b.z from bar b
以下のようにすれば通るが、エイリアスを多用する身としては、それならいっそINSERT
で列名明示すればいいじゃんと思う今日この頃。
insert into foo by name
select b.x as x, b.y as y, b.z as z from bar b
-
擁護すると、構造型を列として定義できるため致し方ない ↩︎
unnestをメソッドチェインで使用する際の注意点
unnest
関数を使用することで、list
, array
, struct
のような構造化された列を行に展開してくれる。
また、duckdb
は、関数をメソッドチェインで記述できる。
これを組み合わせて、以下のように記述すると、items
列を展開できないとされてエラー扱いされてしまう。
select t.items.unnest() from t
一方上記を、以下のように書き換えると問題なく表示される。
select unnest(t.items) from t
メソッドチェインはapply
関数の後のような、2番目の関数に対して使うのが心の平穏を保てそう。
ツイ....Xで流れてきた、PostgreSQL
の基数変換
DuckDB
でもチャレンジ (1.0.0時点)
10進数 -> 2進数
SELECT 33::bit(16);
結果はパースエラー
Parser Error: Type BIT does not support any modifiers!
DuckDB
は幅指定のBIT型
(BITSTRING型
)をサポートしていない模様
以下の記述なら、形だけでも2進数にはなる
select printf('%b', 37);
┌──────────────────┐
│ printf('%b', 37) │
╞══════════════════╡
│ 100101 │
└──────────────────┘
2進数 → 10進数
SELECT '0000000000100001'::bit(16)::int;
幅指定のBIT型
型はエラーになるが、以下のように幅指定を除けば10進数になる模様
SELECT '0000000000100001'::bit::int;
┌──────────────────────────────────────────────────┐
│ CAST(CAST('0000000000100001' AS BIT) AS INTEGER) │
╞══════════════════════════════════════════════════╡
│ 33 │
└──────────────────────────────────────────────────┘
10進数 -> 16進数
SELECT to_hex(33);
┌────────────┐
│ to_hex(33) │
╞════════════╡
│ 21 │
└────────────┘
これは、DuckDB
でも通る
16進数 → 10進数
SELECT 'x0021'::bit;
Conversion Error: Invalid character encountered in string -> bit conversion: 'x'
変換エラーになった。
x始まりのリテラルダメっぽい。
該当する方法も見つからず。むね〜ん。
追記
16進数 → 10進数について以下のコメントをいただきました。
SELECT '0x21'::int;
┌─────────────────────────┐
│ CAST('0x21' AS INTEGER) │
╞═════════════════════════╡
│ 33 │
└─────────────────────────┘
期待通りの結果が得られました。
JOINに関する論理プラン
全く聞いたことないことばかりのものが多かったのでChat GPT
大先生にお伺いを立てた。
LOGICAL_JOIN
Joinに関する論理プランの親クラス
duckdb::LogicalJoin
LOGICAL_DELIM_JOIN
デリミティッド外部結合 (というものらしい)
左外部結合の最適化版
SQL構文木からLOGICAL_COMPARISON_JOIN
なプランが作成されたのち最適化フェーズでLOGICAL_DELIM_JOIN
に差し替えられてそう。
duckdb::LogicalComparisonJoin
- 左側のテーブルの結合キーが少ない ( = カーディナリティが低い)場合に選択されるプラン
- 左側のテーブルをグループ化してJoinの総ループ回数を減らす
- 低カーディナリティゆえ、ハッシュ結合よりも効果的にハッシュ化可能
- 左側のテーブルに比べて右側のテーブルのレコード数が少ない場合にも選択される
- 右側のテーブルでハッシュテーブルを作ることで総ループ回数を減らす
- スカラ問い合わせの際、サブクエリ側のレコード数が少ない場合に選択される事象を確認
- ハッシュ化がいけると判断された模様
LOGICAL_COMPARISON_JOIN
比較演算子(<, <=, >, >=)を使用したJoinに選択されるプラン
duckdb::LogicalComparisonJoin
二重ループを総当たりで回す (Nested Loop Join)
たいていこれが選択されるっぽい
LOGICAL_ANY_JOIN
ORを使用したJoinに選択されるプラン
duckdb::LogicalAnyJoin
LOGICAL_CROSS_PRODUCT
直積 (cross join)を使用した際に選択されるプラン
duckdb::LogicalCrossProduct
LOGICAL_POSITIONAL_JOIN
positional join
を使用した際に選択されるプラン
duckdb::LogicalPositionalJoin
LOGICAL_ASOF_JOIN
asof join
を使用した際に選択されるプラン
LOGICAL_DEPENDENT_JOIN
相関サブクエリのように他のテーブルの値に依存してJoin使用した際に選択されるプラン。
duckdb::LogicalDependentJoin
二重ループを総当たりで回す (Nested Loop Join)
-
lateral join
で使用されやすい?
ただし以下の場合、LOGICAL_DELIM_JOINが選択されることもある。
- 左右のレコード数に大きな開きがある
- 左側のカーディナリティが低い
最適化フェーズで事前計算の結果、相関サブクエリが非相関サブクエリにできると判断された場合、再起的にフラットなJoinに変換される場合もあるっぽい。
ただしlateral
が明示された場合は、極力duckdb::LogicalDependentJoin
に振ってるっぽい。
Joinの論理プランの決定タイミング
TableRef
(FORM
)のステージで大まかに型が分類され、Join
のステージで詳細化される。
LIMIT句
ドキュメントでは、LIMIT
に対して整数値を指定した例のみが表記されている。
実は整数値以外の指定も可能。
そんなアンドキュメントな機能のメモ(そのうち記載されるかも)。
Percentage LIMIT
以下のように、数値%
として割合で指定できる。
PREAPRE q AS SELECT * FROM range(1, 20) LIMIT 20%;
プレースホルダを介して指定したい場合は以下のようにする。
/* %をプレースホルダの外に出し数値だけ渡す */
PREAPRE q AS SELECT * FROM range(1, 20) LIMIT $lim%;
EXECUTE q(lim := 20);
割合の対象は元のクエリのレコードに対して。
OFFSET
でカーソルを動かしたとしても。
LIMIT NULL
NULL
を指定するとリミット指定なしとして扱われる。
プレースホルダを用意し、NULL
を渡した場合も同様
PREAPRE q AS SELECT * FROM range(1, 20) LIMIT $lim;
EXECUTE q(lim := NULL);
LIMIT $lim%
とした上で、NULL
を渡した場合も同様に全件取得。
LIMIT NULL
はPostgreSQL
でサポートされているため、その関係で入っているのかも(なんとなく)
LIMIT ALL
ALL
を指定するとリミット指定なしとして扱われる。
SELECT * FROM range(1, 20) LIMIT ALL;
ただしプレースホルダを介してALL
を渡すと実行時エラーとなる。
代わりにNULL
を渡すよろし
LIMIT ALL
もまた同様にPostgreSQL
でサポートされている機能。
Merk Join
Mark Join
はDuckDB
のクエリプラン内で使用される結合方式であり、SQL
文で直接指定することはできない。
主にエンジン内部で相関サブクエリを効率化するために利用される。
通常、相関サブクエリをそのまま個別に評価すると非効率
Mark Join
はEXISTS
やNOT EXISTS
のようにブール値を返す相関サブクエリの部分的な評価結果をキャッシュし、親リレーションに対する再評価の際にそのキャッシュを使用することで効率を向上させる。
- まず、相関サブクエリの評価結果は依存する列以外で処理され、結果がマーク(ブール値)として保存される。
- その後、マークされた結果と依存する列を組み合わせて再評価を行う。
ただし、単純なEXISTS
クエリであれば、SEMI JOIN
、NOT EXISTS
の場合はANTI JOIN
が結合種として選ばれることが一般的。
Mark Join
が採用されるのは、例えば、EXISTS
句内でさらに別のEXISTS
を使用したり、Lateral Join
の内部でEXISTS
を使用したりする複雑なケース。
手元の環境では、CTE
(共通テーブル式)の結果をLateral Join
内のEXISTS
句で使用することでMark Join
が現れた。
Merk Join
LISTのunnestの怪しい挙動
Case 1: 異なるLISTを一度にunnestする
以下の構造のJSON
があるとして
[
{
"data": {
"k": 42,
"v": {
"v1": "42", "v2": "xyz"
}
},
"id": 99, "code": "99"
},
{
"data":{
"k": 0, "v": null
},
"id": 101, "code": null
}
]
以下のクエリを流すと
select k, v1, unnest(v1), v2, v3, unnest(v3)
from read_json( '_dataset-examples/list_sample.json') t(k, v1, v2, v3)
以下の結果となる。
k varchar |
v1 int64[] |
unnest(v1) int64 |
v2 int64[] |
v3 int64[] |
unnest(v3) int64 |
---|---|---|---|---|---|
odd | [1, 3, 5, 7] | 1 | [2, 4, 6] | [10, NULL, 30] | 10 |
odd | [1, 3, 5, 7] | 3 | [2, 4, 6] | [10, NULL, 30] | |
odd | [1, 3, 5, 7] | 5 | [2, 4, 6] | [10, NULL, 30] | 30 |
odd | [1, 3, 5, 7] | 7 | [2, 4, 6] | [10, NULL, 30] | |
even | [2, 8] | 2 | [20] | 20 | |
even | [2, 8] | 8 | [20] |
case 2: NULLのunnestとの組み合わせ
以下のクエリを流すと
select unnest(j.data_1), unnest(j.data_2.code)
from read_json( '_dataset-examples/struct_sample_2.json') t(j)
k int64 |
v struct(v1 varchar, v2 varchar) |
unnest(j.data_2.code) varchar |
---|---|---|
42 | {'v1': 42, 'v2': xyz} | 99 |
となる。
二行目のdata_2.code
がNULL
のため、たとえunnest(j.data_1)
から結果が取り出せてても、行ごと亡くされる。
data_2.code
をNULL
ではなく空配列にしてもNULL
として扱われてしまう。
VALUES句
duckdb_cli
で
VALUES (1, 2, 3), (4, 5,6);
と入力すると、普通にSELECT
として結果が表示される。
これは、duckdb
のパーザが
SELECT * FROM (VALUES (1, 2, 3), (4, 5,6)) as valueslist
に相当する構文木に作り替えているから。
ただし、
(VALUES (1, 2, 3)) CROSS JOIN (VALUES (4, 5,6))
までは面倒見てくれず、
Parser Error: syntax error at or near "cross"
LINE 1: (values (1, 'a')) cross join values (2, 'b');
となるのでご注意あれ。
SELECT * FROM (VALUES (1, 2, 3)) CROSS JOIN (VALUES (4, 5,6))
とすれば問題なく通る。