【SQL】COUNT関数完全ガイド|COUNT(*)・COUNT(列)・DISTINCT・条件付きカウント・ウィンドウ関数・NULL挙動まで解説

【SQL】COUNT関数完全ガイド|COUNT(*)・COUNT(列)・DISTINCT・条件付きカウント・ウィンドウ関数・NULL挙動まで解説 SQL

COUNT 関数は SQL で最も頻繁に使う集計関数のひとつです。「テーブルの全レコード数」から「特定条件を満たす件数」「重複を除いた種類数」「グループごとの件数」まで、引数の書き方によって動作が大きく変わります。

特に COUNT(*)COUNT(列名)NULL の扱いの違い は実務でよく混乱するポイントです。この記事では COUNT 関数の全形式の違い・NULL 挙動・GROUP BY / HAVING との組み合わせ・条件付きカウント・ウィンドウ関数形式・主要 RDBMS の差異まで体系的に解説します。

スポンサーリンク

COUNT の 3 種類の書き方と違い

COUNT には引数の書き方が 3 種類あり、それぞれ集計対象が異なります。

3種類の構文比較
-- ① COUNT(*) : NULL を含むすべての行数をカウント
SELECT COUNT(*) FROM orders;          -- 結果: 7

-- ② COUNT(列名) : 指定列が NULL でない行数をカウント
SELECT COUNT(amount) FROM orders;     -- 結果: 6(amountがNULLの行6を除く)

-- ③ COUNT(DISTINCT 列名) : 重複を除いた種類数をカウント
SELECT COUNT(DISTINCT customer_id) FROM orders;  -- 結果: 5(101〜105)
書き方 カウント対象 NULL の扱い 主な用途
COUNT(*) 全行 含む テーブル全体の行数・件数確認
COUNT(列名) 指定列が NULL でない行 除外 値が存在する行の件数
COUNT(DISTINCT 列名) 指定列の重複を除いた行 除外 ユニークな値の種類数
サンプルデータで動作を確認
-- サンプルテーブル: orders(注文テーブル)
-- order_id | customer_id | status    | amount | region
-- ---------+-------------+-----------+--------+---------
--        1 |         101 | shipped   |   5000 | east
--        2 |         102 | pending   |   3000 | west
--        3 |         101 | shipped   |   8000 | east
--        4 |         103 | cancelled |   2000 | east
--        5 |         102 | shipped   |   6000 | west
--        6 |         104 | pending   |   NULL | north
--        7 |         105 | NULL      |   4000 | east

NULL の挙動:COUNT(*) と COUNT(列名) の違い

COUNT 関数で最も重要な知識が NULL の扱いです。間違えると件数がずれて集計ミスにつながります。

NULL の挙動の確認
-- status が NULL の行(行7)が 1 件ある
SELECT
    COUNT(*)          AS count_all,    -- 7 ← NULL行も含む
    COUNT(status)     AS count_status, -- 6 ← statusがNULLの行7を除く
    COUNT(amount)     AS count_amount  -- 6 ← amountがNULLの行6を除く
FROM orders;

-- 結果:
-- count_all | count_status | count_amount
-- ----------+--------------+-------------
--         7 |            6 |            6
COUNT(列名) は NULL を除外する:
COUNT(列名) は指定した列の値が NULL の行を カウントしません。テーブルの実際の件数を数えたいのに COUNT(nullable_column) を使うと、NULL が含まれる行が抜け落ちて正確な件数が取れなくなります。テーブル全体の行数には必ず COUNT(*) を使ってください。
NULL のみをカウントする・NULL 以外をカウントする
-- NULL の行だけを数える(IS NULL)
SELECT COUNT(*) AS null_count
FROM orders
WHERE status IS NULL;   -- 結果: 1

-- NULL でない行だけを数える(IS NOT NULL)
SELECT COUNT(*) AS non_null_count
FROM orders
WHERE status IS NOT NULL;  -- 結果: 6

-- または COUNT(列名) で直接取得(IS NOT NULL と等価)
SELECT COUNT(status) AS non_null_count
FROM orders;               -- 結果: 6

WHERE と組み合わせて条件付きでカウントする

WHERE 句で行を絞り込んでからカウントするのが基本的な条件付きカウントです。

WHERE と COUNT の組み合わせ
-- status が 'shipped' の注文数
SELECT COUNT(*) AS shipped_count
FROM orders
WHERE status = 'shipped';  -- 結果: 3

