SQL GROUP BY — グループ化・集計・条件付き集計の基礎

基礎GROUP BY集計関数WHERE / HAVING条件付き集計PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

グループ化の基礎 — GROUP BY と COUNT(*) で行をグループに畳み込む

GROUP BYCOUNT(*)グループ集計行の畳み込み
前提知識

SQL の集計の出発点が GROUP BY によるグループ化です。指定した列の値が等しい行を1つのグループにまとめ、グループごとに集計関数(COUNT・SUM など)を1つの値へ畳み込み(collapse)ます。「明細データ」から「サマリ」を作る最初の一歩です。

-- GROUP BY: category の値が同じ行を1つのグループにまとめる
SELECT   category, COUNT(*) AS order_count
FROM     orders
GROUP BY category;
畳み込みの大原則:GROUP BY を書くと、結果は「1グループ=1行」になります。そのため SELECT に書けるのはグループ化キー(GROUP BY に書いた列)か、集計関数だけです。COUNT(*) はグループ内の行数を数え、NULL を含めて全行をカウントします。
問題

orders テーブルから、カテゴリ別の注文件数を求めてください。出力列は category, order_count。order_count の降順、件数が同じ場合は category の昇順で返してください。

使用テーブル
► orders(6行)
order_idcategoryamount
1Books1200
2Books800
3Toys3000
4Food500
5Toys1500
6Books2000
期待出力
categoryorder_count
Books3
Toys2
Food1
模範解答コード
SELECT
  category,                -- グループ化の基準となる列(SELECT句に出力可能)
  COUNT(*) AS order_count  -- グループ内の行数を数える集計関数(NULLも含めて全行をカウント)
FROM     orders
GROUP BY category          -- 指定した列(category)の値が同じ行を1つのグループにまとめる
ORDER BY order_count DESC, category; -- 件数が多い順(降順)、同数の場合はカテゴリ名の昇順に並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders        → 行を読込
  2. GROUP BY category  → カテゴリでグループ化
  3. COUNT(*)           → 各グループの行数を集計
  4. SELECT             → category, order_count を出力
  5. ORDER BY order_count DESC, category  → 件数の多い順で並べ替え
  */
解説(テーブル変化・ポイント)
SELECT category, COUNT(*) AS order_count FROM orders GROUP BY category ORDER BY order_count DESC, category;
LEGEND
データ取得・読込対象
① FROM orders(6行)
FROM ordersorders テーブルの6行を読み込みます。category 列には Books / Toys / Food の3種類の値があり、まだ行はバラバラの状態です。
1 / 4
order_idcategoryamount
1Books1200
2Books800
3Toys3000
4Food500
5Toys1500
6Books2000
6行読込(グループ化前)
学習ポイント
GROUP BY は「行を畳み込む」操作:グループ化すると1グループが必ず1行になります。明細(6行)がサマリ(3行)に変わるこの畳み込みこそが集計の本質です。出力行数=グループの種類数になる、と覚えておくと結果を予測しやすくなります。
SELECT に書けるのは「キー」か「集計関数」だけ:畳み込み後は1グループに複数の amount 値が同居するため、SELECT category, amount のように非集計列を裸で書くと PostgreSQL ではエラーになります。GROUP BY に書いた列か、SUM()・COUNT() などの集計関数のみが書けます。
COUNT(*) は NULL も含めて全行を数える:COUNT(*) はグループ内の行の総数を返します。後で学ぶ COUNT(列名) が NULL を除外するのとは対照的で、「件数」を素直に数えたいときは COUNT(*) が基本です。
アンチパターン
非集計列を SELECT に書いてしまう:SELECT category, order_id, COUNT(*) ... GROUP BY category はエラーです。order_id はグループ内で値が一意に定まらないため。1グループに1つの値が決まる列(=キーか集計値)だけを選びましょう。
WHERE で件数を絞ろうとする:WHERE COUNT(*) > 1 は構文エラーになります。集計結果でグループを絞るのは HAVING の仕事です(Q4で詳説)。WHERE はグループ化の前、生の行に対してのみ働きます。
実務コラム:明細からサマリへ、レポートの第一歩
売上明細、アクセスログ、注文履歴——実務データの多くは「1行=1イベント」の明細形式です。GROUP BY はこの明細を意味あるサマリに変える最初の道具です。「カテゴリ別」「日別」「ユーザー別」と切り口(グループ化キー)を変えるだけで、同じデータから無数のレポートが生まれます。Q2 以降では、この畳み込みに SUM / AVG などの集計関数や複数列のキーを重ねて、表現力を一段ずつ広げていきます。
QUESTION 2

