SQL GROUP BY — ピボット・時系列集計・ROLLUPの応用

応用FILTERピボットDATE_TRUNC時系列ROLLUP / GROUPING構成比 / ABC分析STRING_AGGPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

条件付き集計でピボット — FILTER で行を列に展開しクロス集計表を作る

FILTERCOALESCEピボット行→列
前提知識

基礎編で学んだ FILTER を応用すると、縦持ち(long)のデータを横持ち(wide)のクロス集計表へ展開(ピボット)できます。ステータスごとの集計を別々の列として並べ、1カテゴリ=1行のサマリ表を作ります。

-- status の値ごとに「列」を作る(ピボット)
SUM(amount) FILTER (WHERE status = 'completed') -- 完了の列
SUM(amount) FILTER (WHERE status = 'pending')   -- 保留の列
COALESCE(SUM(...) FILTER (...), 0)            -- 該当なしの NULL を 0 に
FILTER が NULL を返す場面:あるグループに該当する行が1件も無いと、SUM(...) FILTER(...) は 0 ではなく NULL を返します。クロス集計表では空欄を 0 と見せたいことが多いため、COALESCE(集計, 0) で 0 に変換します。CASE 式版 SUM(CASE WHEN status='completed' THEN amount ELSE 0 END) でも同じ表を作れます。
問題

orders から、カテゴリを行・ステータスを列にしたピボット表を作ってください。出力列は category, completed_amt, pending_amt, cancelled_amt, total_amt。該当が無いセルは 0 とし、total_amt の降順で返してください。

使用テーブル
► orders(8行)
order_idcategorystatusamount
1Bookscompleted1200
2Bookscompleted800
3Bookscancelled500
4Toyscompleted3000
5Toyspending1500
6Toyscancelled1000
7Foodcompleted600
8Foodpending400
期待出力
categorycompleted_amtpending_amtcancelled_amttotal_amt
Toys3000150010005500
Books200005002500
Food60040001000
模範解答コード
SELECT
  category,
  COALESCE(SUM(amount) FILTER (WHERE status = 'completed'), 0) AS completed_amt, -- 完了の合計(該当なしは0に)
  COALESCE(SUM(amount) FILTER (WHERE status = 'pending'),   0) AS pending_amt,   -- 保留の合計(該当なしは0に)
  COALESCE(SUM(amount) FILTER (WHERE status = 'cancelled'), 0) AS cancelled_amt, -- 取消の合計(該当なしは0に)
  SUM(amount) AS total_amt                                       -- 全ステータス合計(FILTERなし=グループ全体)
FROM     orders
GROUP BY category                                                  -- 行の軸=カテゴリでグループ化
ORDER BY total_amt DESC;                                          -- 合計の降順で並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders          → 行を読み込む
  2. GROUP BY category    → グループ化
  3. FILTER 付き集計       → 集計関数を評価
  4. SELECT               → 列を評価(COALESCE で整形)
  5. ORDER BY total_amt DESC   → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT category, COALESCE(SUM(amount) FILTER (WHERE status = 'completed'), 0) AS completed_amt, COALESCE(SUM(amount) FILTER (WHERE status = 'pending'), 0) AS pending_amt, COALESCE(SUM(amount) FILTER (WHERE status = 'cancelled'), 0) AS cancelled_amt, SUM(amount) AS total_amt FROM orders GROUP BY category ORDER BY total_amt DESC;
