行を要約する:GROUP BY

集計は複数の行を件数、合計、平均にまとめます。注文ごとの結果ではなく、状態別やカテゴリ別の結果を作れます。

グループごとに1つの数字

GROUP BY が同じ値を持つ行を集め、集計関数がそれぞれの山を1つの数字に潰します。

1

SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
Planche で実行

結果 — 7 行のうち先頭 5 行

statusorders
delivered1351
paid695
shipped641
pending349
refunded327

ブラウザの別タブで Planche が開き、サンプルデータベースを読み込んだ状態でこのクエリが入ります。このページの例はどれも同じタブを使い回します。F9 を押すまで実行はされません。

ステータスが7種類なので7行です。SELECT に書ける列は、グループ化に使った列か、集計した値のどちらかに限られます。これが結果を一意に決めるための規則です。ここで order_number を要求したら、質問自体が意味を失います。配達済みの1,351件のうち、どの注文番号を返せばいいのでしょうか。

COUNT(*) と COUNT(列) は違う

2

SELECT COUNT(*) AS customers,
       COUNT(phone) AS with_phone,
       COUNT(*) - COUNT(phone) AS without_phone
FROM customers;
Planche で実行

結果 — 1 行

customerswith_phonewithout_phone
15001221279

COUNT(*) は行を数えます。COUNT(phone) は phone が NULL でない行を数えます。差は絞り込みのレッスンに出てきた279人で、これは実用的な技でもあります。2つの差を取れば、2本目のクエリなしに「欠けている数」が分かります。

他の集計関数も NULL を飛ばします。いちばん効いてくるのは AVG です。10、20、NULL の平均は10ではなく15です。NULL はゼロとして扱われるのではなく、そもそも数に入りません。ゼロとして扱ってほしいなら COALESCE(列, 0) と書いて、そう指示します。

複数の集計をいっぺんに

3

SELECT cat.name AS category,
       COUNT(*) AS products,
       ROUND(MIN(p.price), 2) AS cheapest,
       ROUND(AVG(p.price), 2) AS average,
       ROUND(MAX(p.price), 2) AS dearest
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
ORDER BY average DESC
LIMIT 20;
Planche で実行

結果 — 20 行のうち先頭 5 行

categoryproductscheapestaveragedearest
Wearables11417.31672.292433.73
Programming106.891585.672420.02
Tablets17314.761529.452412.93
Audio11593.81482.892334.28
Kitchen12670.541443.212293.8

テーブルを1回なめるだけで、カテゴリごとに5つの問いに答えています。同じことをアプリ側でやるなら、400行の商品データを全部転送してループを回すことになります。データベースは行がある場所で5つの数字を計算し、20行だけを返します。

結合をまたいで集計する

集計と結合は組み合わせられます。まず結合し、結合後の行をグループ化します。

4

SELECT cat.name AS category,
       COUNT(DISTINCT o.id) AS orders,
       SUM(oi.quantity) AS units,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
WHERE o.status IN ('paid', 'shipped', 'delivered')
GROUP BY cat.id
ORDER BY revenue DESC;
Planche で実行

結果 — 27 行のうち先頭 5 行

categoryordersunitsrevenue
Watches395815933900.92
Board Games395801929534.55
Laptops366768900778.09
Science340709888542.78
Cycling405823858478.88

WHERE は前、HAVING は後

どちらも絞り込みです。違うのは絞り込む時点だけで、違いはそれだけです。

5

SELECT cat.name AS category,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
HAVING SUM(oi.quantity * oi.unit_price) > 1000000
ORDER BY revenue DESC;
Planche で実行

結果 — 8 行のうち先頭 5 行

categoryrevenue
Board Games1377454.99
Tablets1348572.94
Science1347046.09
Watches1325717.58
Cycling1288143.46

WHERE はグループ化の前に動き、どの行が参加するかを決めます。HAVING は後で動き、どのグループが残るかを決めます。だから生の列に対する条件は WHERE に、集計に対する条件は HAVING にしか書けません。WHERE が動いている時点では、集計はまだ存在しないからです。

SELECT のレッスンの句の順序を並べてみれば、規則はひとりでに説明されます。

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY  ->  LIMIT

計算した値でグループ化する

グループ化できるのは列だけではなく、式でもかまいません。タイムスタンプの先頭7文字を切り出せば 2025-01、つまり月になります。

6

SELECT substr(ordered_at, 1, 7) AS month,
       COUNT(*) AS orders,
       ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
  AND ordered_at >= '2025-01-01'
GROUP BY month
ORDER BY month;
Planche で実行

結果 — 12 行のうち先頭 5 行

monthordersrevenue
2025-0152311707.73
2025-0253350775.62
2025-0358351184.96
2025-0452374465.94
2025-0553381541.09

こうして導いた値でまとめるのが、「月次」「週次」レポートのほぼすべての書き方です。GROUP BY month が SELECT の別名を使い回している点にも注目してください。GROUP BY と ORDER BY ではどのエンジンもこれを許します。厳しいのは WHERE と HAVING だけです。

ここまでで書けるもの

レポートが作れるようになりました。まだできないのは、1行を要約と突き合わせることです。「平均より高い商品」「カテゴリごとの上位3件」。これには2つのクエリを同時に走らせる必要があり、次のレッスンで扱います。

自分で書いてみる

  1. 国ごとの顧客数は何人ですか。100人以上の国だけを、多い順に。

    答えを見る

    答えを見る

    SELECT country, COUNT(*) AS customers
    FROM customers
    GROUP BY country
    HAVING COUNT(*) >= 100
    ORDER BY customers DESC;
    Planche で実行

    結果 — 12 行のうち先頭 5 行

    countrycustomers
    KR155
    AU132
    IT130
    ES126
    GB126

    GROUP BY が国ごとに1行を作ります。条件は件数に対するものなので、WHERE ではなく HAVING に入ります。別名ではなく式を書き直しているのは、そうすればどのエンジンでも動くからです。

  2. 評価の高い商品15件。ただしレビューが8件以上ある商品だけを対象にします。

    答えを見る

    答えを見る

    SELECT p.name,
           COUNT(r.id) AS reviews,
           ROUND(AVG(r.rating), 2) AS avg_rating
    FROM products p
    JOIN reviews r ON r.product_id = p.id
    GROUP BY p.id
    HAVING COUNT(r.id) >= 8
    ORDER BY avg_rating DESC
    LIMIT 15;
    Planche で実行

    結果 — 15 行のうち先頭 5 行

    namereviewsavg_rating
    Pulse Yoga Mat84.63
    A Winter Letter (3rd ed.)94.56
    Terra Planter84.5
    Nimbus Cosmos84.5
    Ivory Toaster94.33

    同じグループに集計を2つ。片方がレビュー数を数え、もう片方が評価を平均します。HAVING の条件が、星5のレビュー1件で首位に立つのを防いでいます。