数える、合計する、束ねる
「今月の売上は?」「顧客ごとの注文数は?」。集計関数と GROUP BY の使い方、そして注文のない顧客が集計から消えてしまう問題を解決します。
- 行を数える・合計する・平均するにはどう書くか
- 「〜ごとに」を SQL でどう言うか
- なぜ「顧客ごとの注文数」で 0 件の顧客が消えるのか
表を集計する
50という数字が返ってきましたね。これは、現在の orders(オーダー)全てを集計した値です。式にある COUNT(*) は、対象の行数を数える関数です。このように多数の行から 1 つの集約された値を算出する関数を集計関数(集約関数)と呼びます。
続いて、過去の注文明細から、合計・平均・最小・最大を求めてみましょう。
AS 別名 を使うと、計算結果の列にわかりやすい名前を付けられます。指定しない場合は SUM(quantity) などの式がそのまま列名になります。
WHERE 句は集計関数が計算される前に適用されます。「まず配達済みの注文だけに絞り込み、残った行をカウントした」という順序で処理されています。
COUNT(*) と COUNT(列名) の違い
括弧の中にアスタリスクではなく列名を指定すると、集計の挙動が変化します。実行してどのような結果になるか確かめてみてください。
全行は 50 件、発送日ありは 47 件となったはずです。COUNT(列名) は、その列の値が NULL でない行だけ をカウントします。 同様に、SUM や AVG も列名を指定した場合は NULL の値を計算対象から除外します。 たとえば AVG(注文から発送までの日数) を計算した場合、未発送の注文は平均の分母にも含まれません。 そのため、「未発送を 0 日として含めた平均」と「発送済みの注文だけを対象にした平均」では、結果が同じでもビジネス上の意味は大きく異なります。集計するときは、NULL がどのように扱われているかを意識することが重要です。
GROUP BY: グループに束ねて、グループごとに集計する
「注文ステータスごとの注文件数」を集計してみましょう。実行結果から何が起きているかを確認します。
全 50 行の注文データが、ステータスごとの 4 行にまとまりました。GROUP BY status は、status 列の値が同じ行同士を 1 つのグループ(束)にまとめます。 生成された 4 つのグループそれぞれに対して COUNT(*) が計算され、グループごとに 1 行ずつ結果が出力されます。 SQL で「〜ごとに集計したい」という要件が出てきたら、GROUP BY の出番です。
50 行のデータがどのように 4 行に集約されたかを図で見てみましょう。
図の左側は注文表 orders の 50 行です。status が同じ行同士が矢印で中央の 4 つの束に集められます。各束に対して COUNT(*) が 1 回ずつ計算され、右側の結果表に束ごとの件数が 1 行ずつ出力されます。
続いて月ごとの集計も見てみましょう。
テーブルの列そのものだけでなく、関数で計算した値をもとにグループ化することも可能です。DATE_FORMAT は日時データを指定した書式の文字列に変換する関数です。書式の '%Y-%m' は年-月を表し、同じ年月になった行同士が 1 つの束にまとまります。
次に、月毎の状態別の注文件数をカウントしてみましょう。
GROUP BY 月, status は、データをどの単位でまとめるかを指定しています。今回は「月」と「状態」の 2 つを指定しているため、「月 × 状態」の組み合わせごとに行がまとめられ、そのグループごとの件数が COUNT(*) で計算されます。
GROUP BY を使う際の必須ルール
GROUP BY は、グループごとに結果を 1 行作ります。その 1 行の各セルには、1 つの値しか入りません。 たとえば下の図の「delivered」グループには、顧客 ID が 7、1、1 … と異なる複数の行が混ざっています。この 1 行の customer_id のセルに何を入れるべきか、1 つに定まりません。 そのため SELECT 句に書けるのは、値が 1 つに定まるものだけです。「GROUP BY に指定した列」(束のキーなので全行同じ)か「集計関数」(束全体から 1 つの値を計算する)のどちらかです。定まらない列を書くとエラーになります。実際に試してみましょう。
HAVING: 集計後のグループを絞り込む
WHERE 句はグループ化する前の「元の行」を絞り込みますが、グループ化して集計した「集計結果」をもとに絞り込みたい場合は HAVING 句を使用します。
2 つを組み合わせることもできます。次のクエリは「配達済みの注文が 6 件以上ある顧客」を探しています。WHERE で配達済みに絞ってから顧客ごとに束ね、HAVING で 6 件以上の束だけ残します。
データベース内部の実行順序は以下のようになります。
FROM 対象テーブルを特定 WHERE 行を絞り込む(グループ化の前処理) GROUP BY グループにまとめる HAVING 集計結果でグループを絞り込む SELECT 出力する列と集計値を計算 ORDER BY 結果を並び替える LIMIT 指定件数で切り出す
「元のテーブルにある列での絞り込みは WHERE、集計関数の結果による絞り込みは HAVING」と整理しておくと判断に迷いません。なお、ORDER BY では SELECT 句で定義した別名(件数 など)が利用できます。
注意すべき挙動: 0 件のデータが結果から消える
チケットの要件「顧客ごとの注文数を集計し、注文が 0 件の顧客も 0 件として表示する」を試してみましょう。顧客は全部で 10 人登録されています。何行返ってくるか予想してから実行してください。
結果は 10 行ではなく、8 行になったはずです。顧客 ID が 8 と 10 の顧客が表示されていません。 orders(注文)テーブルには「これまでに注文したことがある人」のレコードしか存在しないため、注文実績が一度もない顧客はグループを作ることができず、結果行として現れないのです。GROUP BY は「テーブル内に実在するデータ」をグループ化する命令であり、「存在しないデータ」を自動的に 0 件として補完して出力することはできません。
注文が 0 件の顧客も含めて漏れなく出力するには、顧客マスタ(customers)を起点として、そこに注文テーブルを結合(JOIN)する必要があります。 この手法は次章で詳しく解説します。
集計関数は多数の行を 1 つの値に集約し、GROUP BY は同じ値を持つ行をグループ化して集計します。実在しないデータはグループ化の対象になりません。
確認
WHERE と HAVING は、どちらも絞り込みなのに何が違うか。
GROUP BY を書いたクエリの SELECT に、束ねていない列を書けないのはなぜか。
顧客ごとの注文数を GROUP BY で出すと、注文が 0 件の顧客が消えるのはなぜか。
次の章へ
GROUP BY を使った集計の基本と、注文が 0 件の顧客が表示されない課題を確認しました。 注文テーブルには顧客 ID の数値しかなく、顧客の氏名やメールアドレスは別のテーブルにあります。 次の章では、複数のテーブルを結合して 1 つの結果にまとめる JOIN の仕組みを学び、0 件の顧客も含めて正しく集計する方法をマスターしましょう。