LEGEND
データ取得・読込対象
① FROM orders(8行)
FROM ordersorders テーブルの8行を読み込みます。status には completed / pending / cancelled の3種があり、これを「列」に展開していきます。
1 / 5
order_idcategorystatusamount
1Bookscompleted1200
2Bookscompleted800
3Bookscancelled500
4Toyscompleted3000
5Toyspending1500
6Toyscancelled1000
7Foodcompleted600
8Foodpending400
8行読込(縦持ち)
学習ポイント
FILTER で縦持ちを横持ちにピボット:status の値ごとに SUM(amount) FILTER (WHERE status='...') を「列」として並べると、1カテゴリ=1行のクロス集計表になります。1回のグループ化・1回のスキャンで複数列を同時に生成でき、明細を何度も読み直す必要がありません。
空セルの NULL は COALESCE で 0 に:該当行が0件のグループでは SUM(...) FILTER(...) は 0 ではなく NULL を返します。表の見栄えと後続計算のために COALESCE(集計, 0) で 0 に整えます。COUNT は0、SUM/AVG/MAX は NULL を返す違いを押さえましょう。
FILTER と CASE WHEN は等価:SUM(x) FILTER (WHERE c)SUM(CASE WHEN c THEN x END) と同じ結果です。PostgreSQL では FILTER が読みやすい第一選択、他の DB では CASE 版で移植します。どちらも「条件で列を切り分ける」ピボットの心臓部です。
アンチパターン
NULL を 0 と取り違える:COALESCE を省くと空セルが NULL のまま残り、その列を使った合計や割合が NULL 伝播で丸ごと NULL になります。ピボット表で「0 を意図する空欄」は明示的に 0 へ変換してください。
列の軸まで GROUP BY に入れる:GROUP BY category, status にすると元の縦持ちに逆戻りします。ピボットでは行の軸(category)だけを GROUP BY し、列の軸(status)は FILTER の条件側で展開するのが正解です。
実務コラム:レポートのピボットは FILTER が定番
月別×部門、チャネル別×ステータス、デバイス別×イベント——実務のダッシュボードは「行の軸 × 列の軸」のクロス集計だらけです。FILTER(または CASE WHEN)でのピボットは、専用の crosstab 拡張より読みやすく保守しやすいのが利点で、列を増やしたいときは SELECT に1行足すだけ。逆に「列の値が動的に増える」ケースでは、SQL 側はピボットせず縦持ちのまま返し、表計算や BI ツール側で横に開く方が柔軟です。固定列なら SQL、可変列なら BI——この線引きが実務の勘どころです。
QUESTION 2

DATE_TRUNC で時系列グループ化 — 日付を月に丸めて月次集計する

DATE_TRUNC時系列集計月次バケツ日付の切り捨て
前提知識

時系列データの集計では、日付を月や週などの単位に切り捨ててグループ化します。PostgreSQL の DATE_TRUNC('month', ts) は、タイムスタンプをその月の初日 00:00に丸めるため、同じ月の行が自然に1グループへまとまります。

-- 月初に切り捨てて「月」でグループ化
DATE_TRUNC('month', order_date)   -- 2024-02-15 → 2024-02-01
GROUP BY DATE_TRUNC('month', order_date)
切り捨てが「バケツ」を作る:日付をそのまま GROUP BY すると1日(厳密には1タイムスタンプ)ごとに細かく割れてしまいます。DATE_TRUNC で粒度を月に落とすことで、同月の行が同じ「バケツ」に入ります。表示用に ::date で時刻を落とすと読みやすくなります。
問題

orders から、月別の注文件数と合計金額を求めてください。出力列は month, order_count, total_amount。month は月初日(date 型)で表し、month の昇順で返してください。

使用テーブル
► orders(7行)
order_idorder_dateamount
12024-01-051000
22024-01-201500
32024-02-032000
42024-02-15500
52024-02-281000
62024-03-103000
72024-03-222000
期待出力
monthorder_counttotal_amount
2024-01-0122500
2024-02-0133500
2024-03-0125000
模範解答コード
SELECT
  DATE_TRUNC('month', order_date)::date AS month, -- 月初に切り捨て、::date で時刻を除去して表示を整える
  COUNT(*)    AS order_count,                  -- 月内の注文件数
  SUM(amount) AS total_amount                  -- 月内の合計金額
