GROUP BYとは、同じ値を持つ行を1つのグループにまとめ、グループごとに件数・合計・平均などを集計するための句です。HAVINGは、集計した結果に条件をかけてグループを絞り込む句です。 「会員ごとの注文合計」「合計が一定額以上の会員だけ」といった集計は、この2つで書けます。

この記事では、集計関数の一覧表をMySQL・PostgreSQL・Oracle・SQL Serverの対応つきで整理し、よく使う集計をコピペできるSQLで紹介します。PostgreSQL 16.13とMySQL 8.0.46で実際に実行し、エラー文を含めて出力をそのまま掲載しています(OracleとSQL Serverは公式ドキュメント準拠)。

GROUP BY・HAVINGと集計関数の早見表

やりたいこと MySQL 8.0(実行確認済み) PostgreSQL 16(実行確認済み) Oracle(ドキュメント準拠) SQL Server(ドキュメント準拠) NULLの扱い
行数 COUNT(*) 同左 同左 同左 NULLの行も数える
値がある行の数 COUNT(列) 同左 同左 同左 数えない
種類の数 COUNT(DISTINCT 列) 同左 同左 同左 数えない
合計 SUM(列) 同左 同左 同左 無視。対象0行ならNULL
平均 AVG(列) 同左 同左 同左 分母に入らない
最小・最大 MIN / MAX 同左 同左 同左 無視
文字列を連結 GROUP_CONCAT(列 ORDER BY … SEPARATOR ',') STRING_AGG(列, ',' ORDER BY …) LISTAGG(列, ',') WITHIN GROUP (ORDER BY …) STRING_AGG(列, ',') WITHIN GROUP (ORDER BY …) 飛ばす
配列・JSON JSON_ARRAYAGG(列) ARRAY_AGG(列) / JSON_AGG(列) JSON_ARRAYAGG(列) JSON_ARRAYAGG(列)(2025以降) MySQL・PGでは要素に残る
すべて真/1つでも真 MIN(列) / MAX(列)(0/1) BOOL_AND / BOOL_OR MIN / MAXで代用 MIN / MAXで代用 無視
ビット演算 BIT_AND / BIT_OR / BIT_XOR 同左 BIT_AND_AGG など(21c以降) 集計版なし 無視
標準偏差・分散 STDDEV_POP / STDDEV_SAMP / VAR_POP 同左 同左 STDEVP / STDEV / VARP 無視
中央値 なし(ウィンドウ関数で計算) PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY 列) MEDIAN(列) PERCENTILE_CONT(ウィンドウ関数) 無視
最頻値 なし MODE() WITHIN GROUP (ORDER BY 列) STATS_MODE(列) なし 無視
どれか1つの値 ANY_VALUE(列) ANY_VALUE(列)(16以降) ANY_VALUE(列) なし ―

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL GROUP BY HAVING 使い方|集計関数一覧とDB比較・実行結果つき」

サンプルデータ(コピペ用)

NULLを含む行と、注文0件の会員(江藤)を入れたデータです。両DBでそのまま動きます。

CREATE TABLE members (id INT PRIMARY KEY, name VARCHAR(20));
INSERT INTO members VALUES (1,'青木'),(2,'井上'),(3,'上田'),(4,'江藤');

CREATE TABLE orders (
  id INT PRIMARY KEY, member_id INT, category VARCHAR(10),
  amount INT, shipped BOOLEAN, ordered_on DATE
);
INSERT INTO orders VALUES
 (1,1,'本',1200,TRUE,'2026-08-03'), (2,1,'本',800,TRUE,'2026-08-10'),
 (3,1,'文具',300,FALSE,'2026-09-01'), (4,2,'本',1500,TRUE,'2026-08-21'),
 (5,2,'家電',9800,TRUE,'2026-09-05'), (6,3,'文具',NULL,FALSE,'2026-09-07'),
 (7,3,NULL,500,TRUE,'2026-09-12'), (8,3,NULL,700,FALSE,'2026-09-15');

実行環境は PostgreSQL 16.13とMySQL 8.0.46(sql_mode既定値) です。MySQL 8.0は2026年4月にサポートを終えていますが、この記事の構文は長期サポート版の8.4・9.7でも同じです。

GROUP BYの基本とNULLのグループ

GROUP BY 列と書くと、その列の値が同じ行が1行にまとまり、集計関数はグループごとに計算されます。