集計関数 — SUM / AVG / MAX / MIN でグループごとに数値を集計する

SUM / AVGMAX / MINROUND集計関数
前提知識

グループ化の真価は集計関数と組み合わせたときに発揮されます。1つのグループに対し、合計・平均・最大・最小などをそれぞれ1つの値として算出できます。複数の集計関数は1回のグループ化で同時に計算されます。

-- 集計関数はグループごとに1つの値を返す
SUM(amount)              -- 合計
AVG(amount)              -- 平均(NULL は無視される)
MAX(amount), MIN(amount) -- 最大・最小
ROUND(AVG(amount), 0)   -- 小数桁の整形
AVG と NULL の関係:AVG(amount) は NULL の行を分母にも分子にも含めません(無視する)。一方 COUNT(*) は NULL 行も数えるため、「AVG の分母」と「COUNT(*)」がずれることがあります。平均の整形には ROUND(値, 桁数) を使います。
問題

orders テーブルから、カテゴリ別に 件数・合計・平均(整数に丸め)・最大・最小を算出してください。出力列は category, order_count, total_amount, avg_amount, max_amount, min_amount。total_amount の降順で返してください。

使用テーブル
► orders(6行)
order_idcategoryamount
1Books1200
2Books800
3Books2000
4Toys3000
5Toys1500
6Food500
期待出力
categoryorder_counttotal_amountavg_amountmax_amountmin_amount
Toys24500225030001500
Books3400013332000800
Food1500500500500
模範解答コード
SELECT
  category,                             -- グループ化の基準列
  COUNT(*)             AS order_count,  -- グループ内の全行の件数をカウント
  SUM(amount)          AS total_amount, -- グループ内のamount(金額)の合計を算出
  ROUND(AVG(amount), 0) AS avg_amount,  -- 平均値を算出し、小数第1位を四捨五入して整数(0桁)に丸める
  MAX(amount)          AS max_amount,   -- グループ内の最大値を抽出
  MIN(amount)          AS min_amount    -- グループ内の最小値を抽出
FROM     orders
GROUP BY category                       -- カテゴリ単位でグループ化
ORDER BY total_amount DESC;             -- 合計金額の降順で並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                 → 行を読込
  2. GROUP BY category           → カテゴリでグループ化
  3. 各グループで集計                    → COUNT/SUM/AVG/MAX/MIN
  4. ORDER BY total_amount DESC  → 合計の多い順で並べ替え
  */
解説(テーブル変化・ポイント)
SELECT category, COUNT(*) AS order_count, SUM(amount) AS total_amount, ROUND(AVG(amount), 0) AS avg_amount, MAX(amount) AS max_amount, MIN(amount) AS min_amount FROM orders GROUP BY category ORDER BY total_amount DESC;
LEGEND
データ取得・読込対象
① FROM orders(6行)
FROM ordersorders テーブルの6行を読み込みます。今回は amount 列の数値をグループごとに集計するのが目的です。
1 / 4
order_idcategoryamount
1Books1200
2Books800
3Books2000
4Toys3000
5Toys1500
6Food500
6行読込
学習ポイント
複数の集計関数は1パスで同時計算:COUNT・SUM・AVG・MAX・MIN を並べても、テーブルは1回しか走査されません。グループ化のついでに必要な集計値をまとめて取れるため、別々のクエリを何本も書くより圧倒的に効率的です。
AVG は NULL を無視する(COUNT(*) とずれる):AVG(amount) は NULL の行を計算対象から外します。つまり平均の「分母」は COUNT(amount)(NULL 除外)であり、COUNT(*)(全行)とは一致しないことがあります。意図した分母になっているかを意識すると、平均値の解釈ミスを防げます。
ROUND で表示桁を整える:ROUND(AVG(amount), 0) は平均を小数0桁(整数)に丸めます。レポートでは桁を揃えると一気に読みやすくなります。割合計算なら ROUND(..., 1) のように桁数を変えるだけで調整できます。
アンチパターン
整数同士の割り算で平均が切り捨てられる:SUM(amount) / COUNT(*) を整数型のまま計算すると、小数が切り捨てられ平均が狂います。素直に AVG() を使うか、SUM(amount) * 1.0 / COUNT(*) のように小数化してください。
MAX / MIN を文字列の大小と混同する:数値列なら大小は直感どおりですが、文字列に MAX() を使うと辞書順('Z' > 'A')で評価されます。対象列の型を意識しないと「最大の注文額」のつもりが別物になります。
実務コラム:1つのクエリで KPI ダッシュボードを作る
売上ダッシュボードの「件数・売上・平均単価・最高額」は、まさに今回の1クエリで揃います。集計関数を増やすコストはほぼゼロなので、必要そうな指標は最初から並べておくのが実務の定石です。さらに Q5 で学ぶ COUNT(DISTINCT ...)FILTER を足せば「ユニーク顧客数」「完了率」まで一気に取れ、1本のクエリがそのままダッシュボードの裏側になります。
QUESTION 3

