MySQLのCOUNT 4種類:NULL扱いと使い分けを整理

black samsung android smartphone at 10 00 SQL

MySQLで行数を数えるとき、COUNT(*)COUNT(1)COUNT(col)COUNT(DISTINCT col)の4通りの書き方があります。どれも「件数を返す」関数ですが、NULLの扱いや集計対象が異なります。書き方を間違えると、「ゲストユーザーを除いたつもりが全件を返していた」というバグにつながります。本記事では4種類の挙動を実例と比較表で整理し、どの場面でどれを選ぶかの判断基準を示します。

このシナリオで考える

ECサイトのアクセスログテーブル access_log を使います。user_id が NULL の行は未ログインのゲストユーザーを表します。

CREATE TABLE access_log (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  user_id    INT,            -- ゲストはNULL
  page       VARCHAR(100)    NOT NULL,
  created_at DATETIME        NOT NULL
);

INSERT INTO access_log (user_id, page, created_at) VALUES
(1,    '/home',    '2024-01-01 10:00:00'),
(2,    '/about',   '2024-01-01 10:05:00'),
(NULL, '/contact', '2024-01-01 10:10:00'),
(1,    '/home',    '2024-01-01 10:15:00'),
(3,    '/home',    '2024-01-01 10:20:00');

合計5行のうち id=3 の行だけ user_id = NULL(ゲスト)です。page = '/home' のアクセスは3件あり、user_id のユニーク値は 1・2・3 の3種類です。この前提で4種類のCOUNTを比べます。

COUNT(*) と COUNT(1) の挙動

COUNT(*) はテーブルのすべての行を数えます。列を指定しないため、NULL を含む行もカウント対象です。行そのものの存在を数える構文として、「テーブルの総件数を取得する」最も基本的な書き方です。

SELECT
  COUNT(*)  AS cnt_star,
  COUNT(1)  AS cnt_one
FROM access_log;
+----------+---------+
| cnt_star | cnt_one |
+----------+---------+
|        5 |       5 |
+----------+---------+

どちらも5を返します。COUNT(1) は「各行に定数1を評価し、その件数を数える」構文ですが、MySQL 8.0 の InnoDB ではオプティマイザが COUNT(*) と同一の実行計画で処理します。パフォーマンスに差はありません。

COUNT(1) の方が速い」という説を見かけることがあります。これはかつての古いRDBMSや旧バージョン由来の誤解で、現代のMySQLでは根拠がありません。標準SQL準拠の観点からも、COUNT(*) を使うことを推奨します。

COUNT(col) はNULLを除外して数える

COUNT(col) は、指定した列の値が NULL でない行だけを数えます。列名を指定した時点で「その列に値が存在する行」に絞り込まれます。

SELECT
  COUNT(*)       AS total_rows,
  COUNT(user_id) AS member_accesses
FROM access_log;
+------------+-----------------+
| total_rows | member_accesses |
+------------+-----------------+
|          5 |               4 |
+------------+-----------------+

COUNT(*) は5件(ゲスト含む全行)、COUNT(user_id) は4件(NULL行を除外)を返します。ログインユーザーのアクセス数だけを集計したい場合、COUNT(user_id) が正解です。

「全アクセス数を調べたいのに COUNT(user_id) を書いてしまう」ミスはよくあります。対象列に NULL がひとつでも含まれていると結果が変わるため、全行を数えるときは必ず COUNT(*) を使います。

COUNT(DISTINCT col) で重複を除いた種類数を得る

COUNT(DISTINCT col) は、NULL を除外したうえで重複する値も1件として数えます。「何種類の値が存在するか」「何人のユーザーがアクセスしたか」を知りたいときに使います。

SELECT
  COUNT(user_id)          AS member_accesses,
  COUNT(DISTINCT user_id) AS unique_members
FROM access_log;
+-----------------+----------------+
| member_accesses | unique_members |
+-----------------+----------------+
|               4 |              3 |
+-----------------+----------------+

COUNT(user_id) は4件(NULL除外・重複あり)、COUNT(DISTINCT user_id) は3件(ユーザー1が2回アクセスしているため重複を1と数える)を返します。

MySQLでは複数列を DISTINCT に渡すことも可能です。

-- user_id と page の組み合わせがユニークな件数
SELECT COUNT(DISTINCT user_id, page) AS uniq_combinations
FROM access_log;
+-------------------+
| uniq_combinations |
+-------------------+
|                 3 |
+-------------------+

ユーザー1の /home アクセスは2件ありますが、user_id=1 + page='/home' の組み合わせは1件として数えます。NULL行は除外されます。

4種類の違いを比較表で整理

構文 NULLを数えるか 重複を数えるか 主な用途
COUNT(*) 数える 数える 全行数、テーブルの総件数
COUNT(1) 数える 数える COUNT(*) と実質同じ
COUNT(col) 除外する 数える 特定列に値がある行の件数
COUNT(DISTINCT col) 除外する 除外する ユニークな値の種類数

よくある落とし穴 / 注意点

GROUP BY と組み合わせたとき NULL グループに注意

GROUP BY を使うと NULL もひとつのグループとして集計されます。

SELECT user_id, COUNT(*) AS access_count
FROM access_log
GROUP BY user_id
ORDER BY user_id;
+---------+--------------+
| user_id | access_count |
+---------+--------------+
|    NULL |            1 |
|       1 |            2 |
|       2 |            1 |
|       3 |            1 |
+---------+--------------+

NULL グループを除きたい場合は WHERE user_id IS NOT NULL を前置きします。NULL をひとつの区分として集計に含めたい場合はそのままで問題ありません。

COUNT(DISTINCT) は性能コストが高い

COUNT(DISTINCT col) は内部でソートまたはハッシュ処理が発生するため、COUNT(*) に比べて処理コストが高くなります。数百万行を超えるテーブルで使う場合は EXPLAIN で実行計画を確認しましょう。

EXPLAIN SELECT COUNT(DISTINCT user_id) FROM access_log;

Extra 列に Using index と出ればカバリングインデックスが効いており効率的です。出ない場合は user_id 単独のインデックス追加を検討します。

WHERE と COUNT(col) の組み合わせに注意

WHERE で絞り込んだうえで COUNT(col) を使うと、「WHERE での除外」と「NULL 除外」が二重にかかります。どの条件で件数が減っているかを意識しないと、デバッグが難しくなります。

-- page = '/home' かつ user_id が NULL でない行数
SELECT COUNT(user_id) AS cnt
FROM access_log
WHERE page = '/home';
+-----+
| cnt |
+-----+
|   2 |
+-----+

この結果は WHERE で page='/home' の3行に絞り、さらに NULL を除外した2件です。全条件を明示するなら WHERE page = '/home' AND user_id IS NOT NULL と書いた方が意図が伝わりやすくなります。

まとめ

4種類のCOUNTを使い分ける判断軸は「NULLを含めるか」と「重複を排除するか」の2点です。全行を数えるなら COUNT(*)、NULL を除いた特定列の件数なら COUNT(col)、ユニーク値の種類数なら COUNT(DISTINCT col) が適切です。COUNT(1) は現代のMySQLでは COUNT(*) と同じ挙動のため、可読性を優先して COUNT(*) に統一することを推奨します。

参考リンク

アイキャッチ画像: Photo by KOBU Agency on Unsplash

タイトルとURLをコピーしました