FROM     orders
GROUP BY DATE_TRUNC('month', order_date)        -- 切り捨てた「月」単位でグループ化(式をそのまま書く)
ORDER BY month;                                  -- 月初日の昇順=時系列順に並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders          → 行を読み込む
  2. DATE_TRUNC           → 値を整形(月初に変換)
  3. GROUP BY (月)         → グループ化
  4. COUNT / SUM          → 集計関数を評価
  5. ORDER BY month       → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT DATE_TRUNC('month', order_date)::date AS month, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY DATE_TRUNC('month', order_date) ORDER BY month;
LEGEND
データ取得・読込対象
① FROM orders(7行)
FROM orders7行を読み込みます。order_date は日単位でバラバラですが、これを月の粒度に丸めて集計するのが目的です。
1 / 5
order_idorder_dateamount
12024-01-051000
22024-01-201500
32024-02-032000
42024-02-15500
52024-02-281000
62024-03-103000
72024-03-222000
7行読込(日単位)
学習ポイント
DATE_TRUNC は時系列の「バケツ化」:DATE_TRUNC('month', ts) は日付を月初に丸め、同じ月の行を1グループに束ねます。'day' / 'week' / 'month' / 'quarter' / 'year' と粒度を変えるだけで、同じデータから日次・月次・年次レポートを自在に切り替えられます。
GROUP BY には式をそのまま書く:集計キーが式(DATE_TRUNC(...))なら、GROUP BY にも同じ式を書きます。PostgreSQL は出力別名や序数(GROUP BY 1)も許しますが、式を明示する方が他 DBへ移植しやすく意図も明確です。
表示整形は ::date / TO_CHAR:DATE_TRUNC の戻り値は時刻付きの timestamp です。::date で時刻を落とす、TO_CHAR(month,'YYYY-MM') で 'YYYY-MM' の文字列にするなど、集計のキーと表示の整形は分けて考えると扱いやすくなります。
アンチパターン
生の日付で GROUP BY する:GROUP BY order_date は1日(タイムスタンプ単位なら1秒)ごとに分割され、月次のつもりが大量の行に膨らみます。集計したい粒度に必ず DATE_TRUNC で丸めてからグループ化します。
EXTRACT(MONTH ...) だけで年をまたぐ:GROUP BY EXTRACT(MONTH FROM order_date) は「月の数字」だけで束ねるため、2024年1月と2025年1月が同じグループに混ざります。年も区別したいなら DATE_TRUNC か (年, 月) の組をキーにします。
実務コラム:欠けた期間をどう埋めるか
時系列集計はダッシュボードの心臓部ですが、「売上が0だった月」は GROUP BY の結果に行として現れません(元データに行が無いため)。推移グラフで月が飛んでしまうのを防ぐには、generate_series('2024-01-01','2024-03-01','1 month') で連続した日付軸を作り、集計結果を LEFT JOIN して COALESCE(..., 0) で穴埋めするのが定石です。「データに現れた行しか出ない」という GROUP BY の性質を、時系列では特に意識してください。
QUESTION 3

ROLLUP で小計と総計 — 1クエリで明細・カテゴリ小計・全体総計を出す

ROLLUPGROUPING小計・総計集計の階層
前提知識

合計行を伴うレポートは GROUP BY ROLLUP が最短です。ROLLUP(a, b) は (a,b) の明細に加え、(a)小計()総計を、1回のクエリでまとめて出力します。

-- 明細 + カテゴリ小計 + 総計を一度に生成
GROUP BY ROLLUP (category, status)
-- 生成される集計単位: (cat,status) / (cat) / ()
GROUPING(status)  -- 1 なら status を畳んだ小計/総計行
小計行の NULL を見分ける:小計・総計行では、畳まれた列が NULL になります。これは「データが無い」NULL ではなく「この次元は集計済み」という意味。本物の NULL と区別するため GROUPING(列)(小計なら1)を使い、ラベル付けや並べ替えに利用します。
問題