複数列での集計 — GROUP BY に2列指定して組み合わせ単位で集計する

GROUP BY 複数列SUM複合グループ組み合わせ単位
前提知識

GROUP BY には複数の列を指定できます。その場合、グループは「列の組み合わせが同じ行」ごとに作られます。GROUP BY category, status なら (Books, completed) と (Books, cancelled) は別グループです。切り口を掛け合わせたクロス集計の基礎になります。

-- (category, status) の組み合わせごとにグループ化
SELECT   category, status, COUNT(*), SUM(amount)
FROM     orders
GROUP BY category, status;
キーの個数 = 組み合わせの数:グループ数は「実際に存在する (category, status) の組み合わせの数」です。理論上の全組み合わせではなく、データに現れた組み合わせだけが行になります。SELECT の非集計列は category と status の両方を GROUP BY に書く必要があります。
問題

orders テーブルから、カテゴリ × ステータスの組み合わせごとに、件数と合計金額を求めてください。出力列は category, status, order_count, total_amount。category, status の昇順で返してください。

使用テーブル
► orders(7行)
order_idcategorystatusamount
1Bookscompleted1200
2Bookscompleted800
3Bookscancelled2000
4Toyscompleted3000
5Toyscancelled1500
6Toyscancelled1000
7Foodcompleted500
期待出力
categorystatusorder_counttotal_amount
Bookscancelled12000
Bookscompleted22000
Foodcompleted1500
Toyscancelled22500
Toyscompleted13000
模範解答コード
SELECT
  category,
  status,
  COUNT(*)    AS order_count,  -- 組み合わせごとの件数
  SUM(amount) AS total_amount  -- 組み合わせごとの合計金額
FROM     orders
GROUP BY category, status      -- カテゴリとステータスの「組み合わせ」単位でグループ化
ORDER BY category, status;     -- 複数列での並べ替え(まずカテゴリ昇順、次にステータス昇順)

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                → 行を読込
  2. GROUP BY category, status  → 組み合わせでグループ化
  3. COUNT(*) / SUM(amount)     → 各組で集計
  4. ORDER BY category, status  → 名前順に整列
  */
