- SQL
SQL COUNTの使い方|DISTINCT・条件付きカウントをDB別に解説
COUNTとは、行の数を数える集計関数です。COUNT(*) は行そのものを数え、COUNT(列) はその列が NULL でない行だけを数え、COUNT(DISTINCT 列) は重複を除いた値の種類を数えます。 「条件に合う行だけ数える」には、COUNT(CASE WHEN 条件 THEN 1 END) がどのデータベースでも動く書き方です。
この記事では、同じサンプル表に対して6通りの数え方を実行し、結果を横に並べて比べます。MySQL 8.0.46・PostgreSQL 16.15・SQLite 3.45.1 で実際に実行した出力を載せ、SQL Server と Oracle は公式ドキュメントで構文を確認して対応表にまとめました。NULL の扱い、複数列の DISTINCT、0件のグループを出す LEFT JOIN、100万行での速度比較、件数を速く知る方法、COUNT() OVER まで扱います。
なお、MySQL 8.0 は2026年4月にサポートが終了しています。手元で動かせた版が 8.0.46 のためその結果を載せていますが、ここで使う COUNT の書き方は標準的なものです。8.4 以降をお使いの場合は、同じ SQL を手元で流して確認してください。
SQL COUNTの早見表
| やりたいこと | 書き方 | NULL の扱い |
|---|---|---|
| 全行を数える | COUNT(*) |
NULL の行も数える |
| 値が入っている行を数える | COUNT(列) |
その列が NULL の行は数えない |
| 値の種類を数える | COUNT(DISTINCT 列) |
NULL は種類に含めない |
| 条件に合う行を数える(どのDBでも動く) | COUNT(CASE WHEN 条件 THEN 1 END) |
条件に合わない行は NULL になり数えない |
| 条件に合う行を数える(標準SQLの FILTER) | COUNT(*) FILTER (WHERE 条件) |
PostgreSQL・SQLite で動作 |
| 条件に合う行を数える(短い書き方) | SUM(条件) |
MySQL・SQLite で動作 |
| NULL の行を数える | COUNT(*) - COUNT(列) |
― |
| グループごとに数える | COUNT(*) ... GROUP BY 列 |
NULL も1つのグループになる |
| 件数で絞り込む | HAVING COUNT(*) >= 2 |
― |
| 0件のグループも出す | LEFT JOIN と COUNT(相手側の列) |
COUNT(*) にすると1になる |
| 各行に件数を付ける | COUNT(*) OVER (PARTITION BY 列) |
― |
| 条件つきで種類を数える | COUNT(DISTINCT CASE WHEN 条件 THEN 列 END) |
― |
サンプル表(この記事で使う8行)に対する実行結果を1つの表にまとめると次のとおりです。3つのデータベースで同じ値になりました(「―」は構文エラーになった組み合わせ)。
| 数え方 | 式 | MySQL 8.0 | PostgreSQL 16 | SQLite 3.45 |
|---|---|---|---|---|
| 全行 | COUNT(*) |
8 | 8 | 8 |
| クーポンあり | COUNT(coupon) |
4 | 4 | 4 |
| クーポンの種類 | COUNT(DISTINCT coupon) |
2 | 2 | 2 |
| 支払い済み(CASE) | COUNT(CASE WHEN status='paid' THEN 1 END) |
5 | 5 | 5 |
| 支払い済み(FILTER) | COUNT(*) FILTER (WHERE status='paid') |
― | 5 | 5 |
| 支払い済み(SUM) | SUM(status='paid') |
5 | ― | 5 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL COUNTの使い方|DISTINCT・条件付きカウントをDB別に解説」
サンプル表を用意する
会員表 members と注文表 orders を使います。次の SQL は MySQL・PostgreSQL・SQLite のどれでもそのまま実行できます。
CREATE TABLE members (id INTEGER PRIMARY KEY, name VARCHAR(20) NOT NULL);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
member_id INTEGER, -- 会員でない注文は NULL
status VARCHAR(10) NOT NULL, -- paid / pending / cancelled
coupon VARCHAR(10), -- クーポンを使わなければ NULL
amount INTEGER NOT NULL
);
INSERT INTO members VALUES (1,'青木'),(2,'井上'),(3,'上田'),(4,'遠藤');
INSERT INTO orders VALUES
(1,1,'paid','A10',1200),(2,1,'paid',NULL,800),(3,1,'cancelled',NULL,500),
(4,2,'paid','A10',3000),(5,2,'pending','B20',1500),
(6,3,'paid',NULL,700),(7,3,'paid','A10',700),
(8,NULL,'pending',NULL,400);
注文は8行。coupon が NULL の行が4つ、member_id が NULL の行が1つあります。会員の遠藤さん(id=4)には注文がありません。
COUNT(*)・COUNT(列)・COUNT(DISTINCT) の違い
SELECT COUNT(*) AS c_all,
COUNT(coupon) AS c_coupon,
COUNT(member_id) AS c_member,
COUNT(DISTINCT coupon) AS d_coupon,
COUNT(DISTINCT member_id) AS d_member
FROM orders;
実行結果(PostgreSQL 16。MySQL 8.0・SQLite でも同じ値):
c_all | c_coupon | c_member | d_coupon | d_member
-------+----------+----------+----------+----------
8 | 4 | 7 | 2 | 3
COUNT(*)は 8。行があれば、列の値に関係なく数えます。COUNT(coupon)は 4。coupon が NULL の4行は数えません。COUNT(DISTINCT coupon)は 2。A10とB20の2種類で、NULL は種類に含めません。COUNT(member_id)は 7、COUNT(DISTINCT member_id)は 3 です。
「COUNT(列) は NULL を数えない」が、COUNT で最も大事な性質です。 条件付きカウントの書き方は、すべてこの性質を利用しています。NULL そのものの性質は、別記事「SQLのNULLの扱い|COALESCE・NULLIFと落とし穴12選」で扱っています。
NULL の行を数える
NULL の件数を知りたいときは、引き算か CASE を使います。WHERE coupon = NULL は常に0件になります(NULL との = 比較は真にならないため)。
SELECT COUNT(*) - COUNT(coupon) AS null_cnt,
COUNT(CASE WHEN coupon IS NULL THEN 1 END) AS null_cnt2
FROM orders; -- 4 | 4
SELECT COUNT(*) FROM orders WHERE coupon = NULL; -- 0(IS NULL と書く)
行がないときの COUNT は 0、SUM は NULL
条件に合う行が1つもないとき、COUNT は 0 を返しますが、SUM や MAX は NULL を返します。
SELECT COUNT(*) AS c, SUM(amount) AS s, MAX(amount) AS m
FROM orders WHERE amount > 99999;
+---+------+------+
| c | s | m |
+---+------+------+
| 0 | NULL | NULL |
+---+------+------+
ただし GROUP BY を付けると、行がなければグループも作られないため、結果は0行になります(0という行は返りません)。この違いは「0件のグループを出す」の節で使います。
条件付きカウントの書き方6通りを比べる
「支払い済み(status='paid')の注文数」を、いろいろな書き方で数えます。
SELECT COUNT(CASE WHEN status='paid' THEN 1 END) AS case_cnt,
SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END) AS case_sum,
COUNT(CASE WHEN status='paid' THEN 1 ELSE 0 END) AS else0,
COUNT(status='paid') AS bool_cnt,
COUNT(status='paid' OR NULL) AS or_null,
COUNT(NULLIF(status,'paid')) AS not_paid
FROM orders;
実行結果(MySQL 8.0。PostgreSQL 16・SQLite でも同じ値):
+----------+----------+-------+----------+---------+----------+
| case_cnt | case_sum | else0 | bool_cnt | or_null | not_paid |
+----------+----------+-------+----------+---------+----------+
| 5 | 5 | 8 | 8 | 5 | 3 |
+----------+----------+-------+----------+---------+----------+
| 書き方 | 結果 | 解説 |
|---|---|---|
COUNT(CASE WHEN 条件 THEN 1 END) |
5 | 推奨。 ELSE を書かないと条件外は NULL になり、数えられない |
SUM(CASE WHEN 条件 THEN 1 ELSE 0 END) |
5 | 1 と 0 を足す。こちらは ELSE 0 が必要 |
COUNT(CASE WHEN 条件 THEN 1 ELSE 0 END) |
8 | 0 も「値」なので全行が数えられる |
COUNT(条件) |
8 | 条件の結果は真か偽で、どちらも NULL ではないので全行 |
COUNT(条件 OR NULL) |
5 | 偽のとき 偽 OR NULL が NULL になることを利用 |
COUNT(NULLIF(列, 値)) |
3 | 「その値ではない行」を数える |
COUNT と ELSE 0 の組み合わせは、条件に関係なく全行を数えます。 COUNT に渡すなら ELSE を書かない、SUM に渡すなら ELSE 0 を書く、と覚えると迷いません。
FILTER 句(PostgreSQL・SQLite)
標準SQLには、集計関数に条件を付ける FILTER 句があります。
SELECT COUNT(*) FILTER (WHERE status='paid') AS f_paid,
COUNT(DISTINCT member_id) FILTER (WHERE status='paid') AS f_dist,
COUNT(coupon) FILTER (WHERE status='paid') AS f_coupon
FROM orders;
PostgreSQL 16 と SQLite 3.45 の結果:
f_paid | f_dist | f_coupon
--------+--------+----------
5 | 3 | 3
MySQL 8.0 では構文エラーになりました。
ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(WHERE status='paid') AS f_paid, ...
SUM(条件)(MySQL・SQLite)
MySQL と SQLite は、条件式の結果を 1 か 0 の数値として扱うので、そのまま SUM で足せます。
SELECT SUM(status='paid') AS sum_bool FROM orders; -- MySQL・SQLite: 5
PostgreSQL 16 では、真偽値は数値と別の型なのでエラーになります。整数に変換すれば同じ書き方ができます。
ERROR: function sum(boolean) does not exist
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
SELECT SUM((status='paid')::int) AS sum_cast FROM orders; -- PostgreSQL: 5
条件つきで「種類」を数える
「支払い済みの注文がある会員は何人か」のように、条件と DISTINCT を組み合わせるときは、CASE の THEN に数えたい列を書きます。
SELECT COUNT(DISTINCT CASE WHEN status='paid' THEN member_id END) AS paid_members
FROM orders; -- 3
割合を出す
SELECT ROUND(100.0 * COUNT(CASE WHEN status='paid' THEN 1 END) / COUNT(*), 1) AS paid_pct
FROM orders; -- 62.5
100.0 と小数で書いておくと、整数同士の割り算で小数点以下が切り捨てられるのを避けられます(3つのデータベースとも 62.5 になりました)。
データベース別:動く書き方の対応表
| 書き方 | MySQL 8.0 | PostgreSQL 16 | SQLite 3.45 | SQL Server | Oracle | BigQuery |
|---|---|---|---|---|---|---|
COUNT(*) / COUNT(列) |
動く | 動く | 動く | 構文あり | 構文あり | 関数あり |
COUNT(DISTINCT 列) |
動く | 動く | 動く | 構文あり | 構文あり | ※1 |
COUNT(CASE WHEN ... END) |
動く | 動く | 動く | 標準的な書き方 | 標準的な書き方 | 標準的な書き方 |
COUNT(*) FILTER (WHERE ...) |
エラー | 動く | 動く | 構文に記載なし | 構文に記載なし | ※1 |
SUM(条件) |
動く | エラー(::int で可) |
動く | ※2 | ※2 | ※1 |
COUNT(DISTINCT a, b) |
動く | エラー | エラー | 構文は式1つ | 構文は式1つ | ※1 |
COUNT(DISTINCT (a, b)) |
― | 動く(NULL を含む組も数える) | ― | ― | ― | ― |
COUNT(*) OVER (...) |
動く | 動く | 動く | 構文あり | 構文あり | ※1 |
| 条件付きカウント専用の関数 | ― | ― | ― | ― | ― | COUNTIF |
- PostgreSQL 16 では
pg_typeof(COUNT(*))が bigint を返しました。 - 「動く」「エラー」は、この記事のために MySQL 8.0.46・PostgreSQL 16.15・SQLite 3.45.1 で実行して確かめた結果です。
- SQL Server と Oracle(19c のリファレンス)は、今回公式ドキュメントの COUNT のページを開いて構文を確認しました(実行はしていません)。SQL Server の構文は
COUNT ( { [ [ ALL | DISTINCT ] expression ] | * } )で、戻り値が int の上限(2,147,483,647)を超える場合はCOUNT_BIGを使うと説明されています。Oracle は DISTINCT 付きの COUNT を分析関数として使う場合、指定できるのは PARTITION BY だけと記載されています。 - ※1 BigQuery は、公式リファレンスの集計関数一覧に
COUNT・COUNTIF・APPROX_COUNT_DISTINCTがあることまでを確認しました。個々の構文の細部は今回確認・実行できていません。 - ※2 SQL Server と Oracle での
SUM(条件)は未実行です。一般に、条件式をそのまま数値として集計する書き方はこの2つでは使わず、CASE 式で書きます。
迷ったら COUNT(CASE WHEN 条件 THEN 1 END) を選ぶと、データベースを移っても同じ SQL が使えます。
複数列の組み合わせを DISTINCT で数える
「会員とクーポンの組み合わせは何通りあるか」を数えます。MySQL は COUNT(DISTINCT a, b) と書けます。
SELECT COUNT(DISTINCT member_id, coupon) AS d2 FROM orders; -- MySQL 8.0: 4
PostgreSQL と SQLite ではエラーになりました。
-- PostgreSQL 16
ERROR: function count(integer, character varying) does not exist
-- SQLite 3.45
Parse error near line 1: wrong number of arguments to function COUNT()
どのデータベースでも動く書き換え
サブクエリで SELECT DISTINCT してから数えます。
-- NULL を含む組み合わせも1通りとして数える
SELECT COUNT(*) AS pairs
FROM (SELECT DISTINCT member_id, coupon FROM orders) t; -- 7
-- どちらかが NULL の組み合わせは除く(MySQL の COUNT(DISTINCT a, b) と同じ結果)
SELECT COUNT(*) AS pairs_notnull
FROM (SELECT DISTINCT member_id, coupon FROM orders
WHERE member_id IS NOT NULL AND coupon IS NOT NULL) t; -- 4
| 書き方 | 結果 | NULL を含む組 |
|---|---|---|
MySQL COUNT(DISTINCT member_id, coupon) |
4 | 数えない |
PostgreSQL COUNT(DISTINCT (member_id, coupon)) |
7 | 数える |
サブクエリ+SELECT DISTINCT |
7 | 数える |
サブクエリ+IS NOT NULL |
4 | 数えない |
同じ「複数列の DISTINCT」でも、NULL を含む組を数えるかどうかで結果が 4 と 7 に分かれました。 データベースを移すときは、どちらの意味で数えていたかを確かめてから書き換えると安心です。
GROUP BY・HAVING と組み合わせる
グループごとの件数は GROUP BY で、件数による絞り込みは HAVING で書きます。
SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status
ORDER BY cnt DESC, status;
status | cnt
-----------+-----
paid | 5
pending | 2
cancelled | 1
SELECT member_id, COUNT(*) AS cnt, COUNT(coupon) AS with_coupon
FROM orders
GROUP BY member_id
HAVING COUNT(*) >= 2
ORDER BY member_id;
member_id | cnt | with_coupon
-----------+-----+-------------
1 | 3 | 1
2 | 2 | 2
3 | 2 | 1
WHERE は集計の前に行を絞り、HAVING は集計の後にグループを絞ります。WHERE COUNT(*) >= 2 とは書けません。重複データを探すときの定番も、この形です(GROUP BY 列 HAVING COUNT(*) > 1)。
GROUP BY の列に NULL があると、NULL 同士は1つのグループにまとまります。
SELECT coupon, COUNT(*) AS c FROM orders GROUP BY coupon ORDER BY 1;
+--------+---+
| coupon | c |
+--------+---+
| NULL | 4 |
| A10 | 3 |
| B20 | 1 |
+--------+---+
上は MySQL の出力です。PostgreSQL では同じ件数で、NULL のグループが最後に並びました(NULL の並び順の既定がデータベースで違うため)。GROUP BY と HAVING の詳しい使い方は、別記事「SQL GROUP BY HAVING 使い方|集計関数一覧とDB比較・実行結果つき」で扱っています。
LEFT JOIN で 0 件のグループも出す
「会員ごとの注文数」を、注文が0件の会員も含めて出します。数える対象を COUNT(*) ではなく、結合した相手側の列にするのがポイントです。
SELECT m.id, m.name,
COUNT(*) AS cnt_star,
COUNT(o.id) AS cnt_id
FROM members m
LEFT JOIN orders o ON o.member_id = m.id
GROUP BY m.id, m.name
ORDER BY m.id;
id | name | cnt_star | cnt_id
----+------+----------+--------
1 | 青木 | 3 | 3
2 | 井上 | 2 | 2
3 | 上田 | 2 | 2
4 | 遠藤 | 1 | 0
注文のない遠藤さんは、LEFT JOIN によって「注文側の列がすべて NULL の行」が1行だけ作られます。COUNT(*) はその行を数えて 1 になり、COUNT(o.id) は NULL を数えないので 0 になります。
条件は WHERE ではなく ON に書く
「支払い済みの注文数」に絞るとき、条件を WHERE に書くと0件の会員が消えます。
-- 直す前:遠藤さんが結果から消える
SELECT m.id, m.name, COUNT(o.id) AS paid_cnt
FROM members m LEFT JOIN orders o ON o.member_id = m.id
WHERE o.status = 'paid'
GROUP BY m.id, m.name ORDER BY m.id;
id | name | paid_cnt
----+------+----------
1 | 青木 | 2
2 | 井上 | 1
3 | 上田 | 2
-- 直した後:条件を ON に移す
SELECT m.id, m.name, COUNT(o.id) AS paid_cnt
FROM members m LEFT JOIN orders o ON o.member_id = m.id AND o.status = 'paid'
GROUP BY m.id, m.name ORDER BY m.id;
id | name | paid_cnt
----+------+----------
1 | 青木 | 2
2 | 井上 | 1
3 | 上田 | 2
4 | 遠藤 | 0
WHERE は結合の後に働くため、o.status が NULL の行(=注文のない会員)が除かれます。外部結合の相手側の条件は ON に書くと、0件の行が残ります。条件付きカウントで COUNT(CASE WHEN o.status='paid' THEN 1 END) と書いても同じ結果になります。
ウィンドウ関数版の COUNT() OVER
GROUP BY は行をまとめますが、COUNT(*) OVER (...) は行を残したまま、各行に件数を付けます。
SELECT id, member_id, status,
COUNT(*) OVER () AS total,
COUNT(*) OVER (PARTITION BY member_id) AS per_member,
COUNT(*) OVER (ORDER BY id) AS running,
COUNT(coupon) OVER (ORDER BY id) AS running_coupon
FROM orders
ORDER BY id;
id | member_id | status | total | per_member | running | running_coupon
----+-----------+-----------+-------+------------+---------+----------------
1 | 1 | paid | 8 | 3 | 1 | 1
2 | 1 | paid | 8 | 3 | 2 | 1
3 | 1 | cancelled | 8 | 3 | 3 | 1
4 | 2 | paid | 8 | 2 | 4 | 2
5 | 2 | pending | 8 | 2 | 5 | 3
6 | 3 | paid | 8 | 2 | 6 | 3
7 | 3 | paid | 8 | 2 | 7 | 4
8 | | pending | 8 | 1 | 8 | 4
| 書き方 | 意味 | 使いどころ |
|---|---|---|
COUNT(*) OVER () |
全体の件数を各行に付ける | 一覧と総件数を1回のクエリで取る、割合の分母 |
COUNT(*) OVER (PARTITION BY 列) |
グループの件数を各行に付ける | 「2件以上ある会員の注文だけ」を明細つきで出す |
COUNT(*) OVER (ORDER BY 列) |
先頭からの累計件数 | 通し番号、累計 |
COUNT(列) OVER (ORDER BY 列) |
NULL を除いた累計件数 | 値が入った回数の推移 |
3つのデータベースで同じ結果になりました。ウィンドウ関数の全体像は、別記事「SQLウィンドウ関数の使い方|累計・前月比・上位N件を実行結果で解説」にまとめています。
COUNT(*)・COUNT(1)・COUNT(主キー) の速度を100万行で比べる
「COUNT(1) のほうが速い」「主キーを指定すると速い」と言われることがあります。100万行の表を作って、実際に測りました。
-- PostgreSQL の例(member_id は NOT NULL、coupon は 4行に1行だけ値あり)
CREATE TABLE big (id integer PRIMARY KEY, member_id integer NOT NULL,
coupon varchar(10), note text);
INSERT INTO big
SELECT g, g % 50000, CASE WHEN g % 4 = 0 THEN 'C' || (g % 100) END, repeat('x', 50)
FROM generate_series(1,1000000) g;
VACUUM ANALYZE big;
各クエリを続けて2〜3回実行したときの所要時間です(PostgreSQL は \timing、MySQL は SHOW PROFILES、SQLite は .timer on の値)。
| クエリ | PostgreSQL 16 | MySQL 8.0(InnoDB) | SQLite 3.45 |
|---|---|---|---|
COUNT(*) |
23〜26 ms | 約 30 ms | 13〜14 ms |
COUNT(1) |
26〜28 ms | 25〜29 ms | 26 ms |
COUNT(id)(主キー) |
30〜33 ms | 25〜28 ms | 33〜34 ms |
COUNT(coupon)(NULL あり) |
32〜38 ms | 96〜103 ms | 41〜42 ms |
COUNT(DISTINCT member_id) |
180〜362 ms | 約 233 ms | 170 ms |
検証用コンテナでの値なので、絶対値は環境で変わります。読み取れる傾向は次のとおりです。
COUNT(*)・COUNT(1)・COUNT(主キー)の差は数ミリ秒の範囲で、どれを選んでも体感は変わりません。 この測定ではCOUNT(*)が同等か、わずかに速い結果でした。- MySQL の
EXPLAIN FORMAT=TREEは、COUNT(*)とCOUNT(id)のどちらも-> Count rows in bigと表示しました。同じ処理として扱われています。 - PostgreSQL の
EXPLAINは、COUNT(*)とCOUNT(id)で同じ並列シーケンシャルスキャン(Workers Planned: 2)の計画でした。違いは読む列の幅(width=0 と width=4)だけです。 - NULL を含む列の
COUNT(列)は、行ごとに値を確かめる分だけ時間がかかります。 MySQL では主キーだけの走査(Using index)から全件走査(type: ALL)に変わり、約3倍になりました。 COUNT(DISTINCT)は重複を取り除く処理が加わるため、けた違いに時間がかかります。
意味の違いもあります。COUNT(*) と COUNT(1) は常に全行を数えますが、COUNT(列) は NULL を除きます。「行数が知りたい」なら COUNT(*) と書くのが、意図が最も伝わります。
なお、MySQL で member_id に二次インデックスを追加したところ、COUNT(*) の実行計画は idx_big_member を使う形に変わり、この環境では 92〜114 ms になりました。インデックスの構成によって選ばれる計画が変わるので、気になるときは EXPLAIN で確かめます。実行計画の読み方は、別記事「実行計画の見方|EXPLAIN ANALYZEの読み方を実行結果で解説」を参照してください。
大きなテーブルの件数を速く知る(統計値を使う)
画面に「約100万件」と出したいだけなら、正確に数えなくても、データベースが持っている統計値を読む方法があります。
-- PostgreSQL:統計上の推定行数
SELECT reltuples::bigint AS estimate FROM pg_class WHERE oid = 'big'::regclass;
estimate
----------
1000000
Time: 0.112 ms
-- MySQL:InnoDB の推定行数
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 't167' AND TABLE_NAME = 'big';
+------------+
| TABLE_ROWS |
+------------+
| 995030 |
+------------+
| 方法 | 所要時間(今回の測定) | 正確さ |
|---|---|---|
SELECT COUNT(*) FROM big |
23〜30 ms | 正確 |
PostgreSQL pg_class.reltuples |
0.1 ms | 推定。VACUUM / ANALYZE の時点の値 |
MySQL information_schema.TABLES.TABLE_ROWS |
1.5 ms | 推定(実際は 1,000,000 行で、995,030 と表示) |
PostgreSQL は直前に VACUUM ANALYZE を実行したので推定値が実際と一致しましたが、更新が続くテーブルでは次の ANALYZE まで値がずれます。MySQL の InnoDB は標本から推定するため、今回は約0.5%の差がありました。「だいたいの件数」には統計値、「正確な件数」には COUNT(*) と使い分けます。
| 目的 | 書き方 |
|---|---|
| 行があるかどうかだけ知りたい | SELECT EXISTS (SELECT 1 FROM t WHERE ...)(1行見つかった時点で終わる) |
| だいたいの総件数を表示したい | 統計値(上の SQL) |
| 絞り込んだ条件での件数が頻繁に必要 | 条件の列にインデックスを張る/集計結果を別の表に持つ |
| 正確な総件数が必要 | COUNT(*) |
「1件でもあるか」を調べるために COUNT(*) > 0 と書くと、全部を数え終わるまで結果が出ません。存在確認には EXISTS が向いています。
よくあるつまずきと直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| 条件付きカウントが全行の数になる | COUNT(CASE ... ELSE 0 END) や COUNT(条件) |
ELSE を外す、または SUM にする |
| 件数が思ったより少ない | COUNT(列) の列に NULL がある |
行数なら COUNT(*) |
| 0件のグループが 1 になる | LEFT JOIN で COUNT(*) を使っている |
COUNT(相手側の主キー) |
| 0件のグループが出てこない | 相手側の条件を WHERE に書いた | 条件を ON に移す |
| JOIN したら件数が増えた | 1対多の結合で行が増えている | COUNT(DISTINCT 主キー)、または集計してから結合 |
FILTER が MySQL で構文エラー |
MySQL 8.0 は未対応 | COUNT(CASE WHEN ...) |
SUM(条件) が PostgreSQL でエラー |
真偽値は数値と別の型 | COUNT(*) FILTER か ::int |
WHERE COUNT(*) > 1 がエラー |
集計結果の条件は WHERE に書けない | HAVING に書く |
よくある質問
Q. COUNT(*) と COUNT(1) はどちらが速いですか?
A. 100万行で測ったところ、PostgreSQL 16 で 23〜26 ms と 26〜28 ms、MySQL 8.0 で約 30 ms と 25〜29 ms で、差は数ミリ秒の範囲でした。どちらも「全行を数える」という同じ意味です。行数を数える意図が伝わりやすい COUNT(*) をおすすめします。
Q. COUNT(DISTINCT) で NULL は数えられますか?
A. 数えられません。今回の例でも、coupon が NULL の4行は種類に含まれず、結果は 2 でした。NULL も1種類として数えたいときは、COUNT(DISTINCT coupon) + MAX(CASE WHEN coupon IS NULL THEN 1 ELSE 0 END) のように NULL の有無を足すか、COALESCE(coupon, '(なし)') で別の値に置き換えてから数えます。
Q. 1回のクエリで、条件ごとの件数を横に並べられますか?
A. できます。SELECT COUNT(*) AS total, COUNT(CASE WHEN status='paid' THEN 1 END) AS paid, COUNT(CASE WHEN status='pending' THEN 1 END) AS pending FROM orders のように、条件付きカウントを列の数だけ並べます。表を1回読むだけで済むので、条件ごとに別々の SELECT を発行するより効率的です。
Q. COUNT の結果が NULL になることはありますか? A. COUNT 自体は、行がなくても 0 を返します。NULL になるのは SUM・MAX・AVG などです。ただし GROUP BY を付けたクエリで該当する行がないと、結果が0行になり「0」という行は返りません。0も表示したい場合は、LEFT JOIN で基準になる表から結合します。
まとめ
COUNT(*)は行数、COUNT(列)は NULL でない行数、COUNT(DISTINCT 列)は値の種類数- 条件付きカウントは
COUNT(CASE WHEN 条件 THEN 1 END)がどのデータベースでも動く。ELSE 0 を付けると全行になる FILTER句は PostgreSQL・SQLite、SUM(条件)は MySQL・SQLite で動いた- 複数列の DISTINCT は、サブクエリで
SELECT DISTINCTしてから数えると移植しやすい。NULL を含む組の扱いに注意 - 0件のグループは LEFT JOIN と
COUNT(相手側の列)。相手側の条件は ON に書く COUNT(*)・COUNT(1)・COUNT(主キー)の速度差は100万行でも数ミリ秒。意味で選ぶ- だいたいの件数は統計値、存在確認は EXISTS を使うと速い
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL COUNTの使い方|DISTINCT・条件付きカウントをDB別に解説」
株式会社RJCでは、SQLのCOUNTと集計の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。