orders から、カテゴリ×ステータスの明細に加え、カテゴリ小計と全体総計を1つのクエリで出してください。小計行の status は「(小計)」、総計行は category「【全カテゴリ】」・status「(総計)」と表示し、レポート順(カテゴリ→明細→小計→最後に総計)で返してください。

使用テーブル
► orders(6行)
order_idcategorystatusamount
1Bookscompleted1200
2Bookscancelled800
3Toyscompleted3000
4Toyscompleted1000
5Toyscancelled500
6Foodcompleted600
期待出力
categorystatustotal_amount
Bookscancelled800
Bookscompleted1200
Books(小計)2000
Foodcompleted600
Food(小計)600
Toyscancelled500
Toyscompleted4000
Toys(小計)4500
【全カテゴリ】(総計)7100
模範解答コード
SELECT
  COALESCE(category, '【全カテゴリ】') AS category,   -- 総計行は category が NULL → ラベルに置換
  COALESCE(
    status,
    CASE WHEN GROUPING(category) = 0 THEN '(小計)'  -- category は生きていて status だけ畳まれた=小計
         ELSE '(総計)' END                          -- 両方畳まれた=総計
  ) AS status,
  SUM(amount) AS total_amount                     -- 各集計単位(明細/小計/総計)の合計
FROM     orders
GROUP BY ROLLUP (category, status)                -- (cat,status)/(cat)/() の3階層を一度に生成
ORDER BY GROUPING(category), category,           -- 総計(GROUPING=1)を最後へ/カテゴリ昇順
         GROUPING(status),   status;            -- 小計(GROUPING=1)を各カテゴリの最後へ

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                       → 行を読み込む
  2. GROUP BY ROLLUP(category, status) → グループ化(明細・小計・総計)
  3. SUM(amount)                       → 集計関数を評価
  4. SELECT                            → 列を評価(COALESCE + GROUPING で整形)
  5. ORDER BY GROUPING(category), category, GROUPING(status), status            → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT COALESCE(category, '【全カテゴリ】') AS category, COALESCE(status, CASE WHEN GROUPING(category) = 0 THEN '(小計)' ELSE '(総計)' END) AS status, SUM(amount) AS total_amount FROM orders GROUP BY ROLLUP (category, status) ORDER BY GROUPING(category), category, GROUPING(status), status;
LEGEND
データ取得・読込対象
① FROM orders(6行)
FROM orders6行を読み込みます。category(3種)と status(completed/cancelled)の組み合わせ単位に加え、その小計と総計まで一気に作るのがゴールです。
1 / 5
order_idcategorystatusamount
1Bookscompleted1200
2Bookscancelled800
3Toyscompleted3000
4Toyscompleted1000
5Toyscancelled500
6Foodcompleted600
6行読込
学習ポイント
ROLLUP は階層的な小計+総計を一発生成:ROLLUP(a, b)(a,b)→(a)→() と次元を右から1つずつ畳み、明細・小計・総計をまとめて出力します。UNION ALL で何本も書く必要が無く、テーブルのスキャンも1回で済みます。
畳まれた次元は NULL になる:小計行は status=NULL、総計行は category も status も NULL です。これは欠損ではなく「この次元は集計済み」の印COALESCE で「(小計)/(総計)」のラベルに置き換えると、そのままレポートとして読めます。
GROUPING() で小計/総計を判定・整列:GROUPING(列)=1 ならその列が畳まれた行です。ORDER BY GROUPING(category), category, GROUPING(status), status とすると、明細→小計→総計のレポート順が安定して得られ、本物の NULL データとも確実に区別できます。
アンチパターン
ROLLUP の NULL を欠損と誤解する:小計行の status=NULL を「データ抜け」と読むと、小計を明細と二重に数えてしまいます。畳まれた NULL か実データの NULL かは GROUPING() で必ず判定してください。
ROLLUP を UNION ALL で手作りする:GROUP BY cat,statusGROUP BY cat ∪ 全体、と3本のクエリを継ぎ足すのは冗長で、テーブルを3回走査します。ROLLUP なら1回のスキャンで同じ結果が得られます。
実務コラム:ROLLUP / CUBE / GROUPING SETS の使い分け
小計の出し方には3兄弟があります。ROLLUP は「地域>国>都市」のような階層の小計(左から順に畳む)、CUBE は指定列の全組み合わせの小計、GROUPING SETS欲しい集計単位だけを明示列挙します。請求書や財務レポートの「合計行」は ROLLUP が定番で、Excel ピボットテーブルの小計と同じ発想を SQL 側で完結できます。複数の切り口の小計を1表に同居させたいときは GROUPING SETS が最も無駄がありません。
QUESTION 4