解説(テーブル変化・ポイント)
SELECT category, status, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY category, status ORDER BY category, status;
LEGEND
データ取得・読込対象
① FROM orders(7行)
FROM ordersorders テーブルの7行を読み込みます。category(3種)と status(completed/cancelled)の組み合わせで集計します。
1 / 4
order_idcategorystatusamount
1Bookscompleted1200
2Bookscompleted800
3Bookscancelled2000
4Toyscompleted3000
5Toyscancelled1500
6Toyscancelled1000
7Foodcompleted500
7行読込
学習ポイント
複数列キー=「組み合わせ」でグループ化:GROUP BY に列を並べると、その列の値の組み合わせが等しい行が1グループになります。同じ category でも status が違えば別グループ。切り口を掛け合わせた分析(クロス集計)の土台です。
グループ数は「実在する組み合わせ」だけ:category 3種 × status 2種 = 6通りが理論上の最大ですが、データに現れた組み合わせは5通りなので結果は5行です。存在しない組み合わせ(例: Food, cancelled)は行になりません。全組み合わせを 0 埋めで出したい場合はマスタとの JOIN が必要になります。
非集計列はすべて GROUP BY へ:SELECT に category と status を裸で出すなら、両方とも GROUP BY に書く必要があります。片方でも欠けると「1グループに複数の値」が生じてエラーになります。「SELECT の非集計列 ⊆ GROUP BY」と覚えましょう。
アンチパターン
GROUP BY を category だけにする:SELECT category, status ... GROUP BY category はエラー(status がグループ内で一意に定まらない)。出したい粒度(組み合わせ単位)と GROUP BY の列をそろえてください。
列の順序で粒度が変わると誤解する:GROUP BY category, statusGROUP BY status, categoryグループの集合としては同一です(並び順は ORDER BY が決める)。GROUP BY の列順は結果の意味を変えない、と理解しておくと混乱しません。
実務コラム:縦持ち集計からピボット表へ
今回の「category × status」の結果は縦持ち(long format)のクロス集計です。これを「行=category、列=status」のピボット表にしたいときは、Q2で学んだ SUM(CASE WHEN status='completed' THEN amount ELSE 0 END) のような条件付き集計で列に展開できます。複数列 GROUP BY で粒度を作り、条件付き集計で横に開く——この2段構えが実務のクロス集計の王道です(条件付き集計は Q5 で扱います)。
QUESTION 4

WHERE と HAVING — グループ化の前と後でフィルタを使い分ける

WHEREHAVING実行順序集計後フィルタ
前提知識

フィルタには2種類あります。WHERE はグループ化の「前」に生の行を絞り、HAVING はグループ化・集計の「後」にグループを絞ります。集計関数で条件を付けたいときは HAVING の出番です。両者の違いは SQL の実行順序を理解する鍵です。

-- 評価順: FROM → WHERE → GROUP BY → 集計 → HAVING → SELECT → ORDER BY
SELECT   category, SUM(amount)
FROM     orders
WHERE    status = 'completed'   -- ① 行を絞る(集計前)
GROUP BY category
HAVING   SUM(amount) >= 2000    -- ② グループを絞る(集計後)
使い分けの判断基準:生の列値(status など)で絞るなら WHERE。集計結果(SUM・COUNT など)で絞るなら HAVING。WHERE は集計前なので集計関数を書けません。HAVING は集計後なので集計関数を書けます
問題

orders テーブルから、completed の注文だけを対象に、カテゴリ別合計金額が 2000 以上のカテゴリを求めてください。出力列は category, order_count, total_amount。total_amount の降順で返してください。

使用テーブル
► orders(7行)
order_idcategorystatusamount
1Bookscompleted1200
2Bookscompleted800
3Bookscancelled5000
4Toyscompleted3000
5Toyscompleted1500
6Foodcompleted500
7Foodcancelled9000
期待出力
categoryorder_counttotal_amount
Toys24500
Books22000
模範解答コード
SELECT
  category,
  COUNT(*)    AS order_count,
  SUM(amount) AS total_amount
FROM     orders
WHERE    status = 'completed'  -- 【集計前フィルタ】グループ化の「前」に、完了した注文のみに行を絞り込む
GROUP BY category              -- カテゴリ単位でグループ化
HAVING   SUM(amount) >= 2000   -- 【集計後フィルタ】グループ化・集計の「後」に、合計金額が2000以上のグループだけを残す
ORDER BY total_amount DESC;    -- 合計金額の降順で並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                 → 行を読込
  2. WHERE status='completed'    → 完了行に絞る
  3. GROUP BY category           → カテゴリでグループ化
  4. HAVING SUM(amount)          → しきい値以上のグループを残す
  5. ORDER BY total_amount DESC  → 合計の多い順で並べ替え
  */
