タイトルどおり。
前の案件でフレームワークを使っていて、現案件に移ってからSQLをゴリゴリ書くのに慣れていなかったので、
勉強したり人に聞いたりしまくった結果、ある程度理解できるようになったSQLを備忘録的に書いていきます。
中の人のSQLスキルが著しく低いゆえに超初歩の初歩です。あしからず。
ちなみにサンプルデータは以下の2つ。見づらくて申し訳ありません・・・
>select * from dogs;
id | dog_name | dog_type | dog_breed | dog_owners_id |
---|---|---|---|---|
1 | ココ | 2 | ラブラドール・レトリーバー | 2 |
2 | マロン | 2 | ジャイアント・シュナウザー | 2 |
3 | レオ | 2 | スコティッシュ・ディアハウンド | 3 |
4 | ラッキー | 2 | ボルゾイ | 5 |
5 | プリン | 2 | ダルメシアン | 1 |
6 | ショコラ | 2 | セント・バーナード | 1 |
7 | モカ | 1 | ポメラニアン | 3 |
8 | 空 | 1 | チワワ | 5 |
9 | クッキー | 1 | ミニチュア・ダックスフンド | 5 |
10 | ベル | 1 | ジャック・ラッセル・テリア | 4 |
11 | ココア | 1 | トイプードル | 4 |
12 | モモ | 1 | キャバリア・キングチャールズ・スパニエル | 2 |
13 | リン | 1 | 柴犬 | 1 |
14 | 姫 | 1 | ヨークシャー・テリア | 4 |
15 | 小太郎 | 1 | ビション・フリーゼ | 3 |
>select * from owners;
id | owners_name | gender | generation |
---|---|---|---|
1 | 三浦 | 1 | 3 |
2 | 筒香 | 2 | 1 |
3 | 今永 | 1 | 2 |
4 | 山﨑 | 2 | 4 |
5 | 関根 | 1 | 5 |
JOIN文
各テーブルの共通カラムを明示することで連結させて結果を表示する。個人的イメージは正規化。
両方に合致したを保つ場合に表示するinner joinと、どちらかにしか値がなくても表示するouter joinがある。
上の例では、dog_owners_idとownersのIDが共通項なのでこれでくっつける。
INNER JOIN テーブル名1 (AS テーブル名1の別名) ON テーブル名2 (AS テーブル名1の別名) 条件式
例:犬の名前とその犬の飼い主の名前を知りたい(INNER JOIN)
> SELECT dog.id,dog.dog_name, ow.owners_name
> FROM dogs AS dog
> INNER JOIN owners AS ow
> ON dog.dog_owners_id = ow.id
> order by dog.id;
表示結果
id | dog_name | owners_name |
---|---|---|
1 | ココ | 筒香 |
2 | マロン | 筒香 |
3 | レオ | 今永 |
4 | ラッキー | 関根 |
5 | プリン | 三浦 |
6 | ショコラ | 三浦 |
7 | モカ | 今永 |
8 | 空 | 関根 |
9 | クッキー | 関根 |
10 | ベル | 山﨑 |
11 | ココア | 山﨑 |
12 | モモ | 筒香 |
13 | リン | 三浦 |
14 | 姫 | 山﨑 |
15 | 小太郎 | 今永 |
dogsのdog_owners_idカラムに、ownersのidカラムの値が入っているすべてのデータが名前と紐付けて表示される。上の例では全員のデータが紐付いていたので、すべてのデータが出てきた。
ここに、
> INSERT INTO owners
> VALUES (6, "白崎", 2, 1);
> INSERT INTO dogs
> VALUES (16, "ポチ", 2, "シーズー", 7);
のデータを追加しても、
両方共に紐づくデータが存在しないため、INNER JOINでは表示されない。
ちなみに、しれっとASを使っているが、
実際の業務だと、長ったらしいテーブル名が付いていることが多かったので、使用頻度は高かった。
今回のようなテーブル名を何回も記入する場合や、あえて別名を付けたい場合などに使う。
ちなみにASがなくてもいい。どういうことかというと、
dogs dog
としてもOKということ。これでdogとして扱ってくれる。可読性は下がりそうだがきちんと別名として認識してくれるのは賢い。
例:犬の名前とその犬の飼い主の名前を知りたい(OUTER JOIN)
OUTER JOINはそのままテーブルをドッキングするイメージに近い。共通項にあたる値が相手テーブルにあろうがなかろうがお構いなしに結合する。その際、どっちが主テーブルにあたるかを必ず明示する。
> SELECT dog.id,dog.dog_name, ow.owners_name
> FROM dogs as dog LEFT JOIN owners as ow
> ON dog.dog_owners_id = ow.id
> ORDER BY dog.id;
id | dog_name | owners_name |
---|---|---|
1 | ココ | 筒香 |
2 | マロン | 筒香 |
3 | レオ | 今永 |
4 | ラッキー | 関根 |
5 | プリン | 三浦 |
6 | ショコラ | 三浦 |
7 | モカ | 今永 |
8 | 空 | 関根 |
9 | クッキー | 関根 |
10 | ベル | 山﨑 |
11 | ココア | 山﨑 |
12 | モモ | 筒香 |
13 | リン | 三浦 |
14 | 姫 | 山﨑 |
15 | 小太郎 | 今永 |
16 | ポチ |
先ほどのINNER JOIN では表示されなかったポチのデータも表示されているのが分かる。ポチのデータと対応するownersのデータがないため、owners_nameは空欄になっている。
上の例の場合、
dogs as dog LEFT JOIN owners as ow
となっているため、左側に示したテーブル、つまりはdogsが主テーブルになっている。実際にはLEFT JOINを用いることが多いらしい。
さて、先ほどお構いなしに結合する、と書いたばかりだが、一つ表示されるはずのデータが足りないことに気づいたでしょうか。
> SELECT * FROM owners WHERE id = 6;
id | owners_name | gender | generation |
---|---|---|---|
6 | 白崎 | 2 | 1 |
ポチと一緒に追加したこいつです。
これがpostgreSQLだけなのか、ほかのRDBでも同じように働くのか、定かではないが、主テーブルに紐付いていない小テーブルの値はOUTER JOINでも表示されない。これを明示しているサイトを調べたのですが、私の力では見つけることができなかった・・・
もちろん、このデータを出力する方法はある。ownersを主テーブルにしてしまえばいい。
> SELECT dog.id,dog.dog_name, ow.owners_name
> FROM dogs as dog RIGHT JOIN owners as ow
> ON dog.dog_owners_id = ow.id
> ORDER BY dog.id;
---別解---
> SELECT dog.id,dog.dog_name, ow.owners_name
> FROM owners as ow LEFT JOIN dogs as dog
> ON dog.dog_owners_id = ow.id
> ORDER BY dog.id;
---別解ここまで---
id | dog_name | owners_name |
---|---|---|
1 | ココ | 筒香 |
2 | マロン | 筒香 |
3 | レオ | 今永 |
4 | ラッキー | 関根 |
5 | プリン | 三浦 |
6 | ショコラ | 三浦 |
7 | モカ | 今永 |
8 | 空 | 関根 |
9 | クッキー | 関根 |
10 | ベル | 山﨑 |
11 | ココア | 山﨑 |
12 | モモ | 筒香 |
13 | リン | 三浦 |
14 | 姫 | 山﨑 |
15 | 小太郎 | 今永 |
白崎 |
そのかわり、今度は小テーブルになったポチのデータが非表示になっている。
全部表示できないものか・・・という時にCROSS JOINを使うといけるそうだが現案件では使ってないので今回は説明を省く。
あと、JOINのJOINとかいう地獄みたいな書き方ももちろんできる。
CASE WHEN
SQLでいうところのif文。
カラムから取得したい値を設定したり、値ごとに分類したり、使いドコロは多い。
例:犬が大型犬か小型犬か知りたい
> SELECT dog_name,dog_breed,
> case
> when dog_type = 1 then '小型犬'
> when dog_type = 2 then '大型犬'
> end as type
> FROM dogs
> order by type;
dog_name | dog_breed | type |
---|---|---|
ベル | ジャック・ラッセル・テリア | 小型犬 |
ココア | トイプードル | 小型犬 |
モモ | キャバリア・キングチャールズ・スパニエル | 小型犬 |
リン | 柴犬 | 小型犬 |
姫 | ヨークシャー・テリア | 小型犬 |
小太郎 | ビション・フリーゼ | 小型犬 |
モカ | ポメラニアン | 小型犬 |
空 | チワワ | 小型犬 |
クッキー | ミニチュア・ダックスフンド | 小型犬 |
ポチ | シーズー | 大型犬 |
ココ | ラブラドール・レトリーバー | 大型犬 |
マロン | ジャイアント・シュナウザー | 大型犬 |
レオ | スコティッシュ・ディアハウンド | 大型犬 |
プリン | ダルメシアン | 大型犬 |
ショコラ | セント・バーナード | 大型犬 |
ラッキー | ボルゾイ | 大型犬 |
case when~ の中で条件を指定し、thenでその条件に合致したカラムにどういう操作をするか、を記述する。書き方はかなりいわゆるif文に近い。
最後に閉じ括弧のかわりにend句をつける。その後ろにasを置くことでカラム名に別名をつけることもできる。
いちおう、これをphpに置き換えてみると、こうなる。
if($dog_type == 1){
$type = '小型犬';
} elseif($dog_type == 2){
$type = '大型犬';
}
このcase文には、2通りの書き方があるらしく、
if文のようにwhen句の中に条件を書き連ねる書き方と、
caseの直後にカラムを指定し値によって振り分けるswitchのような書き方もできる。
> SELECT dog_name,dog_breed,
> case dog_type
> when 1 then '小型犬'
> when 2 then '大型犬'
> end as type
> FROM dogs
> order by type;
これをphpで書くとこんな感じでしょうか。
switch ($dog_type) {
case 1:
$type = '小型犬';
break;
case 2:
$type = '大型犬';
break;
}
caseが紛らわしい感じだが、文法的にはほとんど差はないので、Javaなりphpなり、ちょっと言語をかじった人からしても使いやすいのではないかと。
どちらを使うかは状況次第だと思われる。
例:各ownerごとに飼っている大型犬と小型犬の数を知りたい
case whenとjoinを組み合わせて、こういうことを知ることもできる。推奨されてるやり方かどうかは知らん。
> SELECT own.owners_name,
> SUM(case when dog.dog_type = 1 then 1 end) AS "大型犬",
> SUM(case when dog.dog_type = 2 then 1 end) AS "小型犬"
> FROM owners own
> INNER JOIN dogs dog ON dog.dog_owners_id = own.id
> GROUP BY own.owners_name;
owners_name | 大型犬 | 小型犬 |
---|---|---|
関根 | 2 | 1 |
今永 | 2 | 1 |
山﨑 | 3 | |
三浦 | 1 | 2 |
筒香 | 1 | 2 |
CASE WHENは、設定した条件に合わなかった場合、ELSE句を書くことでデフォルト値を設定できるのだが、
SUM(case when dog.dog_type = 1 then 1 else 2 end)
これは一般的if文と同様に省略が可能で、その際はnullを取得する。この性質を利用すると、カラム内で値ごとにいくつデータが入っているかを上のように分けることができる。dog.dog_typeが1、または2であるごとにフラグを立て、そのフラグが立ったデータの個数を取っているというイメージ。
もっとも、このSQLのキモはGROUP BY句で、これが地味にわかりにくかった。
ちょっと脇道にそれてGROUP BYの説明を試みる
SELECT own.owners_name,dog.dog_type
FROM owners own
INNER JOIN dogs dog ON dog.dog_owners_id = own.id;
owners_name | dog_type |
---|---|
関根 | 2 |
筒香 | 2 |
筒香 | 2 |
今永 | 2 |
三浦 | 2 |
三浦 | 2 |
今永 | 1 |
関根 | 1 |
関根 | 1 |
山﨑 | 1 |
山﨑 | 1 |
筒香 | 1 |
三浦 | 1 |
山﨑 | 1 |
今永 | 1 |
おそらく、INNER JOINはSQLの文中で、いの一番に行われている。共通項として値をとった後、それを元にSELECTやWHEREなどで指定されているカラムや集約方法など、指定された値にそってサマリしていくという手順ではないかと思います。
最初のINNER JOINの例で言えば、idやdog_nameが他のデータと重複のないユニークな値だったため、それ以上サマリすることはできなかったが、今回欲しいデータを抜き出した、上の結果をみると、(どうやら組み合わせにパターンがあるし、まとめられそうだぞ)ということになる。
そこで、owners_nameとdog_typeの組み合わせの個数ごとにサマリしてみましょう!というのが、GROUP BYの趣旨。
カラムを指定して、全データが重複しているデータをまとめる。
> SELECT own.owners_name,dog.dog_type,count(*)
> FROM owners own
> INNER JOIN dogs dog ON dog.dog_owners_id = own.id
> GROUP BY own.owners_name,dog.dog_type
> ORDER BY dog.dog_type;
owners_name | dog_type | count |
---|---|---|
関根 | 1 | 2 |
今永 | 1 | 2 |
三浦 | 1 | 1 |
山﨑 | 1 | 3 |
筒香 | 1 | 1 |
関根 | 2 | 1 |
今永 | 2 | 1 |
三浦 | 2 | 2 |
筒香 | 2 | 2 |
count(*)でその組み合わせの個数をとり、owners_nameとdog_typeで絞込を行っている。"関根"かつ"2"のデータが1つ、"山﨑"かつ"1"のデータが3つあるということがわかる。
これがowners_nameだけだったり、dog_typeだけだったりすると、違うデータが混じっているため、エラーになってしまう。"関根"や"1"だけでサマリできないのは、同じ"関根"でも"1"と"2"が混在していたり、同じ"1"でも"今永"や"三浦"など、対応する異なるデータがいるからだというのは、見てもらえばなんとなーくわかってもらえると思う。
これを見たうえで、SUM(CASE WHEN)の説明を見ると、ぼんやりながら見えてくる気がしませんか?CASE WHENの条件と合わない箇所にはNULLが入る、という性質を思い出すと、
owners_name | 大型犬 | 小型犬 |
---|---|---|
関根 | 1 | |
関根 | 1 | |
三浦 | 1 | |
筒香 | 1 | |
三浦 | 1 |
このような歯抜けのデータを取っているというイメージである。
それをSUMで集計し、owners_nameで集約しているという感じです。そうすると、こうなる。
owners_name | 大型犬 | 小型犬 |
---|---|---|
関根 | 2 | 1 |
今永 | 2 | 1 |
山﨑 | 3 | |
三浦 | 1 | 2 |
筒香 | 1 | 2 |
お分かりいただけただろうか・・・いただけなくても責任はもてませんが・・・
まとめ
- 両方のテーブルに共通項の値を持つものだけを取るINNER JOINと、主テーブルならなりふり構わず値を拾うOUTER JOIN。
- ASで別名。カラム名でもテーブル名でもなんでも名前つけちゃうおじさん。
- 他のプログラミング言語と似た感覚で書けるCASE WHEN文。if形式とswitch形式がある。
- カラム内の値で絞込するGROUP BY。使い方が難しい。