Sửa lỗi PostgreSQL "column must appear in the GROUP BY clause"

beginner🐘 PostgreSQL2026-06-23| PostgreSQL 10+ trên 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

Lỗi Gặp Phải

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
               ^

Nhìn vào vấn đề: SELECT yêu cầu lấy customer_name, nhưng GROUP BY chỉ liệt kê status. Một nhóm status có thể chứa 50 hàng với 50 tên khách hàng khác nhau. PostgreSQL không biết phải trả về cái nào — nên nó từ chối thực thi.

Nếu bạn quen dùng MySQL? Điều này có thể khiến bạn bất ngờ. MySQL âm thầm chọn giá trị từ một hàng tùy ý, thường là sai. PostgreSQL từ chối truy vấn ngay lập tức. Đây không phải lỗi — đây là hành vi đúng theo chuẩn SQL.

Truy Vấn Điển Hình Gây Ra Lỗi Này

-- Truy vấn này thất bại:
SELECT customer_name, status, COUNT(*)
FROM orders
GROUP BY status;

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

Bạn đang nhóm theo status nhưng lại chọn customer_name. Mỗi nhóm status chứa nhiều tên khách hàng khác nhau — PostgreSQL sẽ không tự chọn hộ bạn.

Các Cách Sửa Từng Bước

Cách 1: Thêm Cột Còn Thiếu Vào GROUP BY

Cách đơn giản nhất. Mọi cột không được tổng hợp trong SELECT đều phải xuất hiện trong GROUP BY.

-- Bị lỗi:
SELECT customer_name, status, COUNT(*)
FROM orders
GROUP BY status;

-- Đã sửa:
SELECT customer_name, status, COUNT(*)
FROM orders
GROUP BY customer_name, status;

Dùng cách này khi nhóm theo cả hai cột cùng nhau là đúng với yêu cầu báo cáo của bạn.

Cách 2: Bao Cột Trong Hàm Tổng Hợp

Cần lấy một giá trị từ nhóm — không phải tất cả? Dùng MIN(), MAX(), hoặc string_agg().

-- Lấy một tên khách hàng bất kỳ cho mỗi nhóm status:
SELECT MIN(customer_name) AS customer_name, status, COUNT(*)
FROM orders
GROUP BY status;

-- Gộp tất cả tên khách hàng thành danh sách phân cách bằng dấu phẩy:
SELECT status,
       COUNT(*) AS order_count,
       string_agg(DISTINCT customer_name, ', ') AS customers
FROM orders
GROUP BY status;

-- Hoặc dưới dạng mảng:
SELECT status, COUNT(*), array_agg(DISTINCT customer_name) AS customers
FROM orders
GROUP BY status;

Cách 3: Dùng DISTINCT ON (Đặc Trưng PostgreSQL)

DISTINCT ON giữ lại một hàng cho mỗi phân vùng sau khi sắp xếp. Không cần subquery, không cần CTE.

-- Lấy đơn hàng gần nhất cho mỗi status:
SELECT DISTINCT ON (status)
  customer_name, status, order_amount, created_at
FROM orders
ORDER BY status, created_at DESC;

Mệnh đề ORDER BY quyết định hàng nào được chọn cho mỗi nhóm. Ở đây: hàng có created_at mới nhất sẽ được lấy.

Cách 4: Dùng CTE Với ROW_NUMBER()

Muốn lấy toàn bộ hàng khớp với điều kiện tổng hợp — chẳng hạn đơn hàng lớn nhất theo từng status? Hàm window xử lý việc này rất gọn.

-- Lấy đơn hàng có giá trị cao nhất theo từng 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;

Ví Dụ Đầy Đủ Hoạt Động Được

-- Khởi tạo:
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);

-- LỖI — customer_name không có trong 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...

-- PHƯƠNG ÁN A: Nhóm theo cả hai cột:
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

-- PHƯƠNG ÁN B: Tóm tắt theo status — bỏ 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

-- PHƯƠNG ÁN C: Thống kê kèm danh sách khách hàng theo 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

Xác Nhận Kết Quả Sau Khi Sửa

Dán truy vấn đã sửa vào psql và đảm bảo nó trả về dữ liệu, không phải lỗi:

-- Phải trả về dữ liệu, không phải lỗi:
SELECT status, COUNT(*), SUM(order_amount)
FROM orders
GROUP BY status;

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

Đang kiểm tra DISTINCT ON? Xác nhận rằng bạn nhận được đúng một hàng cho mỗi giá trị status duy nhất:

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

-- Bạn sẽ thấy một hàng cho mỗi giá trị status duy nhất.

Nên Dùng Cách Nào?

  • Thêm vào GROUP BY — khi nhóm theo cột đó cũng là điều bạn muốn (cách sửa phổ biến nhất)
  • MIN / MAX / string_agg / array_agg — khi bạn cần giá trị tóm tắt hoặc kết hợp từ các cột không được nhóm
  • DISTINCT ON — khi bạn cần một hàng cho mỗi nhóm và có thể định nghĩa ORDER BY để chọn hàng ưu tiên
  • ROW_NUMBER() trong CTE — khi bạn cần toàn bộ hàng khớp với điều kiện tổng hợp, như doanh số cao nhất theo từng khu vực

Một Lưu Ý Thêm: Alias Trong GROUP BY

PostgreSQL từ chối alias cột trong GROUP BY. Nhiều lập trình viên gặp điều này ngay ngày đầu khi làm việc với trích xuất ngày tháng:

-- Lỗi — dùng alias trong GROUP BY:
SELECT EXTRACT(YEAR FROM created_at) AS year, COUNT(*)
FROM orders
GROUP BY year;
-- ERROR: column "year" does not exist

-- Sửa: lặp lại toàn bộ biểu thức:
SELECT EXTRACT(YEAR FROM created_at) AS year, COUNT(*)
FROM orders
GROUP BY EXTRACT(YEAR FROM created_at);

-- Hoặc dùng vị trí cột (đánh số từ 1):
SELECT EXTRACT(YEAR FROM created_at) AS year, COUNT(*)
FROM orders
GROUP BY 1;

Related Error Notes