構成比と累積構成比 — GROUP BY とウィンドウ関数で「全体に占める割合」

SUM() OVER ()構成比集計の二段重ねABC分析
前提知識

「各カテゴリは全体の何%か」を出すには、グループ集計の結果に対してさらにウィンドウ関数を重ねます。SUM(SUM(amount)) OVER () は、内側 SUM がグループ合計、外側 SUM が全グループを横断した総計を表す、集計の二段重ねです。

-- 構成比 = グループ合計 / 全体総計
SUM(amount) * 100.0 / SUM(SUM(amount)) OVER ()
-- 累積構成比(合計の大きい順に積み上げ)
SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC)
ウィンドウ関数は GROUP BY の「後」に動く:評価順は GROUP BY → 集計 → ウィンドウ関数。だからウィンドウの中で集計値(SUM(amount))を参照でき、SUM(SUM(...)) という二重集計が成立します。OVER () は全行を1つの窓に、OVER (ORDER BY ...) は累積(ランニング合計)を作ります。
問題

orders から、カテゴリ別の合計金額・全体に占める構成比(%)・累積構成比(%)を求めてください。出力列は category, total_amount, pct_of_total, cumulative_pct。合計の降順(ABC分析の並び)で、割合は小数第1位まで丸めて返してください。

使用テーブル
► orders(6行)
order_idcategoryamount
1Books1000
2Books1000
3Toys3000
4Toys2000
5Food2000
6Food1000
期待出力
categorytotal_amountpct_of_totalcumulative_pct
Toys500050.050.0
Food300030.080.0
Books200020.0100.0
模範解答コード
SELECT
  category,
  SUM(amount) AS total_amount,                          -- カテゴリ別の合計(内側の集計)
  ROUND(
    SUM(amount) * 100.0
    / SUM(SUM(amount)) OVER (), 1                -- 自グループ合計 ÷ 全体総計(窓=全カテゴリ)
  ) AS pct_of_total,                                     -- 構成比(%)
  ROUND(
    SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC) * 100.0  -- 合計の大きい順に積み上げた累積
    / SUM(SUM(amount)) OVER (), 1
  ) AS cumulative_pct                                   -- 累積構成比(ABC分析)
FROM     orders
GROUP BY category                                          -- まずカテゴリで集計
ORDER BY total_amount DESC;                              -- 合計の降順(累積の積み上げ順と一致させる)

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders           → 行を読み込む
  2. GROUP BY category     → グループ化
  3. SUM(amount)           → 集計関数を評価
  4. ウィンドウ関数           → ウィンドウ関数を評価(行数は保持)
  5. ORDER BY total_amount DESC → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT category, SUM(amount) AS total_amount, ROUND(SUM(amount) * 100.0 / SUM(SUM(amount)) OVER (), 1) AS pct_of_total, ROUND(SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC) * 100.0 / SUM(SUM(amount)) OVER (), 1) AS cumulative_pct FROM orders GROUP BY category ORDER BY total_amount DESC;
