PostgreSQL「ERROR: column must appear in the GROUP BY clause」の原因と対処
- 作成日 2025.09.12
- 更新日 2025.10.06
- PostgreSQL
- PostgreSQL
概要。集計クエリで「集約していない列」をSELECT/HAVING/ORDER BYに出そうとすると、PostgreSQLは「その列はGROUP BYに現れなければならない」と拒否する。発生条件、最短再現、正しい書き換え方(GROUP BYへ追加 / 集約関数に包む / 代表行の選定)、JOIN時の注意、式と別名の扱い、ウィンドウ関数との併用パターン、デバッグの手順までを網羅。
- 1. エラーの意味と発生条件(まず押さえる)
- 2. 最短再現(NG→OK)
- 3. 基本対策1:非集約列をすべてGROUP BYに入れる
- 4. 基本対策2:集約関数で1値に縮約する(代表値を決める)
- 5. 式・別名・序数の扱い(書き方のバリエーション)
- 6. JOIN時の落とし穴(1対多の爆発と対処)
- 7. 代表行を1件だけ返すパターン(DISTINCT ON / サブクエリ)
- 8. HAVING/ORDER BYでの発生例(NG→OK)
- 9. 条件付き集計(CASE / FILTER)で要件を満たす
- 10. ウィンドウ関数と併用する場合(2段階に分ける)
- 11. デバッグの最短手順(どの列が違反か見抜く)
- 12. 書き換えチートシート(NG→OK)
- 13. 設計の指針(再発させない)
エラーの意味と発生条件(まず押さえる)
・SELECT句・HAVING句・ORDER BY句に現れる「集約していない列」は、すべてGROUP BYに含める必要がある。
・集約関数(SUM/COUNT/MAX/STRING_AGG…)で包まれた式は例外。
・式(date_truncなど)を出す場合は「その式そのもの」をGROUP BYに含めるか、SELECTの別名をGROUP BYに使う。
・JOIN後の列も同じルール。重複(1対多)で列が膨らむなら、代表値の選び方を決める(後述)。
最短再現(NG→OK)
-- サンプル
CREATE TABLE customers(id int PRIMARY KEY, name text, region text);
CREATE TABLE orders(id int PRIMARY KEY, customer_id int, amount numeric, created_at timestamp);
-- NG: 非集約列 name をSELECTしているが、GROUP BYに無い
SELECT c.name, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.id;
-- ERROR: column "c.name" must appear in the GROUP BY clause or be used in an aggregate function
-- OK: name を GROUP BY に追加
SELECT c.id, c.name, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;基本対策1:非集約列をすべてGROUP BYに入れる
-- SELECTに出す列は全て GROUP BY へ
SELECT c.region, c.name, COUNT(*) AS orders
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region, c.name;・列が増えるほどグループは細かくなる。意図が「地域単位の件数」なら、nameを出す/出さないで結果の粒度が変わる点に注意。
基本対策2:集約関数で1値に縮約する(代表値を決める)
-- 代表値を1つに縮めたいなら集約関数で包む
SELECT c.region,
COUNT(*) AS orders,
SUM(o.amount) AS revenue,
MAX(o.amount) AS max_amount,
STRING_AGG(DISTINCT c.name, ', ' ORDER BY c.name) AS customers
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region;・「任意の1件でよい」は MAX/MIN で代用可能(数値/文字列)。こだわりの選択基準があるなら次項の「代表行の選定」を使う。
式・別名・序数の扱い(書き方のバリエーション)
-- 式そのものを GROUP BY
SELECT date_trunc('month', o.created_at) AS ym, SUM(o.amount)
FROM orders o
GROUP BY date_trunc('month', o.created_at)
ORDER BY ym;
-- 別名で GROUP BY(PostgreSQL は可)
SELECT date_trunc('month', o.created_at) AS ym, SUM(o.amount)
FROM orders o
GROUP BY ym
ORDER BY ym;
-- 序数で GROUP BY(1列目=ym)
SELECT date_trunc('month', o.created_at) AS ym, SUM(o.amount)
FROM orders o
GROUP BY 1
ORDER BY 1;JOIN時の落とし穴(1対多の爆発と対処)
-- NG: 「顧客ごとの合計」が意図なのに、商品名までSELECTすると粒度が壊れる
SELECT c.id, c.name, p.product_name, SUM(o.amount)
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN products p ON p.id = o.id
GROUP BY c.id, c.name; -- product_name が未集約 → エラー
-- 対処1: product_name を SELECT しない(粒度を保つ)
SELECT c.id, c.name, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
-- 対処2: product_name を集約(たとえば一覧化)
SELECT c.id, c.name, SUM(o.amount),
STRING_AGG(DISTINCT p.product_name, ', ') AS products
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN products p ON p.id = o.id
GROUP BY c.id, c.name;代表行を1件だけ返すパターン(DISTINCT ON / サブクエリ)
-- 1) DISTINCT ON で「各顧客の最新注文」を1件だけ
-- DISTINCT ON (key) ORDER BY key, 条件 … が鉄板
SELECT DISTINCT ON (o.customer_id)
o.customer_id, o.id AS order_id, o.amount, o.created_at
FROM orders o
ORDER BY o.customer_id, o.created_at DESC; -- 各customer_idで最新
-- 2) サブクエリで「代表行id」を決めてJOIN
WITH latest AS (
SELECT customer_id, MAX(created_at) AS mx
FROM orders GROUP BY customer_id
)
SELECT o.*
FROM orders o JOIN latest l
ON o.customer_id = l.customer_id AND o.created_at = l.mx;・「代表値を決めてからJOIN」または「DISTINCT ON」が混乱しにくい。安易に非集約列を出さない。
HAVING/ORDER BYでの発生例(NG→OK)
-- NG: HAVING に非集約列が登場
SELECT c.region, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region
HAVING c.name = 'Alice'; -- エラー
-- OK: HAVING は集計後の条件。非集約条件は WHERE へ
SELECT c.region, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
WHERE c.name = 'Alice'
GROUP BY c.region;
-- ORDER BY も同様(非集約列を出すなら GROUP BY へ)
SELECT c.region, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region
ORDER BY c.name; -- エラー条件付き集計(CASE / FILTER)で要件を満たす
-- 「状態ごとの件数」を列で出すなら、列を増やさず集計で表現
SELECT c.region,
COUNT(*) FILTER (WHERE o.amount >= 10000) AS big_orders,
COUNT(*) FILTER (WHERE o.amount < 10000) AS small_orders,
SUM(o.amount) AS total
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region;
-- CASE でも同様
SELECT c.region,
SUM(CASE WHEN o.amount >= 10000 THEN 1 ELSE 0 END) AS big_orders
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region;ウィンドウ関数と併用する場合(2段階に分ける)
-- NG: 集計とウィンドウを同じ層で混在させて非集約列を参照
-- OK: まず集計、その結果に対してウィンドウをかける
WITH agg AS (
SELECT c.region, SUM(o.amount) AS revenue
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.region
)
SELECT region,
revenue,
revenue / SUM(revenue) OVER () AS share
FROM agg
ORDER BY revenue DESC;デバッグの最短手順(どの列が違反か見抜く)
-- 1) SELECT の各列を「集約 or GROUP BY」のどちらかに分類
-- 2) HAVING/ORDER BY にも非集約列が紛れていないかを確認
-- 3) 1対多JOINなら、代表値の設計(集約 or 代表行)を決める
EXPLAIN (VERBOSE, COSTS OFF)
SELECT c.name, SUM(o.amount)
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.id; -- 実行計画とエラー箇所を併読書き換えチートシート(NG→OK)
-- P1: 非集約列がSELECTにある
-- NG: SELECT a, SUM(b) FROM t GROUP BY b;
-- OK: SELECT a, SUM(b) FROM t GROUP BY a;
-- P2: JOIN後に片側の列を出したい
-- NG: SELECT c.name, SUM(o.amount) FROM c JOIN o GROUP BY c.id;
-- OK: SELECT c.id, c.name, SUM(o.amount) FROM c JOIN o GROUP BY c.id, c.name;
-- P3: 商品名まで出したいが「顧客ごと合計」が目的
-- NG: SELECT c.name, p.product_name, SUM(o.amount) ... GROUP BY c.name;
-- OK: SELECT c.name, SUM(o.amount), STRING_AGG(DISTINCT p.product_name, ', ') ...
-- GROUP BY c.name;
-- P4: 月別集計(式を出す)
-- NG: SELECT date_trunc('month', ts) AS m, SUM(x) FROM t GROUP BY ts;
-- OK: SELECT date_trunc('month', ts) AS m, SUM(x) FROM t GROUP BY m;
-- P5: 各グループの最新行
-- NG: SELECT *, MAX(ts) FROM t GROUP BY key; -- 非集約列だらけ
-- OK: SELECT DISTINCT ON (key) * FROM t ORDER BY key, ts DESC;
-- P6: HAVINGで非集約列を使っている
-- NG: ... GROUP BY region HAVING name = 'A';
-- OK: ... WHERE name = 'A' GROUP BY region;設計の指針(再発させない)
・「結果の粒度」を先に決め、SELECTに出す列=GROUP BYのキー、というルールを徹底。
・代表値は仕様で明文化(最新・最小・最大・任意など)し、DISTINCT ON か集約で実装。
・JOINは1対多を意識して、後段で列を増やすなら再度の集計/代表選定を行う。
・レビューでは「非集約列が紛れていないか」「HAVING/ORDER BYが集計結果に整合しているか」をチェック。
-
前の記事
PostgreSQL「syntax error in tsquery」の原因と対処 2025.09.11
-
次の記事
PostgreSQL「lock file already exists(postmaster.pid)」の原因と安全な解決手順【完全版】 2025.09.16
コメントを書く