Open29

リレーショナルデータを分析するための言語

todeskingtodesking

SQLはカス、どこでも使えてなんでもできる以外の価値がない

アドホックなデータ分析の領域においてSQLより使いやすいソリューションについての考察。

todeskingtodesking

SQLの悪い点/不便な点

todeskingtodesking

文法

再利用性の低さ

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 secondinterval部分は何なんだとか、文法が複雑すぎる。

人間に厳しい

一貫性を捨てても便利な記法を採用して書きやすくしているのかと思いきや、

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処理系はだいたいどれもこんな感じやんけ!!!

todeskingtodesking

データに関する信念を不十分にしか表明できない

わたしたちがテーブル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

ということになってしまうのだった。

todeskingtodesking

joinに冗長な情報を指定する必要がある

いくらアドホックなデータ分析とはいえユーザIDと商品IDでjoinしたいような例は稀でありどのテーブルどうしがどのカラムを使ってjoinされうるかは事前に決定できることが大半であるのだからそのようなトリビアルなjoinについてはいちいち条件を指定せずとも遂行されてしかるべきだが当然そのような高級な機能は存在せずそのかわりに同じ名前のカラム全部使うnatural joinとかいう暴力的なヤケクソ気味のがあるのみなのであった。

todeskingtodesking

スコープの関係で同じ式を複数回書く必要がある

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

余計複雑になったやんけ。

このスコープのルールがおかしいとは思わないけど、どうにかならないだろうか。

todeskingtodesking

selectの位置がおかしい

他の要素は fromwhere group byorderlimit と処理の流れに沿っているのにselectだけ最初にあって変。LINQ(C#)ではselectが最後になっている。

todeskingtodesking

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投げられるやつがあったような気がする…… ああいうの使えばいいのか?

todeskingtodesking

select時に変数定義したい

先ほどのgroup byと同じ問題なのですが。

select count(*) as total, sum(foo = 1)  as foo_count, ratio as foo_count / total  -- むり
from ...

ひょっとして方法があるかと思ってSO見たらサブクエリ使えと書いてあった💢

todeskingtodesking

カラムに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使えばいいだけでは???

todeskingtodesking

count(distinct expr1を満たすexpr2) over(...) が書けない

そういうことをしたくなるときがあるんですよ(今)

todeskingtodesking

「宣言的に書けばオプティマイザが効率的な実行プランを生成してくれる」という嘘が流布されている

じゃあこの頑張って書いたクソクエリも効率的に実行してくれよ!!!!!!!

todeskingtodesking

This version of MariaDB doesn't yet support 'COUNT(DISTINCT) aggregate as window function'

SQLの存在自体がもたらす苦しみと、処理系がSQLの機能を部分的にしか実装していないことからくる苦しみがあることがわかってきた。

todeskingtodesking

一時テーブルに名前をつけたくない

「usersテーブルに購入金額カラムと年代カテゴリを追加したやつ」の名前なんか考えたいわけないだろ!!!

既存テーブルに一時的かつ仮想的なカラムを追加したい。

todeskingtodesking

select f(x) from foo が集約するのかどうか、構文から判断できない

fが集約関数だったら暗黙のうちに全行グルーピングするというアドホックなルールはまさにSQLを象徴しているといえ、それは短い表記とセマンティクスの混乱を同時にもたらします。

リレーショナル代数に基づいていると言い張っている

じゃあこのwindow関数とかオーダーバイとかユニーク制約のないテーブルとかをリレーショナル代数で説明してみろよ!!!

todeskingtodesking

仕様書が無償で公開されていない

プロなら自分で買えとおっしゃる気持ちはわかりますが…… しかし、仕様について気軽に言及できない現状は本当にこれで良いのでしょうか???????

todeskingtodesking

クエリ言語の名前がデータモデルの代名詞にもなっている

SQLの文法がクソだという話をしたらリレーショナル・データモデルがクソだという話をしていると誤解されてクソコメをされるという恐ろしい事件があったらしいです……

todeskingtodesking

新言語の検討

  • 既存のテーブル定義に加えてデータの制約を表現できる機能
  • 再利用可能でデータ型のエラーが静的に検出できるクエリ言語

がほしい。

todeskingtodesking

油断するとすぐ最強の文法を考えそうになるが、そのへんはSQLから変えなくてもいいな……

todeskingtodesking

さっき妄想で書いたやつ:

-- 同じ年生まれが3000人以上いるユーザを選択するクエリ
from users U
let U.birth_year = year(birthday)
group by birth_year
where count(*) > 3000
ungroup
select *
todeskingtodesking

group by colsされた状況でorder by groupすると order by colsになってくれたら嬉しい

todeskingtodesking
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
;






hikarinhikarin

データの制約を表現できる機能

check制約とか create domain とか enum型 とかどうでしょう?

再利用性の低さ

with句 とか temporary table とかで頑張るっていうのはどうでしょう?

todeskingtodesking

with句 とか temporary table とかで頑張るっていうのはどうでしょう?

単一クエリ内でクエリを再利用するならwith句で事足りるわけですが、クエリ間で共有したい場合は結構面倒なので(データ定義を確認した上でcreate temporary table + insert)、何らかのプリプロセッサが欲しくなりますね。

check制約とか create domain とか enum型 とかどうでしょう?

たとえば「カラムaはbに関数従属している」という制約を書きたくて、クエリ処理時にその制約を考慮して欲しい(group by bした状態でselect aできる)という需要があります。

hikarinhikarin

うーん
やはり

  1. 正規化する
  2. group by じゃなくて over partition by で解決できる場合もありそう
  3. 正規化する

になっちゃうかな?

RyuseiRyusei

だいたい同意します。

クエリの再利用

一応、データ定義を確認しなくても

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には現状未対応)

todeskingtodesking

ERモデルとリレーショナルモデルの違いについて今まで意識したことがなかった