変更要約: 初版
3.4集約・グループ化・ウィンドウ関数
GROUP BYによるグループ化とHAVINGによるグループ条件の使い分け、集約関数(COUNT/SUM/AVG等)とNULLの扱い、そして行を集約せずに順位や累積を求めるウィンドウ関数(分析関数)を、正しい集計設計の判断とともに学びます。
集計クエリは「正しく書けているように見えて数値がわずかにずれている」という不具合が起きやすい領域です。原因の多くはNULLの扱い(COUNT(*)とCOUNT(列)の違い)とWHEREとHAVINGの適用順序の誤解にあります。またウィンドウ関数は「行を減らさずに集計値を各行に付与できる」という点でGROUP BYと本質的に異なる道具であり、使い分けが問われます。この節ではこれらを実務のレポート作成の視点で扱います。
3.4.1GROUP BYとHAVING
- GROUP BY=指定した列の値ごとに行をグループ化し、グループ単位で集約関数を適用する。HAVING=グループ化した後の集約結果(例:
COUNT(*) > 10)で絞り込む句。WHEREは集約前の個々の行を絞り込む点でHAVINGと役割が異なる。 - 処理の論理的な順序は概念上
FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BYである。HAVINGで使える条件に集約前の個別行の値を直接書くと意図しない結果になるため、集約前に絞り込みたい条件は必ずWHEREに書く(WHEREで先に対象行を減らすほうが集約対象も減り効率的)。
3.4.2集約関数とNULLの扱い
- COUNT(*)=NULLを含む全行数を数える。COUNT(列名)=その列がNULLでない行だけを数える。この違いを取り違えると「レコード数」のつもりが「特定項目の入力済み件数」になってしまう。
- SUM・AVG・MAX・MINはいずれもNULLを除外して計算する(NULLを0として扱うわけではない)。対象行が全てNULLの場合、SUM/AVG/MAX/MINの結果はNULLになる(0にはならない)。この性質を知らないと「平均値がなぜか高く/低く出る」といった誤解の原因になる。
3.4.3ウィンドウ関数(分析関数)
- ウィンドウ関数=
OVER()句を伴い、GROUP BYのように行を集約して減らすことなく、各行に対して同じウィンドウ(区画)内での集約値・順位・累積を付与できる関数。PARTITION BYでグループ相当の区画分け、ORDER BYでウィンドウ内の順序を指定する。 - 代表的な関数:RANK(同順位に同じ順位を与え、次の順位を飛ばす)・DENSE_RANK(同順位でも次の順位を飛ばさない)・ROW_NUMBER(同順位があっても一意の連番を振る)。累積和や移動平均には
SUM(...) OVER (ORDER BY … ROWS BETWEEN …)のようなフレーム句を用いる。 - 「各部門内で売上上位3件の明細行を、他の列の情報を保持したまま取得したい」といった要件はGROUP BYでは実現できない(GROUP BYは行を集約して減らすため明細の列が失われる)。行を保持したまま順位付けしたいときはウィンドウ関数、行を集約して代表値だけ欲しいときはGROUP BYという使い分けが基本判断。
「WHEREは集約前の個別行、HAVINGは集約後のグループを絞り込む」「COUNT(*)はNULL込み全行、COUNT(列)はNULL以外の行数」「SUM/AVG/MAX/MINは対象が全てNULLならNULLを返す(0ではない)」「ウィンドウ関数は行を集約せずに順位・累積を付与」が最頻出です。RANKとDENSE_RANKの順位飛ばしの違いも要注意。
ある販売管理システムの担当者が「今月の平均注文単価を出したい」というレポート要件を受け、SELECT AVG(amount) FROM orders WHERE order_month = '2026-07'と書いたところ、キャンセルされた注文の金額列(amount)がNULLで登録されていることに気づいたとします。AVGはNULLを自動的に除外して計算するため、この書き方ではキャンセル分を除いた「有効な注文だけの平均単価」が算出されます。これは一見自然に見えますが、経営側が本当に知りたいのが「キャンセルも含めた全注文に対する実質的な平均単価(キャンセルが多いほど平均を押し下げるべき指標)」であれば、AVGがNULLを除外する挙動そのものが要件と食い違うことになります。この場合の対処は、キャンセル分の金額をCOALESCE(amount, 0)で0に変換してからAVGを取る、あるいは分母を明示的にSUM(COALESCE(amount,0)) / COUNT(*)のように組み立て直すことです。加えて、担当者は「部門ごとの注文金額ランキングで上位5件の明細(顧客名や注文日を保持したまま)を一覧化したい」という追加要件も受けています。これはGROUP BYで部門ごとに集約すると明細列が失われるため実現できず、RANK() OVER (PARTITION BY department ORDER BY amount DESC)のようなウィンドウ関数で各行に部門内順位を付与し、その順位が5以下の行だけを外側のクエリでWHERE絞り込みするという設計が適切です。このように、「NULLをどう扱うべきか」を業務的な意味に照らして判断し、行を残すか集約するかを要件に応じて選ぶのが集計設計の核心です。
| 関数 | 同順位の扱い |
|---|---|
| RANK | 同順位に同じ順位を与え、次の順位を飛ばす(例:1,1,3) |
| DENSE_RANK | 同順位に同じ順位を与えるが次の順位は飛ばさない(例:1,1,2) |
| ROW_NUMBER | 同順位があっても一意の連番を振る(例:1,2,3) |
ひっかけ: 「対象行が全てNULLならSUMは0を返す」は誤りです——SUM/AVG/MAX/MINは対象行が全てNULLの場合、結果もNULLになる(0にはならない)。また「HAVINGはWHEREの代わりに個別行を絞り込むために使ってよい」も誤り=HAVINGは集約後のグループ条件専用で、個別行の絞込みはWHEREに書くべきです(先にWHEREで絞るほうが効率的)。
3.4.4この節のまとめ
- WHEREは集約前の個別行、HAVINGは集約後のグループを絞り込む。処理順序はWHERE→GROUP BY→HAVING
- COUNT(*)は全行、COUNT(列)はNULL以外の行数。SUM/AVG/MAX/MINは対象が全NULLならNULLを返す
- 行を保持したまま順位や累積を付与したいときはウィンドウ関数、行を集約したいときはGROUP BYを使う
進捗の記録にはログインが必要です。
理解度チェック
(軽い確認用)Q1. キャンセル注文のamount列がNULLで登録されており、経営側は「キャンセルも含めた全注文に対する実質平均単価」を求めたい。単純な`AVG(amount)`ではなぜ要件を満たせないか。
Q2. 部門ごとに売上上位3件の明細行を、顧客名や注文日などの列を保持したまま一覧化したい。この要件を満たす仕組みはどれか。
Q3. `SELECT department, COUNT(*) FROM employees WHERE salary > 500000 GROUP BY department HAVING COUNT(*) > 10`というクエリの絞り込みの説明として正しいものはどれか。

