LoginSignup
8
6

More than 3 years have passed since last update.

count(*) するときに気をつけたい NULL

Posted at

create table contents (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY)
comment='コンテンツ';
insert into contents values (), (), (), (), (), ();
create table view_logs (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, content_id BIGINT NOT NULL)
comment='コンテンツに対する閲覧ログ';
insert into view_logs (content_id) values (1), (2), (3);
select * from contents;
select * from view_logs;

今回は簡便さを優先して、外部キー制約は付けていません。

コンテンツ 閲覧ログ
image.png image.png
6レコード 3レコード。コンテンツIDが4,5,6は閲覧ログがありません。

各コンテンツの閲覧回数は何回でしょうか?

次のようになるはずです。

コンテンツID 閲覧回数
1 1
2 1
3 1
4 0
5 0
6 0

次にSQLを書いてみます。

失敗例

内部結合を使ってカウント

INNER JOINで閲覧ログとコンテンツを結合してしまってはいけません。閲覧ログの中にすべてのコンテンツIDが現れないためです。

閲覧ログにあるコンテンツIDしか閲覧数をカウントすることができません。


SELECT
    ct.id AS content_id,
    count(*) AS view_count
FROM
    contents AS ct
    JOIN view_logs AS vl ON ct.id = vl.content_id
GROUP BY
    content_id

image.png

外部結合を使ってカウント

閲覧ログとコンテンツIDを内部結合すると、閲覧ログに存在しないコンテンツIDがカウントできませんでした。

それなら、コンテンツIDを基準に外部結合をすれば、すべてのコンテンツIDが網羅できます。
しかし、安直に count(*) を使うと罠にハマります。


SELECT
    ct.id as content_id
    ,count(*) as view_count
FROM
    contents AS ct
    LEFT OUTER JOIN view_logs AS vl ON ct.id = vl.content_id
group by
    ct.id

閲覧ログがないはずのコンテンツID(4,5,6)が閲覧回数が1になっています。

image.png

これは、 count(*) によって行単位でカウントされてしまっていることが原因です。

COUNT(*) は行数をカウントするため、ペットの数をカウントするクエリーは次のようになります。

出典:https://dev.mysql.com/doc/refman/5.6/ja/counting-rows.html

COUNT 関数には NULL を件数に含めるために特別な アスタリスク(*)を使用する構文が用意されており NULL を含めて行数を取得することができる。

出典:https://www.shift-the-oracle.com/sql/aggregate-functions/count.html

count する前のレコードを観察する

countする前のレコードを見ると、レコードの数は6で、content_idが4,5,6にはlog_idがNULLになっています。


SELECT
    ct.id AS content_id,
    vl.id AS log_id
FROM
    contents AS ct
    LEFT OUTER JOIN view_logs AS vl ON ct.id = vl.content_id

image.png

count(*) はレコードが存在すればカウントするため、すべてのコンテンツIDについて 1 とカウントします。

成功例

カウントしたいカラムを指定して外部結合を使う

view_logs の content_id カラムか id カラムを count() に与えれば、求めていた結果が得られます。

count() には式が入るようになっていて、 NULL を与えた場合 0 とカウント するため、 * ではなく NULL になりうるカラム名を与えれば、正しくカウントできます。


SELECT
    ct.id as content_id,
    count(vl.content_id) as view_count
FROM
    contents AS ct
    LEFT OUTER JOIN view_logs AS vl ON ct.id = vl.content_id
group by
    ct.id

image.png

実験後の後始末

不要になったテーブルはサクッと消して終了です。

drop table view_logs;
drop table contents;
8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6