SELECT category, COUNT(*) AS cnt, SUM(amount) AS total
FROM orders GROUP BY category ORDER BY category;

PostgreSQL 16(MySQL 8.0は同じ4行で、NULLの行が先頭):

 category | cnt | total 
----------+-----+-------
 家電     |   1 |  9800
 文具     |   2 |   300
 本       |   3 |  3500
          |   2 |  1200
  • NULLは「NULLのグループ」として1つにまとまります。 WHEREの=ではNULL同士は一致しませんが、GROUP BYでは同じグループです。
  • NULLグループの並び順はDBで違います(昇順でPostgreSQLは最後、MySQLは最初)。
  • 「未設定」と表示したいときはGROUP BY COALESCE(category, '未設定')でまとめます。両DBで「未設定 | 2」の行になりました。

集計関数の実行結果とNULLの扱い

COUNT(*)・COUNT(列)・COUNT(DISTINCT)の違い

SELECT COUNT(*) AS cnt_all, COUNT(amount) AS cnt_amount,
       COUNT(category) AS cnt_cat, COUNT(DISTINCT category) AS cnt_cat_distinct
FROM orders;

実行結果(両DBで同じ値):

 cnt_all | cnt_amount | cnt_cat | cnt_cat_distinct 
---------+------------+---------+------------------
       8 |          7 |       6 |                3

COUNT(*)は行数、COUNT(列)はNULLでない行数、COUNT(DISTINCT 列)はNULLを除いた種類数です。

SUM・AVG・MIN・MAXと、0行のとき

全体でSUM(amount)は14800、MINは300、MAXは9800、AVGはPostgreSQLで2114.2857142857142857、MySQLで2114.2857でした。AVGはNULLの行を分母に入れません(14800 ÷ 7)。NULLを0として扱うならAVG(COALESCE(amount, 0))です(「文具」で試すとAVG(amount)は300、AVG(COALESCE(amount, 0))は150)。

対象が0行のとき、SUM・AVGはNULL、COUNTは0を返します。 WHERE amount > 100000で試すと、両DBともSUMとAVGがNULL、COUNT(*)が0でした。数値として扱うならCOALESCE(SUM(amount), 0)としておきます。

文字列の連結:STRING_AGGとGROUP_CONCAT

-- PostgreSQL
SELECT member_id, STRING_AGG(category, ',' ORDER BY id) AS cats,
       BOOL_AND(shipped) AS all_shipped, BOOL_OR(shipped) AS any_shipped
FROM orders GROUP BY member_id ORDER BY member_id;

-- MySQL(BOOLEANは0/1なのでMIN/MAXで代用)
SELECT member_id, GROUP_CONCAT(category ORDER BY id SEPARATOR ',') AS cats,
       MIN(shipped) AS all_shipped, MAX(shipped) AS any_shipped
FROM orders GROUP BY member_id ORDER BY member_id;

PostgreSQL 16(MySQLはf/tが0/1になるだけで同じ内容):

 member_id |    cats    | all_shipped | any_shipped 
-----------+------------+-------------+-------------
         1 | 本,本,文具 | f           | t
         2 | 本,家電    | t           | t
         3 | 文具       | f           | t

会員3のカテゴリは「文具・NULL・NULL」ですが、連結ではNULLが飛ばされます。一方、PostgreSQLのARRAY_AGG(amount)は{NULL,500,700}、MySQLのJSON_ARRAYAGG(amount)は[null, 500, 700]となり、配列・JSON系ではNULLも要素に残りました。

GROUP_CONCATには長さの上限(group_concat_max_len、既定1024バイト)があります。 上限を10にして試すと、MySQL 8.0で結果が本,本,で切れ、Warning 1260: Row 3 was cut by GROUP_CONCAT()が出ました。長い一覧はセッションで上限を上げてから連結します。

統計系(標準偏差・中央値・最頻値)

-- PostgreSQL
SELECT ROUND(STDDEV_POP(amount), 1) AS sd_pop,
       ROUND(STDDEV_SAMP(amount), 1) AS sd_samp,
       PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median,
       MODE() WITHIN GROUP (ORDER BY category) AS mode_cat
FROM orders;
 sd_pop | sd_samp | median | mode_cat 
--------+---------+--------+----------
 3160.2 |  3413.4 |    800 | 本

