(タイトルがナンノコッチャ!!)
文字だけじゃわからないはずなので、テーブルを用意します。
- posts
id | post_date | category_id |
---|---|---|
1 | 2015-01-01 | 1 |
2 | 2015-02-12 | 2 |
3 | 2015-01-01 | 1 |
4 | 2015-02-12 | 2 |
5 | 2015-01-01 | 1 |
6 | 2015-04-12 | 2 |
7 | 2015-05-01 | 1 |
8 | 2015-06-15 | 3 |
- categories
id | name |
---|---|
1 | プログラミング |
2 | インフラ |
3 | ハードウェア |
4 | データベース |
5 | その他 |
こんな感じで2つのテーブルがあるとする。
要件
やりたいこととして、postsを月ごとにカテゴリでグルーピングしてカウントしたい。
SQLでやるなら、2つのテーブルをJoinしたあとに、gropu by posts.post_date, categories.name
でいけそうだよね?
しかし、もうひとつとして、categoriesテーブルに登録されているデータはすべて表示したい。こういう結果にしたいのだ。
ym | category_name | count |
---|---|---|
201501 | プログラミング | 1 |
201501 | インフラ | 1 |
201501 | ハードウェア | NULL |
201501 | データベース | NULL |
201501 | その他 | 2 |
見たとおり、ハードウェアとデータベースのcount
がNULL
になっているが、これはその月にそのカテゴリが紐付けられた投稿がないからだ。その場合はこのようにして出したい。
試してみたことその1
んで、私はこのような結果を出したいため、こんなSQLを書いてみた。
SELECT DATE_FORMAT(posts.post_date, '%Y%m') AS ym, categories.name AS category_name, COUNT(posts.id) AS count FROM categories LEFT OUTER JOIN posts ON (categories.id = posts.category_id) GROUP BY DATE_FORMAT(posts.post_date, '%Y%m'), categories.name
まぁ知識浅いからこうなるよね。。。単純に2つのテーブルをJoinして、グルーピングしているだけですね。
お気づきだろうけど、これじゃダメですよね。これの結果はNULL
が出てこない。
ym | category_name | count |
---|---|---|
201501 | プログラミング | 1 |
201501 | インフラ | 1 |
201501 | その他 | 2 |
これだと、postsで紐付けられていないcategoriesを出すことができない。当たり前っちゃー当たり前な話で、存在していないデータではjoinすることはできない。
あれ? じゃあなんのためのleft outer join
? って、思った人もいるかもしれないけど、なぜ取れていないのか俺もよくわかっていない(^ω^ゞ
恐らく、unknown
だがなんだかしらない値のせいじゃないかな。
ということで、これではうまく行きません。。。さーてどうしましょうか。
試してみたことその2
最近サブクエリと、Cross Joinを覚えました。この2つを使って解決しましょう。
先ほどの問題はJoinするときに紐付けるデータがないかも知れないことが問題でした。それなら先にそのようなデータを用意しておけばいい。
イメージはこんな感じ.
- CROSS_DATE
ym | category_name | category_id |
---|---|---|
201501 | プログラミング | 1 |
201501 | インフラ | 2 |
201501 | ハードウェア | 3 |
201501 | データベース | 4 |
201501 | その他 | 5 |
- COUNT_BY_MONTH
ym | count | category_id |
---|---|---|
201501 | 1 | 1 |
201501 | 1 | 2 |
201502 | 3 | 3 |
201501 | 2 | 1 |
201505 | 2 | 2 |
これらのデータをサブクエリで用意してあげて、あとはym
とcategory_id
でJoinしてあげれば良い。実際のSQLは下記.
SELECT CROSS_DATE.ym, CROSS_DATE.category_name, COUNT_BY_MONTH.count FROM (SELECT DATE_FORMAT(posts.post_date, "%Y%m") as ym, categories.name as category_name, categories.id as category_id FROM posts CROSS JOIN categories GROUP BY DATE_FORMAT(posts.post_date, '%Y%m'), categories.name) CROSS_DATE LEFT OUTER JOIN (SELECT DATE_FORMAT(posts.post_date, "%Y%m") as ym, count(posts.post_date) as count, category_id FROM posts GROUP BY DATE_FORMAT(posts.post_date, '%Y%m') ) COUNT_BY_MONTH ON (CROSS_DATE.ym = COUNT_BY_MONTH.ym AND CROSS_DATE.category_id = COUNT_BY_MONTH.category_id) ORDER BY CROSS_DATE.ym
これでおk。これで冒頭のデータを取得することができました。cross join
とサブクエリ便利!
おまけ
先ほどの結果はSQLだといいと思うが、PHPで取得するとちと問題がありました。
<?php array( 'CROSS_DATE' => array( 'ym' => '201501', ) // ~ 省略 ~ 'COUNT_BY_MONTH' => array( // 連想配列のkey 'count' が見つからない! ) );
なんと! PHPではNULLはkeyも消してしまうらしい。。。これは困った。
なので、NULLを0に変換してもらうよう、先ほどのSQLにCASEを追加しました。
SELECT CROSS.ym, CROSS.category_name, CASE WHEN COUNT_BY_MONTH.count IS NULL THEN 0 ELSE COUNT_BY_MONTH.count END AS count FROM (SELECT DATE_FORMAT(posts.post_date, "%Y%m") as ym, categories.name as category_name, categories.id as category_id FROM posts CROSS JOIN categories GROUP BY DATE_FORMAT(posts.post_date, '%Y%m') as ym, categories.name) CROSS LEFT OUTER JOIN (SELECT DATE_FORMAT(posts.post_date, "%Y%m") as ym, count(posts.post_date) as count, category_id FROM posts WHERE GROUP BY DATE_FORMAT(posts.post_date, '%Y%m') COUNT_BY_MONTH ON (CROSS.ym = COUNT_BY_MONTH.ym AND CROSS.category_id = COUNT_BY_MONTH.category_id) ORDER BY CROSS.ym
これでよし!
雑感
ちなみに、まだ不足がある。
実はその月に投稿されていない場合は、その月の日付がとれない。(例:1月に投稿がない => 201501のデータが存在しない)
この場合は解決策が見つからなかったので、その穴埋めはPHPで対応しました。
先程も書いたが、そもそもデータが存在しない場合は紐付けることもグルーピングもできないので、これはしょうがないのかなと思う。
でも、日付だけのテーブルを作成したらできないこともないんだよねー。とても面倒だけど。。。
仮想的にサブクエリでデータフォーマットを用意してそいつをCrossJoinとかできたら完璧だろうけど。んな方法あるのかね?
引き続き勉強します。φ(..)メモメモ
達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)
- 作者: ミック
- 出版社/メーカー: 翔泳社
- 発売日: 2008/02/07
- メディア: 単行本(ソフトカバー)
- 購入: 54人 クリック: 1,004回
- この商品を含むブログ (78件) を見る