-- 特定地域かつ金額が3000以上の件数
SELECT COUNT(*) AS filtered_count
FROM orders
WHERE region = 'east'
  AND amount >= 3000;      -- 結果: 2(行1と行3)

-- 複数条件を OR で
SELECT COUNT(*) AS shipped_or_pending
FROM orders
WHERE status IN ('shipped', 'pending');  -- 結果: 5

条件付きカウント:CASE WHEN で複数条件を 1 クエリで集計する

WHERE を使うと 1 つの条件でしか絞り込めません。CASE WHEN を COUNT 内で使うと、1 クエリで複数の条件ごとの件数を同時に取得できます。レポート系 SQL で頻繁に使うテクニックです。

NG:条件ごとに別クエリを実行する(非効率)
-- 非効率: テーブルを3回スキャンしている
SELECT COUNT(*) FROM orders WHERE status = 'shipped';
SELECT COUNT(*) FROM orders WHERE status = 'pending';
SELECT COUNT(*) FROM orders WHERE status = 'cancelled';
OK:CASE WHEN で 1 クエリに集約する
-- 効率的: テーブルを1回スキャンするだけ
SELECT
    COUNT(*)                                          AS total,
    COUNT(CASE WHEN status = 'shipped'   THEN 1 END) AS shipped_count,
    COUNT(CASE WHEN status = 'pending'   THEN 1 END) AS pending_count,
    COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled_count,
    COUNT(CASE WHEN amount >= 5000       THEN 1 END) AS high_value_count
FROM orders;

-- 結果:
-- total | shipped_count | pending_count | cancelled_count | high_value_count
-- ------+---------------+---------------+-----------------+-----------------
--     7 |             3 |             2 |               1 |               2
COUNT(CASE WHEN … THEN 1 END) の仕組み:
CASE WHEN 条件が真のとき 1、偽のとき NULL(END で暗黙的に NULL)を返します。COUNT は NULL を除外するため、条件が真の行だけがカウントされますSUM(CASE WHEN ... THEN 1 ELSE 0 END) でも同じ結果が得られますが、COUNT(CASE WHEN ... THEN 1 END) の方が意図が明確です。

GROUP BY でグループごとにカウントする

GROUP BY と組み合わせることで、グループ(カテゴリ)ごとの件数を一度に集計できます。

GROUP BY + COUNT の基本
-- ステータスごとの件数
SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status
ORDER BY cnt DESC;

-- 結果:
-- status    | cnt
-- ----------+----
-- shipped   |   3
-- pending   |   2
-- cancelled |   1
-- NULL      |   1  ← NULLのステータスも1グループとして集計される
HAVING でカウント結果を絞り込む
-- 2件以上注文した顧客のみ表示
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 2
ORDER BY order_count DESC;

-- 結果:
-- customer_id | order_count
-- ------------+------------
--         101 |           2
--         102 |           2

-- WHERE は集計前の絞り込み、HAVING は集計後の絞り込み
-- 組み合わせ例: shipped 注文が2件以上の顧客
SELECT customer_id, COUNT(*) AS shipped_count
FROM orders
WHERE status = 'shipped'   -- 先にshippedのみに絞る
GROUP BY customer_id
HAVING COUNT(*) >= 2;
WHERE vs HAVING の使い分け:

  • WHERE: GROUP BY のに個別行を絞り込む(集計関数は使えない)
  • HAVING: GROUP BY のに集計結果を絞り込む(COUNT() などを使える)
  • GROUP BY + COUNT 詳細についてはGROUP BYでグループカウントする方法も参照してください。

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

同じ顧客が複数回注文していても「顧客数」は 1 として数えたい、というケースに COUNT(DISTINCT 列名) を使います。

COUNT(DISTINCT) の使い方
-- ユニークな顧客数(重複する customer_id を除く)
SELECT COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;  -- 結果: 5(101, 102, 103, 104, 105)

-- ユニークなステータスの種類数(NULLは除外)
SELECT COUNT(DISTINCT status) AS status_types
FROM orders;  -- 結果: 3(shipped, pending, cancelled。NULLは除外)

-- 地域ごとのユニーク顧客数
SELECT region, COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
GROUP BY region
ORDER BY region;

-- 結果:
-- region | unique_customers
-- -------+-----------------
-- east   |               3
-- north  |               1
-- west   |               2

ウィンドウ関数 COUNT() OVER でグループ件数を行ごとに持つ

通常の GROUP BY では集計するとその他の列が見えなくなりますが、ウィンドウ関数を使うと元の行をそのまま残しながら、グループごとの件数を各行に付与できます。