MySQL 8.0でもSTDDEV_POP・STDDEV_SAMPは同じ値でした。中央値・最頻値の集計関数はMySQLにないため、MySQLではウィンドウ関数で順位をつけて計算します。

WHERE・GROUP BY・HAVINGの評価順

SQLは「FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY」の順に評価されます(詳しくは「SQLの実行順序」の記事)。

flowchart LR
  A[FROM] --> B[WHERE<br/>行を絞る] --> C[GROUP BY<br/>まとめる] --> D[HAVING<br/>グループを絞る] --> E[SELECT<br/>出力] --> F[ORDER BY]

WHEREの時点ではまだグループがないため集計関数は使えず、HAVINGはGROUP BYの後なので使えます。 ORDER BYはSELECTの後なので別名が使えます。

HAVINGとWHEREの使い分け

直す前:WHEREに集計関数を書くとエラー

SELECT member_id, SUM(amount) AS total
FROM orders WHERE SUM(amount) >= 2000 GROUP BY member_id;

実行結果(上がPostgreSQL 16、下がMySQL 8.0):

ERROR:  aggregate functions are not allowed in WHERE
ERROR 1111 (HY000) at line 8: Invalid use of group function

直した後:集計値の条件はHAVINGへ

SELECT member_id, SUM(amount) AS total
FROM orders GROUP BY member_id
HAVING SUM(amount) >= 2000
ORDER BY member_id;

実行結果(両DBで同じ):

 member_id | total 
-----------+-------
         1 |  2300
         2 | 11300

HAVINGでSELECTの別名は使える?(DBで違う)

HAVING total >= 2000と別名で書くと、MySQL 8.0は成功し、PostgreSQL 16はエラーになりました。

ERROR:  column "total" does not exist
LINE 1: ...t) AS total FROM orders GROUP BY member_id HAVING total >= 2...

どのDBでも動かすなら、HAVINGにはSUM(amount)のように式をそのまま書きます。

使い分けの判断

条件の中身 書く場所 例
1行ごとの列の値 WHERE WHERE shipped、WHERE ordered_on >= '2026-09-01'
集計した値 HAVING HAVING COUNT(*) >= 2、HAVING SUM(amount) >= 2000
両方 WHEREで絞ってからHAVING 下の例

1行ごとの列の値(shipped、日付など)はWHERE、集計した値(件数・合計)はHAVINGに書きます。行の条件をWHEREに書くと、集計する行が先に減るため効率的です。「発送済みの注文が2件以上ある会員」ならWHERE shipped GROUP BY member_id HAVING COUNT(*) >= 2で、会員1・2(各2件)が返りました。

GROUP BYにない列をSELECTするとエラーになる

エラーの再現

1行にまとめた後、グループ内で値が複数ありうる列は、どれを出すか決められません。 そのためSELECTに書けるのは「GROUP BYの列」「集計関数」「定数」です。

SELECT member_id, category, SUM(amount) FROM orders GROUP BY member_id;

PostgreSQL 16:

ERROR:  column "orders.category" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT member_id, category, SUM(amount) FROM orders GROUP BY...
                          ^

MySQL 8.0(sql_mode既定値にONLY_FULL_GROUP_BYを含む):

ERROR 1055 (42000) at line 5: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 't38.orders.category' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

直し方は3通り

目的 直し方
カテゴリ別にも分けたい GROUP BY member_id, category
代表値が1つあればよい MIN(category) / MAX(category) / ANY_VALUE(category)
一覧で見たい STRING_AGG / GROUP_CONCAT

主キーでGROUP BYしていれば、同じテーブルの他の列は書けます。 SELECT m.id, m.name, SUM(o.amount) FROM members m JOIN orders o ON o.member_id = m.id GROUP BY m.idは、両DBでエラーにならず会員名つきで集計できました。

MySQLでONLY_FULL_GROUP_BYを外すと上のSQLも通り、会員1(本・文具)のカテゴリは「本」になりました。どの値が返るかは保証されないため、sql_modeは既定値のまま、SQLを上の3通りで直すと結果が安定します。

小計・総計:ROLLUP・CUBE・GROUPING SETS

ROLLUP

ROLLUPは、右の列から順にまとめた小計と総計の行を追加します。

