リレーショナルデータを分析するための言語
SQLはカス、どこでも使えてなんでもできる以外の価値がない
アドホックなデータ分析の領域においてSQLより使いやすいソリューションについての考察。
SQLの悪い点/不便な点
文法
再利用性の低さ
let premium_users = select * from users where premium = 1;
select count(*) from premium_users;
select * from premium_users order by created_at;
のようなことがしたいわけですよ。でも無理!ワハハ!!
一貫性のなさ
cast(expr as type)
という関数のようでそうでもない謎の記法とか(MariaDBリファレンスだと"function"らしいですが)、 '2023-01-01' + interval 1 second
のinterval
部分は何なんだとか、文法が複雑すぎる。
人間に厳しい
一貫性を捨てても便利な記法を採用して書きやすくしているのかと思いきや、
select a, b, from X;
は当然のようにエラーになるのである。ケツカンマくらい許せよ。
また、MariaDBにおいて
-- on指定が抜けている
select * from X left join Y where a = 1;
は
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'where c = 1' at line 1
という不親切なエラーメッセージが返ってくる。また、存在しないカラムを指定した際のエラーメッセージには位置情報が含まれなかったりもする。人間に厳しすぎる!!!
それはSQL言語の問題ではなく特定のSQL処理系の問題じゃないかって? そうだがSQL処理系はだいたいどれもこんな感じやんけ!!!
データに関する信念を不十分にしか表明できない
わたしたちがテーブルAとBをjoinしたいときは何も考えずやっているわけではなく、
- Aの行ごとに、対応するBの行が0..1行存在する
- Aの行ごとに、対応するBの行が1行存在する
- Aの行ごとに、対応するBの行が0行以上存在する
- Aの行ごとに、対応するBの行が1行以上存在する
などいずれかの信念の元におこなっているわけだが、といった情報はSQLに一切現れずただA join B
と書かれているだけなのである。
また、非正規化されたテーブルにおいては、カラム間に関数従属性が存在することは多々あり、わたしたちはその知識を前提にクエリを書くわけだが、といったことはいっさいSQLに現れないため、
select product_id, product_name, count(*)
from receipts
-- product_nameはproduct_idに応じて一意に定まるためほんらいであればproduct_idのみでgroup byすればじゅうぶんであるところそのような情緒を解する機能を持ち合わせていない愚かなRDBのクレームに応えてproduct_nameもさらに指定する必要があるのだった
group by procuct_id, product_name
ということになってしまうのだった。
joinに冗長な情報を指定する必要がある
いくらアドホックなデータ分析とはいえユーザIDと商品IDでjoinしたいような例は稀でありどのテーブルどうしがどのカラムを使ってjoinされうるかは事前に決定できることが大半であるのだからそのようなトリビアルなjoinについてはいちいち条件を指定せずとも遂行されてしかるべきだが当然そのような高級な機能は存在せずそのかわりに同じ名前のカラム全部使うnatural joinとかいう暴力的なヤケクソ気味のがあるのみなのであった。
スコープの関係で同じ式を複数回書く必要がある
select age >= 60 as is_old, count(*)
from users
group by age >= 60
group byの地点でis_old
が見えないので面倒、まあサブクエリでも使えばいいんですが
select is_old, count(*)
from (
select age >= 60 as is_old, * from users
)
group by is_old
余計複雑になったやんけ。
このスコープのルールがおかしいとは思わないけど、どうにかならないだろうか。
selectの位置がおかしい
他の要素は from
→ where
group by
→ order
→ limit
と処理の流れに沿っているのにselect
だけ最初にあって変。LINQ(C#)ではselectが最後になっている。
group byで複数カラムに同時に同じ演算したい
まったく一般的な悩みではなさそうですが、集計クエリ書いてて
with 商品単位集計 as (
select 商品ID, 商品カテゴリ, xx金額, yy金額, xx個数, ...
),
カテゴリ単位集計 as (
select 商品カテゴリ, sum(xx金額) as xx金額, sum(yy金額) as yy金額, sum(xx個数) xx個数, ...
from 商品単位集計
group by 商品カテゴリ
)
という具合に何度も同じカラム名を書く必要がありつらい!!!
そういえばpandasのインタフェースでSQL投げられるやつがあったような気がする…… ああいうの使えばいいのか?
select時に変数定義したい
先ほどのgroup byと同じ問題なのですが。
select count(*) as total, sum(foo = 1) as foo_count, ratio as foo_count / total -- むり
from ...
ひょっとして方法があるかと思ってSO見たらサブクエリ使えと書いてあった💢
カラムにarrayを保持したりexplode的なことをしたい
explode
というのは一般的ではないですがSpark SQLにある。select explode([1, 2, 3])
で select 1 union select 2 union select 3
相当になる変なやつです。
需要としては、
-- 同じエンティティが複数の行に分かれている(そういうことがあります。ありました。)ので名寄せしたい
select unique_name, group_to_array(id) as ids
group by unique_name
having count(*) > 1
--> | unique_name | id |
-- | name1 | [1, 2, 3] |
-- とか、
select unique_name, explode(group_to_array(id))
group by unique_name
having count(*) > 1
--> | unique_name | id |
-- | name1 | 1 |
-- | name1 | 2 |
-- | name2 | 9 |
-- とかしたい
もうリレーショナルデータとかいう次元じゃないしこれをSQLの悪い点と言うのはどうかと思う、でも便利なので…………
↑よく考えたら、この例ならgroup by
じゃなくwindow使えばいいだけでは???
insert文でCTEが使えない
MariaDB固有の制約かもしれない
count(distinct expr1を満たすexpr2) over(...)
が書けない
そういうことをしたくなるときがあるんですよ(今)
「宣言的に書けばオプティマイザが効率的な実行プランを生成してくれる」という嘘が流布されている
じゃあこの頑張って書いたクソクエリも効率的に実行してくれよ!!!!!!!
This version of MariaDB doesn't yet support 'COUNT(DISTINCT) aggregate as window function'
SQLの存在自体がもたらす苦しみと、処理系がSQLの機能を部分的にしか実装していないことからくる苦しみがあることがわかってきた。
一時テーブルに名前をつけたくない
「usersテーブルに購入金額カラムと年代カテゴリを追加したやつ」の名前なんか考えたいわけないだろ!!!
既存テーブルに一時的かつ仮想的なカラムを追加したい。
select f(x) from foo
が集約するのかどうか、構文から判断できない
f
が集約関数だったら暗黙のうちに全行グルーピングするというアドホックなルールはまさにSQLを象徴しているといえ、それは短い表記とセマンティクスの混乱を同時にもたらします。
リレーショナル代数に基づいていると言い張っている
じゃあこのwindow関数とかオーダーバイとかユニーク制約のないテーブルとかをリレーショナル代数で説明してみろよ!!!
仕様書が無償で公開されていない
プロなら自分で買えとおっしゃる気持ちはわかりますが…… しかし、仕様について気軽に言及できない現状は本当にこれで良いのでしょうか???????
クエリ言語の名前がデータモデルの代名詞にもなっている
SQLの文法がクソだという話をしたらリレーショナル・データモデルがクソだという話をしていると誤解されてクソコメをされるという恐ろしい事件があったらしいです……
新言語の検討
- 既存のテーブル定義に加えてデータの制約を表現できる機能
- 再利用可能でデータ型のエラーが静的に検出できるクエリ言語
がほしい。
油断するとすぐ最強の文法を考えそうになるが、そのへんはSQLから変えなくてもいいな……
さっき妄想で書いたやつ:
-- 同じ年生まれが3000人以上いるユーザを選択するクエリ
from users U
let U.birth_year = year(birthday)
group by birth_year
where count(*) > 3000
ungroup
select *
group by cols
された状況でorder by group
すると order by cols
になってくれたら嬉しい
table users (
id integer
);
table purchases (
id integer,
user_id integer,
price integer,
item_id integer
);
table items(
id integer
);
let join users |-o< purchases on users.id = purchases.user_id;
let join purchases |-| items on purchases.item_id = items.id;
let column users.total_purchases = (select coalesce(sum(price), 0) from self -o< purchases);
select * from users u
join u |-o< receipts r
join r |-| items i
;
データの制約を表現できる機能
check制約とか create domain とか enum型 とかどうでしょう?
再利用性の低さ
with句 とか temporary table とかで頑張るっていうのはどうでしょう?
with句 とか temporary table とかで頑張るっていうのはどうでしょう?
単一クエリ内でクエリを再利用するならwith句で事足りるわけですが、クエリ間で共有したい場合は結構面倒なので(データ定義を確認した上でcreate temporary table + insert)、何らかのプリプロセッサが欲しくなりますね。
check制約とか create domain とか enum型 とかどうでしょう?
たとえば「カラムaはbに関数従属している」という制約を書きたくて、クエリ処理時にその制約を考慮して欲しい(group by bした状態でselect aできる)という需要があります。
うーん
やはり
- 正規化する
- group by じゃなくて over partition by で解決できる場合もありそう
- 正規化する
になっちゃうかな?
だいたい同意します。
クエリの再利用
一応、データ定義を確認しなくても
create temporary table premium_users as
select * from users where premium = 1;
で目的は達成できます。
カラム間の関数従属性
集約時に、MySQLやSQLiteのように非集約カラムがあろうと無視する実装では暗黙の関数従属性を自己責任で使うことができるが、そうでなければ
select product_id, max(product_name) as product_name, count(*)
from receipts
group by procuct_id
のように適当な集約関数を噛ませてお茶を濁すしかないですね。
これは気持ちが悪いので、先進的なSQL処理系では any_value
集約関数を使って
select product_id, any_value(product_name) as product_name, count(*)
from receipts
group by procuct_id
のような指定ができるようになっているけど、これはこれで、複数の値の存在を無視しているだけで、関数従属性の宣言になっているわけではない。
データに関する信念
リレーショナルモデルを超えた信念を記述しようとすると、ORMになるというか、実体関連モデルとリレーショナルモデルのミスマッチが問題になっている気がします。
join
natural join をもう少し穏健にした join ... using (...)
が使えるので、まあそれで頑張るとか。それにしたって、カラム名が完全一致しているものどうしでjoinできるだけであって、どっちかというとforeign keyを使ってjoinして欲しい気持ちがあります。
Erqについて
現状のErqは基本的にSQLiteのセマンティクスをいじらない方向で設計しているので、解決する問題は基本的にはシンタクス上で解決できるものだけになっています。もっと根本的に、セマンティクスから手を入れたい気持ちもありますが、まあ、シンタクスだけでもだいぶ改善できるので、まだやっていません。
再利用
view view_name = query
または table table_name = query
の形でビューやテーブルを定義できるので、一般的なプログラミング言語の変数宣言の書き味とほぼ変わらない。
view temp.premium_users = users[premium = 1];;
premium_users {count(*)};;
premium_users order by created_at;;
table temp.premium_users_20231012 = premium_users;;
あるいは、SQLではCTE(with recursive句)でやるような再帰クエリもできるようにしています。
table rel(x, y) = values [{1, 2}, {2, 3}, {2, 4}, {3, 5}];;
table closure(x, y) = rel {x, x}; c: closure -:y:x:> r: rel { c.x, r.y } distinct;;
selectとgroup
ブレース・アロー記法 { ... => ... }
を導入することで、group by句と二重に書かずに済むようにしてある。
users { is_old: age >= 60 => count(*) }
ブレース(select)が後に来るので、サブクエリも、単にブレースを連鎖させるだけになる。
users { is_old: age >= 60, * } { is_old => count(*) }
join
上に出てきた c: closure -:y:x:> r: rel { c.x, r.y }
は、SQLで書くと select c.x, r.y from closure as c join rel as r on c.y = r.x
という感じです。(複数カラムでのjoinには現状未対応)
ERモデルとリレーショナルモデルの違いについて今まで意識したことがなかった