LEGEND
データ取得・読込対象
① FROM orders(6行)
FROM orders6行を読み込みます。まずカテゴリ別に金額を合計し、その後で「全体に対する割合」を計算します。
1 / 6
order_idcategoryamount
1Books1000
2Books1000
3Toys3000
4Toys2000
5Food2000
6Food1000
6行読込
学習ポイント
SUM(SUM(x)) OVER () は集計の二段重ね:内側 SUM がグループ合計、外側 SUM がウィンドウで全グループを再集計した総計です。これで「自分の合計 ÷ 全体総計」の構成比が、サブクエリも自己結合も無しに1クエリで出せます。
ウィンドウ関数は GROUP BY の後に評価:評価順は集計 → ウィンドウ。だからウィンドウの中で SUM(amount) を参照できます。OVER () は全体を1つの窓に、OVER (ORDER BY ...) は累積(ランニング合計)を作る、と覚えておくと応用が利きます。
累積構成比はABC分析の定番:合計降順で積み上げた cumulative_pct を見れば、「上位何カテゴリで全体の8割を占めるか」が一目で分かります。在庫・売上・顧客の重点管理(パレートの法則)で頻出の表現です。
アンチパターン
整数同士の割り算で 0 になる:SUM(amount) / SUM(SUM(amount)) OVER () を整数のまま割ると、商が1未満のため すべて 0 になります。* 100.0 を掛ける、または ::numeric で実数化してから割ってください。
ウィンドウ関数を WHERE / HAVING に書く:ウィンドウは WHERE・HAVING の段階ではまだ評価されないため、WHERE SUM(...) OVER () > ... は書けません。構成比で絞り込みたいなら、いったんサブクエリ/CTE で包んでから外側の WHERE で絞ります。
実務コラム:OVER () と OVER (PARTITION BY) の使い分け
構成比はKPIレポートの花形です。SUM(...) OVER () を1行足すだけで「全体に対する割合」が付き、これを OVER (PARTITION BY 月) に変えれば「その月の中でのシェア」に早変わりします。同じ二段集計の発想で、月次×カテゴリの中での構成比、地域内シェアなど切り口を自在に変えられます。総計をスカラサブクエリ (SELECT SUM(amount) FROM orders) で別取りしてもよいですが、ウィンドウ版はテーブルを1回しか読まず、読みやすさでも勝ります
QUESTION 5

STRING_AGG / ARRAY_AGG — グループ内の値を1つのリストに集約する

STRING_AGGARRAY_AGGHAVINGリスト集約
前提知識

グループ内の値を1つのリストにまとめたいときは STRING_AGG(文字列連結)や ARRAY_AGG(配列化)を使います。集計関数の内側に ORDER BYDISTINCT を書けるのが特徴です。

-- グループ内の値をカンマ区切りで連結(重複除去・並べ替え付き)
STRING_AGG(DISTINCT product, ', ' ORDER BY product)
-- 配列にまとめる場合
ARRAY_AGG(DISTINCT product ORDER BY product)
集計関数の中の ORDER BY:STRING_AGG(expr, 区切り ORDER BY ...) の ORDER BY はリスト要素の並び順を決めます(クエリ末尾の ORDER BY とは別物)。DISTINCT を付けると重複要素を1つにまとめます(このとき ORDER BY 式は DISTINCT 対象と一致させる必要があります)。
問題

sales から、カテゴリ別に 取扱商品数(種類)・商品名リスト・合計数量を求めてください。商品名リストは重複を除き名前順、出力列は category, product_count, products, total_qty2種類以上の商品を扱うカテゴリだけを、total_qty の降順で返してください。