-- PostgreSQL(MySQLは GROUP BY member_id, category WITH ROLLUP と書く)
SELECT member_id, category, SUM(amount) AS total, GROUPING(member_id, category) AS g
FROM orders WHERE member_id IN (1, 2)
GROUP BY ROLLUP (member_id, category)
ORDER BY member_id, category;

PostgreSQL 16(MySQL 8.0も同じ行。空欄はNULL):

 member_id | category | total | g 
-----------+----------+-------+---
         1 | 文具     |   300 | 0
         1 | 本       |  2000 | 0
         1 |          |  2300 | 1
         2 | 家電     |  9800 | 0
         2 | 本       |  1500 | 0
         2 |          | 11300 | 1
           |          | 13600 | 3

GROUPING()はまとめられた列なら1を返し、2列を渡すとビットで返ります(1=会員の小計、3=総計)。元データのNULLと小計のNULLを区別でき、MySQLならIF(GROUPING(member_id), '合計', member_id)、PostgreSQLならCASE WHEN GROUPING(member_id) = 1 THEN '合計' ELSE member_id::text ENDで、総計行に「合計」と表示できました。

CUBEとGROUPING SETS

GROUPING SETSは欲しい集計の組み合わせを指定し、CUBEはすべての組み合わせを作ります。

-- PostgreSQL:会員別・カテゴリ別・全体を1回で
SELECT member_id, category, COUNT(*)
FROM orders
GROUP BY GROUPING SETS ((member_id), (category), ())
ORDER BY member_id NULLS LAST, category NULLS LAST;

PostgreSQL 16では、会員別3行(3・2・3件)、カテゴリ別4行(家電1・文具2・本3・NULL 2件)、全体1行(8件)の計8行が1回で返りました。MySQL 8.0ではCUBEもGROUPING SETSもERROR 1064 (42000) … syntaxになりました。

機能 MySQL 8.0 PostgreSQL 16 Oracle / SQL Server(ドキュメント準拠)
ROLLUP WITH ROLLUP ROLLUP (…) ROLLUP (…)
CUBE なし(UNION ALLで代用) CUBE (…) CUBE (…)
GROUPING SETS なし(UNION ALLで代用) GROUPING SETS (…) GROUPING SETS (…)
GROUPING() あり(8.0.1以降) あり あり

条件つき集計:FILTER句とCASE式

「全件数」と「発送済み件数」を1回で並べるには、PostgreSQLはFILTER、MySQLはCASE式を使います。

-- PostgreSQL
SELECT member_id, COUNT(*) AS cnt,
       COUNT(*) FILTER (WHERE shipped) AS shipped_cnt,
       SUM(amount) FILTER (WHERE category = '本') AS book_total
FROM orders GROUP BY member_id ORDER BY member_id;

-- MySQL
SELECT member_id, COUNT(*) AS cnt,
       SUM(shipped) AS shipped_cnt,
       SUM(CASE WHEN category = '本' THEN amount END) AS book_total
FROM orders GROUP BY member_id ORDER BY member_id;

PostgreSQL 16(MySQL 8.0も同じ値。空欄はNULL):

 member_id | cnt | shipped_cnt | book_total 
-----------+-----+-------------+------------
         1 |   3 |           2 |       2000
         2 |   2 |           2 |       1500
         3 |   3 |           1 |           

CASE式はどのDBでも動く書き方です。

0件のグループを出す:LEFT JOINとCOUNT

直す前:COUNT(*)だと0件が1になる

SELECT m.id, m.name,
       COUNT(*) AS wrong_cnt, COUNT(o.id) AS right_cnt,
       COALESCE(SUM(o.amount), 0) AS total
FROM members m LEFT JOIN orders o ON o.member_id = m.id
GROUP BY m.id, m.name ORDER BY m.id;

PostgreSQL 16:

 id | name | wrong_cnt | right_cnt | total 
----+------+-----------+-----------+-------
  1 | 青木 |         3 |         3 |  2300
  2 | 井上 |         2 |         2 | 11300
  3 | 上田 |         3 |         3 |  1200
  4 | 江藤 |         1 |         0 |     0

注文のない江藤さんはLEFT JOINで「注文側がすべてNULLの1行」になります。COUNT(*)はその1行を数えて1、COUNT(o.id)はNULLを数えないので0です。

直した後:子テーブルの条件はONに書く