COUNT() OVER の基本
-- 各注文行に「同じ顧客の総注文数」を付与する
SELECT
    order_id,
    customer_id,
    status,
    amount,
    COUNT(*) OVER (PARTITION BY customer_id) AS customer_order_count
FROM orders
ORDER BY customer_id, order_id;

-- 結果:
-- order_id | customer_id | status    | amount | customer_order_count
-- ---------+-------------+-----------+--------+---------------------
--        1 |         101 | shipped   |   5000 |                    2
--        3 |         101 | shipped   |   8000 |                    2
--        2 |         102 | pending   |   3000 |                    2
--        5 |         102 | shipped   |   6000 |                    2
--        4 |         103 | cancelled |   2000 |                    1
--        6 |         104 | pending   |   NULL |                    1
--        7 |         105 | NULL      |   4000 |                    1
全体件数を各行に付与する
-- テーブル全体の件数を各行に付与(ページネーションのトータル件数などに便利)
SELECT
    order_id,
    customer_id,
    COUNT(*) OVER () AS total_count  -- PARTITION BY なし = 全体
FROM orders;
-- 結果: total_count が全行で 7 になる
ウィンドウ関数でグループ内の割合を計算する
-- 各注文が「同一顧客の注文全体」に占める金額の割合
SELECT
    order_id,
    customer_id,
    amount,
    COUNT(*) OVER (PARTITION BY customer_id)                          AS orders_in_group,
    ROUND(amount * 100.0
          / SUM(amount) OVER (PARTITION BY customer_id), 1)           AS amount_pct
FROM orders
WHERE amount IS NOT NULL;

COUNT(*) vs COUNT(1) のパフォーマンス誤解

COUNT(1) の方が COUNT(*) より速い」という説がありますが、現代の主要 RDBMS ではほぼ同じです。

書き方 意味 速度 推奨度
COUNT(*) 全行をカウント(NULL含む) 最適化済み ◎ 推奨
COUNT(1) 定数 1 をカウント(= COUNT(*) と同じ) COUNT(*) と同等 ○ 動作は同じ
COUNT(pk列) 主キー列をカウント(NULL除外) インデックス次第 △ 主キーなら COUNT(*) と同じだが混乱しやすい
COUNT(*) を推奨する理由:
SQL 標準で定義されており、MySQL・PostgreSQL・Oracle・SQL Server のいずれのオプティマイザもCOUNT(*) を認識して最適なインデックス(最小幅の非クラスタインデックスなど)を自動選択します。COUNT(1) も内部的に COUNT(*) と同様に処理されますが、読み手への意図の明確さという点で COUNT(*) が標準的です。

主要 RDBMS ごとの COUNT の注意点

RDBMS 注意点
MySQL / MariaDB COUNT(DISTINCT col1, col2) の複数列指定が可能(他RDBMSは不可)
PostgreSQL COUNT(DISTINCT col) は大テーブルで低速になりやすい。pg_class.reltuples で概算値取得も可能
Oracle COUNT(DISTINCT)GROUP BY の組み合わせで Composite Aggregate Optimization が効く場合がある
SQL Server COUNT(*)int を返す(2億件超は COUNT_BIG(*)bigint を返す)
SQLite 標準的な COUNT(*) / COUNT(列) / COUNT(DISTINCT 列) はすべて対応
SQL Server で大件数をカウントする
-- SQL Server: 20億を超えるレコード数は COUNT(*) だと int オーバーフロー
-- → COUNT_BIG(*) で bigint を返す
SELECT COUNT_BIG(*) AS total_rows
FROM very_large_table;
PostgreSQL の概算カウント(大テーブル向け)
-- PostgreSQL: 正確な件数より「だいたいの件数」でよい場合の高速代替
SELECT reltuples::bigint AS approx_count
FROM pg_class
WHERE relname = 'orders';  -- テーブル名を指定
-- VACUUM/ANALYZE 後の統計情報をもとにした概算値
-- フルスキャン不要で非常に高速

実務でよく使うパターン集

データ品質チェック:NULL の件数を調べる
-- 各列の NULL 件数と NULL 率を一度に確認する
SELECT
    COUNT(*)                                    AS total_rows,
    COUNT(*) - COUNT(customer_id)               AS customer_id_nulls,
    COUNT(*) - COUNT(status)                    AS status_nulls,
    COUNT(*) - COUNT(amount)                    AS amount_nulls,
    ROUND((COUNT(*) - COUNT(amount)) * 100.0
          / COUNT(*), 1)                        AS amount_null_pct