使用テーブル
► sales(7行)
idcategoryproductqty
1BooksSQL Guide3
2BooksPython1
3BooksSQL Guide2
4ToysBlocks5
5ToysPuzzle2
6ToysBlocks1
7FoodCoffee4
期待出力
categoryproduct_countproductstotal_qty
Toys2Blocks, Puzzle8
Books2Python, SQL Guide6
模範解答コード
SELECT
  category,
  COUNT(DISTINCT product)                              AS product_count, -- 重複を除いた商品の種類数
  STRING_AGG(DISTINCT product, ', ' ORDER BY product)  AS products,      -- 商品名を名前順・重複なしで連結
  SUM(qty)                                             AS total_qty      -- 数量の合計
FROM     sales
GROUP BY category                                                       -- カテゴリ単位でグループ化
HAVING   COUNT(DISTINCT product) >= 2                            -- 商品が2種類以上のグループだけ残す(集計後フィルタ)
ORDER BY total_qty DESC;                                                 -- 合計数量の降順で並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM sales         → 行を読み込む
  2. GROUP BY category  → グループ化
  3. 集計               → 集計関数を評価
  4. HAVING             → グループを絞り込む
  5. ORDER BY total_qty DESC → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT category, COUNT(DISTINCT product) AS product_count, STRING_AGG(DISTINCT product, ', ' ORDER BY product) AS products, SUM(qty) AS total_qty FROM sales GROUP BY category HAVING COUNT(DISTINCT product) >= 2 ORDER BY total_qty DESC;
LEGEND
データ取得・読込対象
① FROM sales(7行)
FROM sales7行を読み込みます。product には重複があり(Books の SQL Guide が2回、Toys の Blocks が2回)、これを種類としてまとめます。
1 / 5
idcategoryproductqty
1BooksSQL Guide3
2BooksPython1
3BooksSQL Guide2
4ToysBlocks5
5ToysPuzzle2
6ToysBlocks1
7FoodCoffee4
7行読込
学習ポイント
STRING_AGG / ARRAY_AGG は「行を1つの値に畳む」集計:数値を SUM するのと同じ感覚で、文字列を連結(STRING_AGG)したり配列化(ARRAY_AGG)できます。「グループ内の全要素を一覧化する」定番の集計関数です。
集計関数の内側に ORDER BY と DISTINCT:STRING_AGG(DISTINCT x, ',' ORDER BY x) の ORDER BY はリスト内の並び順、DISTINCT は重複要素の除去を担います。クエリ末尾の ORDER BY(行の並び順)とは役割が異なる点に注意してください。
HAVING で「集計値の条件」を絞る:HAVING COUNT(DISTINCT product) >= 2 は集計後の判定なので HAVING に書きます。WHERE の段階では COUNT(DISTINCT ...) はまだ計算されておらず参照できません。基礎編の WHERE/HAVING の使い分けが、リスト集約でもそのまま効きます
アンチパターン
DISTINCT を忘れて重複が並ぶ:DISTINCT 無しだと "SQL Guide, Python, SQL Guide" のように重複がそのまま連結されます。一覧の重複除去は、クエリ末尾ではなく集計関数の内側の DISTINCT で行います。
並び順を末尾の ORDER BY で制御しようとする:クエリ末尾の ORDER BY は「行の順序」を変えるだけで、STRING_AGG が作るリスト内部の並びは変わりません。要素順は必ず集計関数の内側の ORDER BY で指定します。
実務コラム:まとめ列の生成と肥大化対策
STRING_AGG はレポートの「まとめ列」生成に多用されます。注文ごとの商品名一覧、ユーザーごとのタグ、エラーコードの集約、CSV 出力やメール本文の箇条書きなど用途は様々です。一方で、巨大なグループでは連結文字列が肥大化し、表示崩れや性能劣化を招きます。実務では STRING_AGG(... ORDER BY ...) に件数上限の発想(上位 N 件だけ連結する、LEFT(string, n) || '...' で切り詰める)を併用し、全件が必要なら配列を返す ARRAY_AGG+アプリ側整形に切り替えるなど、出力サイズを意識した設計が安全です。