発送済みの注文だけを数えようとして… LEFT JOIN orders o ON o.member_id = m.id WHERE o.shippedと書くと、結果は会員1〜3の3行だけになり、江藤さんが消えました。WHEREは結合後の行を絞るため、NULLの行も落ちます。 条件をONに書くと残ります。

SELECT m.id, COUNT(o.id) AS cnt
FROM members m LEFT JOIN orders o ON o.member_id = m.id AND o.shipped
GROUP BY m.id ORDER BY m.id;
 id | cnt 
----+-----
  1 |   2
  2 |   2
  3 |   1
  4 |   0

0件の会員(id 4)が残りました。

グループごとの上位N件

明細を残してグループごとに絞るのはウィンドウ関数の役割です。会員ごとに金額の大きい注文を2件ずつ取り出します。

SELECT member_id, id, amount
FROM (
  SELECT member_id, id, amount,
         ROW_NUMBER() OVER (PARTITION BY member_id ORDER BY amount DESC, id) AS rn
  FROM orders
) t
WHERE rn <= 2
ORDER BY member_id, rn;

MySQL 8.0:

+-----------+----+--------+
| member_id | id | amount |
+-----------+----+--------+
|         1 |  1 |   1200 |
|         1 |  2 |    800 |
|         2 |  5 |   9800 |
|         2 |  4 |   1500 |
|         3 |  8 |    700 |
|         3 |  7 |    500 |
+-----------+----+--------+

PostgreSQLは降順でNULLが先頭に来るため、ORDER BY amount DESC NULLS LAST, idとしたところ同じ結果になりました。ウィンドウ関数の詳しい使い方は「SQLウィンドウ関数」の記事で扱っています。

よくある誤りと直し方

症状 原因 直し方
must appear in the GROUP BY clause / ERROR 1055 集計していない列をSELECT GROUP BYに追加、またはMIN・ANY_VALUE
aggregate functions are not allowed in WHERE / ERROR 1111 WHEREに集計関数 HAVINGへ
0件のグループが出ない/0件が1になる WHEREで子を絞った/COUNT(*) 条件をONへ/COUNT(子.id)
件数が倍になる JOINで親の行が増えた 子を先に集計してJOIN
平均が想定より大きい AVGはNULLを分母に入れない AVG(COALESCE(列, 0))
合計がNULL 対象行が0 COALESCE(SUM(列), 0)
連結文字列が途中で切れる group_concat_max_len セッションで上限を上げる
MySQLでCUBEが構文エラー MySQLは未対応 WITH ROLLUPかUNION ALL

よくある質問

Q. GROUP BYとDISTINCTはどう違いますか? A. 重複を除くだけなら結果は同じですが、件数や合計も出すならGROUP BYです。

Q. GROUP BYなしでHAVINGは書けますか? A. 書けます。テーブル全体が1グループになります。SELECT COUNT(*) FROM orders HAVING COUNT(*) > 5は両DBで8を1行返し、> 10にすると0行でした。

Q. COUNT(DISTINCT)で複数列を数えるには? A. MySQLはCOUNT(DISTINCT member_id, category)で5、PostgreSQLはCOUNT(DISTINCT (member_id, category))で6でした。MySQLはNULLを含む組み合わせを数えず、PostgreSQLは数えるためです。NULLを含む列ではWHERE category IS NOT NULLで条件をそろえると確実です。

まとめ

  • GROUP BYは同じ値の行をまとめ、集計関数でグループごとに計算する。NULLも1つのグループになる
  • COUNT(*)は行数、COUNT(列)はNULL以外の数。SUM・AVGはNULLを無視し、対象0行ならNULL
  • 行の条件はWHERE、集計値の条件はHAVING。HAVINGの別名はMySQLだけ使える
  • SELECTに書ける列はGROUP BYの列・集計関数・主キーに従属する列。ONLY_FULL_GROUP_BYは既定値のままにする
  • 小計・総計はROLLUP(MySQLはWITH ROLLUP)、判定はGROUPING()
  • 条件つき集計はFILTER(PG)かCASE式(共通)
  • 0件のグループはLEFT JOIN+COUNT(子.id)、子の条件はONに書く

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL GROUP BY HAVING 使い方|集計関数一覧とDB比較・実行結果つき」

株式会社RJCでは、集計クエリの書き方やDBごとの違いを、コードレビューの中で仲間と確かめ合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?