FROM orders;
サブクエリで件数を比較する
-- 全顧客数と注文済み顧客数を比較して「注文したことがある顧客の割合」を求める
SELECT
    (SELECT COUNT(DISTINCT customer_id) FROM orders)  AS ordered_customers,
    (SELECT COUNT(*)                   FROM customers) AS total_customers,
    ROUND(
        (SELECT COUNT(DISTINCT customer_id) FROM orders) * 100.0
        / (SELECT COUNT(*) FROM customers),
        1
    )                                                  AS conversion_pct;
直近30日の日別件数推移
-- MySQL / PostgreSQL(DATE関数の書き方が異なる)
-- MySQL:
SELECT DATE(created_at) AS order_date, COUNT(*) AS daily_count
FROM orders
WHERE created_at >= CURDATE() - INTERVAL 30 DAY
GROUP BY DATE(created_at)
ORDER BY order_date;

-- PostgreSQL:
SELECT created_at::date AS order_date, COUNT(*) AS daily_count
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY created_at::date
ORDER BY order_date;

よくある質問(FAQ)

QCOUNT(*) と COUNT(1) どちらを使うべきですか?
ACOUNT(*) を使ってください。COUNT(1) は書いても動作は同じですが、SQL 標準は COUNT(*) を全行カウントの標準的な書き方として定めており、すべての主要 RDBMS でオプティマイザが同等に最適化します。「COUNT(1) の方が速い」という説は現代の RDBMS では根拠がありません。コードの可読性と標準準拠の観点からも COUNT(*) を推奨します。
QGROUP BY なしで COUNT を使うと何が返りますか?
AGROUP BY なしの COUNT はテーブル全体(または WHERE 条件を満たす全行)を 1 グループとして集計し、結果として 1 行 を返します。このとき SELECT に集計関数以外の列を指定するとエラーになります(RDBMS によって厳密さは異なりますが、標準SQL では違反)。
QCOUNT が 0 のグループも結果に表示したい。
AGROUP BY だけでは COUNT が 0 のグループは結果に現れません。マスタテーブルや列挙値のリストに対して LEFT JOIN + COUNT を使います。例: SELECT s.status, COUNT(o.order_id) FROM (VALUES ('shipped'),('pending'),('cancelled')) AS s(status) LEFT JOIN orders o ON o.status = s.status GROUP BY s.status のように、すべての値を持つ側から LEFT JOIN することで 0 件グループも表示できます。
QCOUNT が遅い場合のチューニング方法は?
AWHERE の条件列にインデックスを付ける(絞り込みが効く)。②COUNT(DISTINCT) は特に重いので、ユースケース次第ではサブクエリ + COUNT(*) に書き換える。③大テーブルの概算件数が必要なら PostgreSQL の pg_class.reltuples や MySQL の information_schema.TABLES.TABLE_ROWS で統計値を参照する。④集計結果をキャッシュ・マテリアライズドビューに事前集計しておく(バッチ処理との組み合わせ)。
QNULL のグループも COUNT したい場合は?
AGROUP BY を使うと NULL もひとつのグループとして集計されます(SQL標準の動作)。ただし NULL グループに名前を付けたい場合は COALESCE(status, '未設定') で GROUP BY すると「未設定」という文字列でまとめられます。COUNT(status) は NULL を除外しますが、COUNT(*) で GROUP BY すれば NULL グループの件数も取得できます。

まとめ

やりたいこと 書き方
全行数をカウント COUNT(*)
NULL を除いた件数 COUNT(列名)
重複を除いた種類数 COUNT(DISTINCT 列名)
条件に合う件数(単条件) COUNT(*) WHERE 条件
複数条件を 1 クエリで集計 COUNT(CASE WHEN 条件 THEN 1 END)
グループごとの件数 COUNT(*) ... GROUP BY 列名
グループ件数で絞り込む GROUP BY ... HAVING COUNT(*) >= n
行を残しながらグループ件数を付与 COUNT(*) OVER (PARTITION BY 列名)
SQL Server で bigint を返す COUNT_BIG(*)

GROUP BY を使ったグループごとのカウントと HAVING での絞り込みの詳細はGROUP BYでグループカウントする方法を、重複データの集計にはGROUP BY + SUM を使った重複データの集計方法を、平均値の算出にはAVG関数で平均値を求める方法も参照してください。