PostgreSQL「column must appear in the GROUP BY clause」エラーの修正

beginner🐘 PostgreSQL2026-06-23| PostgreSQL 10以降(Linux、macOS、Windows)

Error Message

ERROR: column "table.column_name" must appear in the GROUP BY clause or be used in an aggregate function
#postgresql#sql#group-by#aggregate

エラーの内容

ERROR: column "orders.customer_name" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT customer_name, status, COUNT(*) FROM orders GROUP BY status
               ^

問題を確認してみましょう:SELECTcustomer_name を指定しているにもかかわらず、GROUP BY には status しか含まれていません。ひとつの status グループには、50 件の異なる customer_name を持つ 50 行のデータが存在する可能性があります。PostgreSQL はそのうちどれを返せばよいか判断できないため、クエリを拒否します。

MySQL から移行してきた場合、これは意外な落とし穴になるかもしれません。MySQL は黙って任意の行の値を選んで返しますが、それはたいてい意図しない結果です。PostgreSQL はそのようなクエリを最初から拒否します。これはバグではなく、SQL 標準に準拠した正しい動作です。

このエラーが発生する典型的なクエリ

-- このクエリは失敗します:
SELECT customer_name, status, COUNT(*)
FROM orders
GROUP BY status;

-- ERROR: column "orders.customer_name" must appear in the GROUP BY clause...

status でグループ化しながら customer_name を SELECT しています。各 status グループには多くの異なる customer_name が含まれているため、PostgreSQL はどれを選ぶかを自動的に決めてくれません。

修正方法のステップバイステップ

修正方法 1:GROUP BY に不足しているカラムを追加する

最もシンプルな修正方法です。SELECT に含まれる集約関数を使っていないカラムはすべて、GROUP BY にも含める必要があります。

-- 誤り:
SELECT customer_name, status, COUNT(*)
FROM orders
GROUP BY status;

-- 修正後:
SELECT customer_name, status, COUNT(*)
FROM orders
GROUP BY customer_name, status;

両方のカラムでグループ化することが、レポートの目的に合っている場合に使用してください。

修正方法 2:カラムを集約関数でラップする

グループ内のすべての値ではなく、ひとつの値だけが必要な場合は、MIN()MAX()、または string_agg() を使用します。

-- status グループごとに任意の customer_name を 1 件取得する:
SELECT MIN(customer_name) AS customer_name, status, COUNT(*)
FROM orders
GROUP BY status;

-- すべての customer_name をカンマ区切りのリストとして取得する:
SELECT status,
       COUNT(*) AS order_count,
       string_agg(DISTINCT customer_name, ', ') AS customers
FROM orders
GROUP BY status;

-- または配列として取得する:
SELECT status, COUNT(*), array_agg(DISTINCT customer_name) AS customers
FROM orders
GROUP BY status;

修正方法 3:DISTINCT ON を使用する(PostgreSQL 固有)

DISTINCT ON は、ソート後にパーティションごとに 1 行を残します。サブクエリや CTE は不要です。

-- status ごとに最新の注文を取得する:
SELECT DISTINCT ON (status)
  customer_name, status, order_amount, created_at
FROM orders
ORDER BY status, created_at DESC;

ORDER BY により、各グループで残す行が決まります。この例では、created_at が最も新しい行が選ばれます。

修正方法 4:ROW_NUMBER() を使った CTE を使用する

集約条件に一致する完全な行(たとえば status ごとに最も大きな注文)が必要な場合は、ウィンドウ関数を使うとすっきりと書けます。

-- status ごとに最も金額の高い注文を取得する:
WITH ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY status ORDER BY order_amount DESC) AS rn
  FROM orders
)
SELECT customer_name, status, order_amount
FROM ranked
WHERE rn = 1;

動作する完全なサンプル

-- セットアップ:
CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  customer_name TEXT,
  status TEXT,
  order_amount NUMERIC,
  created_at TIMESTAMP DEFAULT NOW()
);

INSERT INTO orders (customer_name, status, order_amount) VALUES
  ('Alice', 'pending', 150.00),
  ('Bob',   'pending', 200.00),
  ('Carol', 'shipped', 350.00),
  ('Dave',  'shipped', 100.00);

-- 誤り — customer_name が GROUP BY に含まれていない:
SELECT customer_name, status, COUNT(*), SUM(order_amount)
FROM orders
GROUP BY status;
-- ERROR: column "orders.customer_name" must appear in the GROUP BY clause...

-- オプション A: 両方のカラムでグループ化する:
SELECT customer_name, status, SUM(order_amount)
FROM orders
GROUP BY customer_name, status;
--  customer_name | status  |   sum
-- ---------------+---------+--------
--  Alice         | pending | 150.00
--  Bob           | pending | 200.00
--  Carol         | shipped | 350.00
--  Dave          | shipped | 100.00

-- オプション B: status のみで集計する — customer_name を除外:
SELECT status, COUNT(*) AS orders, SUM(order_amount) AS total
FROM orders
GROUP BY status;
--  status  | orders |  total
-- ---------+--------+--------
--  pending |      2 | 350.00
--  shipped |      2 | 450.00

-- オプション C: 統計情報と status ごとの全顧客名を取得:
SELECT status,
       COUNT(*),
       string_agg(customer_name, ', ') AS customers
FROM orders
GROUP BY status;
--  status  | count |   customers
-- ---------+-------+---------------
--  pending |     2 | Alice, Bob
--  shipped |     2 | Carol, Dave

修正の確認

修正したクエリを psql に貼り付けて、エラーではなく正しく行が返ってくることを確認してください:

-- エラーではなく行が返ってくることを確認:
SELECT status, COUNT(*), SUM(order_amount)
FROM orders
GROUP BY status;

--  status  | count |   sum
-- ---------+-------+--------
--  pending |     2 | 350.00
--  shipped |     2 | 450.00
-- (2 rows)

DISTINCT ON をテストする場合は、ユニークな status の値ごとにちょうど 1 行が返ってくることを確認してください:

SELECT DISTINCT ON (status) status, customer_name, created_at
FROM orders
ORDER BY status, created_at DESC;

-- ユニークな status の値ごとに 1 行が返ってくるはずです。

どの修正方法を使うべきか

  • GROUP BY に追加する — そのカラムでもグループ化することが目的に合っている場合(最も一般的な修正方法)
  • MIN / MAX / string_agg / array_agg — グループ化されていないカラムの要約値や結合した値が必要な場合
  • DISTINCT ON — グループごとに 1 行が必要で、ORDER BY によって選択する行を定義できる場合
  • CTE 内の ROW_NUMBER() — 地域ごとの最高売上など、集約条件に一致する完全な行が必要な場合

もうひとつの落とし穴:GROUP BY でのエイリアス

PostgreSQL は GROUP BY 内でカラムエイリアスを使うことを許可しません。多くの開発者が日付の抽出を初めて扱うときにこの問題に直面します:

-- これは失敗します — GROUP BY でエイリアスを使用:
SELECT EXTRACT(YEAR FROM created_at) AS year, COUNT(*)
FROM orders
GROUP BY year;
-- ERROR: column "year" does not exist

-- 修正: 完全な式を繰り返す:
SELECT EXTRACT(YEAR FROM created_at) AS year, COUNT(*)
FROM orders
GROUP BY EXTRACT(YEAR FROM created_at);

-- または列の位置(1 始まりのインデックス)を使用する:
SELECT EXTRACT(YEAR FROM created_at) AS year, COUNT(*)
FROM orders
GROUP BY 1;

Related Error Notes