解説(テーブル変化・ポイント)
SELECT category, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE status = 'completed' GROUP BY category HAVING SUM(amount) >= 2000 ORDER BY total_amount DESC;
LEGEND
データ取得・読込対象
① FROM orders(7行)
FROM orders7行を読み込みます。cancelled の id 3(5000)・id 7(9000) は高額ですが、この後の WHERE で除外される点に注目してください。
1 / 6
order_idcategorystatusamount
1Bookscompleted1200
2Bookscompleted800
3Bookscancelled5000
4Toyscompleted3000
5Toyscompleted1500
6Foodcompleted500
7Foodcancelled9000
7行読込
学習ポイント
WHERE は集計前、HAVING は集計後:実行順序は FROM → WHERE → GROUP BY → 集計 → HAVING → SELECT → ORDER BY。WHERE は生の行を、HAVING は集計済みのグループを絞ります。この順序を頭に入れると、どちらに条件を書くべきか即座に判断できます。
WHERE で先に絞ると結果も性能も正しくなる:cancelled の高額注文(5000・9000)を WHERE で先に落とすことで、合計が不正に膨らむのを防げます。さらに集計対象の行数が減るため処理も軽くなります。「絞れる条件は早く絞る」が鉄則です。
HAVING でしか書けない条件がある:SUM(amount) >= 2000 のような集計結果の条件は HAVING 専用です。WHERE 段階では SUM はまだ計算されていないため参照できません。「集計値で絞る=HAVING」と紐づけて覚えましょう。
アンチパターン
WHERE に集計関数を書く:WHERE SUM(amount) >= 2000 は構文エラーです。集計は WHERE の後の段階で行われるため、WHERE からは集計値が見えません。集計結果の条件は必ず HAVING へ。
行の条件を HAVING に書いて性能を落とす:HAVING status = 'completed'(しかも status は GROUP BY に無い)は誤り・非効率です。生の列値での絞り込みは WHERE に書き、グループ化前に行数を減らすのが正解です。
実務コラム:「早く絞る」がクエリ高速化の第一歩
大規模テーブルでは、WHERE でどれだけ早く行を捨てられるかがクエリ速度を大きく左右します。インデックスが効くのも WHERE 段階の条件です。一方 HAVING はグループ化という重い処理の後に効くため、行数削減の効果は限定的。実務では「行レベルの条件は WHERE、グループレベルの条件は HAVING」と機械的に振り分けるだけで、正確さと速度の両方が手に入ります。Q5 ではこの土台の上に、条件付き集計でさらに表現力を足します。
QUESTION 5

COUNT(DISTINCT) と条件付き集計 — ユニーク数・FILTER・NULLIF を組み合わせる

COUNT(DISTINCT)FILTERNULLIF条件付き集計
前提知識

グループ化の総仕上げとして、COUNT の3つの顔と条件付き集計を学びます。COUNT(*) は全行、COUNT(列) は NULL を除いた行、COUNT(DISTINCT 列) は重複を除いたユニーク数を数えます。FILTER は特定の集計だけに条件を付ける構文です。

COUNT(*)                              -- 全行
COUNT(DISTINCT customer_id)           -- 重複を除いた人数
COUNT(*) FILTER (WHERE status = 'completed')  -- 条件に合う行だけ
... / NULLIF(COUNT(*), 0)             -- ゼロ除算回避
COUNT(*) と COUNT(DISTINCT) の差が示すもの:同じグループで COUNT(*)=3COUNT(DISTINCT customer_id)=2 なら、「3注文を2人が出した(=1人がリピート)」と読めます。FILTER は特定の条件を満たす行だけを数えるため、CASE WHEN を使わず簡潔に条件付き集計が書けます(PostgreSQL の構文)。
問題

orders テーブルから、カテゴリ別に 総注文数・ユニーク顧客数・完了注文数・完了率(%)を求めてください。出力列は category, total_orders, unique_customers, completed_orders, completion_pct。total_orders 降順、同数なら category 昇順で返してください。

使用テーブル
► orders(8行)
order_idcategorycustomer_idstatus
1Books101completed
2Books101completed
3Books102cancelled
4Toys103completed
5Toys103completed
6Toys104cancelled
7Food105completed
8Food105cancelled
期待出力
categorytotal_ordersunique_customerscompleted_orderscompletion_pct
Books32266.7
Toys32266.7
Food21150.0
模範解答コード
SELECT
  category,
  COUNT(*)                                     AS total_orders,     -- 【全行数】グループ内の全注文数をカウント
  COUNT(DISTINCT customer_id)                  AS unique_customers, -- 【ユニーク数】顧客IDの重複を除外し、何人の顧客がいるかをカウント
  COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders, -- 【条件付き集計】完了ステータスの行だけを対象にカウント
  ROUND(
    COUNT(*) FILTER (WHERE status = 'completed') * 100.0            -- 完了数を100.0倍して実数にし、パーセント表記の分子にする
    / NULLIF(COUNT(*), 0), 1)                  AS completion_pct    -- 分母が0の時のゼロ除算エラーをNULLIFで防ぎ、小数を第1位に丸める
FROM     orders
GROUP BY category                                                   -- カテゴリ単位でグループ化
ORDER BY total_orders DESC, category;                               -- 注文数降順、同数の場合はカテゴリ名昇順で並べ替え

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                           → 行を読込
  2. GROUP BY category                     → カテゴリでグループ化
  3. 各グループで集計                              → COUNT/DISTINCT顧客/完了率
  4. ORDER BY total_orders DESC, category  → 件数の多い順で並べ替え
  */
解説(テーブル変化・ポイント)
SELECT category, COUNT(*) AS total_orders, COUNT(DISTINCT customer_id) AS unique_customers, COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders, ROUND( COUNT(*) FILTER (WHERE status = 'completed') * 100.0 / NULLIF(COUNT(*), 0), 1) AS completion_pct FROM orders GROUP BY category ORDER BY total_orders DESC, category;
LEGEND
データ取得・読込対象
① FROM orders(8行)
FROM orders8行を読み込みます。customer_id には重複(101が2回、103が2回、105が2回)があり、status には completed / cancelled が混在します。
1 / 4
order_idcategorycustomer_idstatus
1Books101completed
2Books101completed
3Books102cancelled
4Toys103completed
5Toys103completed
6Toys104cancelled
7Food105completed
8Food105cancelled
8行読込
学習ポイント
COUNT の3つの顔を使い分ける:COUNT(*)=全行、COUNT(列)=NULL を除く行、COUNT(DISTINCT 列)=重複も NULL も除いたユニーク数。「注文数」は COUNT(*)、「顧客数」は COUNT(DISTINCT customer_id) のように、数えたい対象で正しく選ぶことが集計の精度を決めます。
FILTER は CASE WHEN より簡潔な条件付き集計:COUNT(*) FILTER (WHERE status='completed') は「完了注文だけ数える」を一行で表現します。同じグループ化の中で、指標ごとに別々の条件をかけられるのが強力で、PostgreSQL では CASE WHEN より読みやすい第一選択です。
NULLIF で割合計算を安全にする:/ NULLIF(COUNT(*), 0) は分母が0のとき NULL を返し、ゼロ除算エラーを防ぎます。「注文ゼロのカテゴリの完了率」は0% ではなく定義不能(NULL)が正しく、ROUND と組み合わせて読みやすい割合に整形します。
アンチパターン
顧客数を COUNT(*) で数える:リピートのある Books で COUNT(*)(=3)を顧客数と誤れば、実際の2人を3人と過大評価します。人数を数えるなら必ず COUNT(DISTINCT customer_id) を使ってください。
NULLIF を省いてゼロ除算を招く:COUNT(*) FILTER(...) * 100.0 / COUNT(*) のように分母を生のままにすると、対象行が0件のグループで division by zero エラーになります。割合計算では分母を必ず NULLIF(分母, 0) で包んでください。
実務コラム:FILTER で作るワンクエリ・ダッシュボード
FILTER 句の真価は「1回のグループ化で複数指標を同時に出す」点にあります。実務では SUM(amount) FILTER (WHERE status='completed')(完了売上)、COUNT(*) FILTER (WHERE amount >= 5000)(高額注文数)などを同じ SELECT に並べ、カテゴリ別 KPI 表を一発で生成します。同じデータを何度もスキャンせず、条件ごとの集計を横に並べられるため、ダッシュボード用の集計クエリが劇的に簡潔になります。CASE WHEN でも同等の表現は可能ですが、PostgreSQL では FILTER の方が意図が明確で保守しやすい第一選択です。