GROUP BY는 같은 값을 가진 행을 그룹으로 묶어서, COUNT, SUM, AVG 같은 집계 함수가 테이블 전체에 대해 하나가 아니라 그룹마다 하나씩 결과를 반환하게 해요. 예제는 PostgreSQL, MySQL, SQLite, SQL Server에서 동작해요.
GROUP BY 사용법
GROUP BY 뒤에 그룹으로 묶을 컬럼을 적고, SELECT하는 나머지 컬럼은 모두 COUNT, SUM, AVG, MIN, MAX 같은 집계 함수로 감싸세요. 고유한 값마다 행이 하나씩 나와요:
SELECT group_column, AGGREGATE(value_column)
FROM table_name
GROUP BY group_column;
-- Input: scores
-- player | team | points
-- -------+------+-------
-- Ana | Red | 12
-- Ben | Blue | 9
-- Cleo | Red | 7
-- Dev | Blue | 15
-- Eli | Red | 4
SELECT team, SUM(points) AS total_points, COUNT(*) AS players
FROM scores
GROUP BY team;
-- Output:
-- team | total_points | players
-- -----+--------------+--------
-- Red | 23 | 3
-- Blue | 24 | 2
5개 행이 팀별로 하나씩, 2개 행이 되고, 각 집계는 해당 팀의 행만 봐요.
알아 둘 규칙
- SELECT한 모든 컬럼은 GROUP BY에 있거나 집계 함수 안에 있어야 해요. 그렇지 않으면 그룹에 보여 줄 단일 값이 없어서 쿼리가 실패해요:
-- Input: products
-- name | category
-- --------+---------
-- Blender | Kitchen
-- Toaster | Kitchen
-- Pillow | Bedroom
SELECT name, category, COUNT(*) AS product_count
FROM products
GROUP BY category;
-- ERROR: column "products.name" must appear in the GROUP BY clause
-- or be used in an aggregate function
-- (SQLite runs it anyway and takes name from one row in each group)
해당 컬럼을 GROUP BY에 추가하거나, 예를 들어 MIN(name)처럼 집계해서 해결하세요.
- NULL은 하나의 그룹이 돼요. 그룹 컬럼이 NULL인 행은 모두 같은 그룹에 들어가요. COALESCE로 라벨을 붙여 보세요:
-- Input: products
-- name | category
-- ------------+-----------
-- Notebook | Stationery
-- Gift Card | NULL
-- Pen Set | Stationery
-- Mystery Box | NULL
-- Stapler | Stationery
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category;
-- Output:
-- category | product_count
-- -----------+--------------
-- NULL | 2
-- Stationery | 3
SELECT COALESCE(category, 'Uncategorized') AS category, COUNT(*) AS product_count
FROM products
GROUP BY category;
-- Output:
-- category | product_count
-- --------------+--------------
-- Uncategorized | 2
-- Stationery | 3
-
WHERE는 그룹화 전에 행을, HAVING은 그룹화 후에 그룹을 필터링해요.
COUNT(*) > 2같은 집계 조건은 HAVING에서만 쓸 수 있어요. 집계가 필요 없는 조건은 WHERE에 두어야 그룹화할 행이 줄어들어요. - 그룹은 특정 순서 없이 반환돼요. 예를 들어 SQLite와 SQL Server는 위의 점수 쿼리에서 Red보다 Blue를 먼저 보여 줘요. 순서가 중요하다면 항상 ORDER BY를 추가하세요.
다른 문법
표현식을 반복하는 대신 컬럼 별칭이나 SELECT 목록에서의 컬럼 위치로 그룹화할 수도 있어요:
-- Input: orders
-- order_id | amount
-- ---------+-------
-- 1 | 35.00
-- 2 | 240.00
-- 3 | 18.50
-- 4 | 120.00
-- 5 | 64.00
SELECT CASE WHEN amount >= 100 THEN 'large' ELSE 'small' END AS order_size,
COUNT(*) AS order_count
FROM orders
GROUP BY order_size -- or GROUP BY 1
ORDER BY order_size;
-- SQL Server allows neither: repeat the CASE expression in GROUP BY
-- Output:
-- order_size | order_count
-- -----------+------------
-- large | 2
-- small | 3
ROLLUP은 소계와 총계를 추가해요. 소계 행은 합산 대상 컬럼에 NULL을 표시해요:
-- Input: sales
-- region | category | amount
-- -------+----------+-------
-- East | Books | 100.00
-- East | Toys | 50.00
-- West | Books | 80.00
-- East | Books | 40.00
SELECT region, category, SUM(amount) AS total_sales
FROM sales
GROUP BY ROLLUP(region, category);
-- MySQL: GROUP BY region, category WITH ROLLUP
-- SQLite has no ROLLUP
-- Output:
-- region | category | total_sales
-- -------+----------+------------
-- East | Books | 140.00
-- East | Toys | 50.00
-- East | NULL | 190.00
-- West | Books | 80.00
-- West | NULL | 80.00
-- NULL | NULL | 270.00
집계 없는 GROUP BY는 고유한 값을 한 번씩 반환해서, SELECT DISTINCT category FROM products와 같은 결과가 나와요.
GROUP BY는 언제 사용하나요?
질문에 “~별” 또는 “~마다”가 들어가면 GROUP BY를 쓰세요. 고객별 주문, 월별 매출, 부서별 직원 수처럼요. 대부분의 용도는 다음 4가지 중 하나에 해당해요.
그룹별 행 개수 세기
상태별 주문 수, 카테고리별 상품 수, 일별 가입자 수 같은 경우예요. COUNT(*)는 그룹의 모든 행을 세고, COUNT(column)은 해당 컬럼이 NULL이 아닌 행만 세요.
그룹별 합계, 평균, 범위
SUM, AVG, MIN, MAX는 각 그룹의 값을 하나의 숫자로 바꿔 줘요. 고객별 매출, 평균 주문 금액, 카테고리에서 가장 싼 상품과 가장 비싼 상품 같은 것이죠. 쿼리 하나로 필요한 만큼 여러 개를 반환할 수 있어요.
여러 컬럼으로 결과 나누기
GROUP BY 뒤에 여러 컬럼을 나열하면 조합마다 행이 하나씩 나와요. 지역별·월별 매출, 창고별·상품 유형별 재고처럼요. 데이터에 실제로 있는 조합만 표시돼요.
필요한 그룹만 남기기
HAVING은 그룹이 만들어진 뒤 집계 값을 사용해 그룹을 필터링해요. 직원이 10명 넘는 부서, $1,000 넘게 쓴 고객, 또는 두 번 이상 나타나는 값 등이요. 마지막 방법으로 중복을 찾을 수 있어요.
GROUP BY 실전 예제
빈 카테고리를 포함해 모든 카테고리의 상품 수 세기
GROUP BY는 존재하는 값에 대해서만 그룹을 만들어요. 상품이 없는 카테고리까지 나열하려면 categories 테이블에서 시작해 products를 LEFT JOIN하세요. * 대신 조인한 테이블의 컬럼을 세야 해요. 빈 카테고리도 NULL로 채워진 행이 하나 생기기 때문에 COUNT(*)는 그 행을 세거든요.
-- Input: categories
-- category_name
-- -------------
-- Audio
-- Cameras
-- Drones
-- Input: products
-- id | name | category
-- ---+------------+---------
-- 1 | Earbuds | Audio
-- 2 | Soundbar | Audio
-- 3 | Action Cam | Cameras
-- Wrong: COUNT(*) counts the NULL row of an empty category
SELECT c.category_name, COUNT(*) AS product_count
FROM categories c
LEFT JOIN products p ON p.category = c.category_name
GROUP BY c.category_name
ORDER BY c.category_name;
-- Output:
-- category_name | product_count
-- --------------+--------------
-- Audio | 2
-- Cameras | 1
-- Drones | 1
-- Right: COUNT(p.id) skips it
SELECT c.category_name, COUNT(p.id) AS product_count
FROM categories c
LEFT JOIN products p ON p.category = c.category_name
GROUP BY c.category_name
ORDER BY c.category_name;
-- Output:
-- category_name | product_count
-- --------------+--------------
-- Audio | 2
-- Cameras | 1
-- Drones | 0
매출 상위 3개 카테고리 찾기
카테고리별 매출을 합산하고 합계로 정렬한 뒤 처음 3개만 남기세요. Books는 개별 매출이 Toys의 단일 매출보다 모두 작지만 2위를 차지해요:
-- Input: purchases
-- category | amount
-- ---------+-------
-- Books | 45.00
-- Toys | 120.00
-- Garden | 310.00
-- Books | 80.00
-- Music | 25.00
SELECT category, SUM(amount) AS revenue
FROM purchases
GROUP BY category
ORDER BY revenue DESC
LIMIT 3;
-- SQL Server has no LIMIT: write SELECT TOP 3 category, ... instead
-- Output:
-- category | revenue
-- ---------+--------
-- Garden | 310.00
-- Books | 125.00
-- Toys | 120.00
지역별·월별 매출 보고서
지역과 날짜에서 추출한 월로 그룹화하면 지역마다 월별로 행이 하나씩 나와요:
-- Input: sales
-- region | sale_date | amount
-- -------+------------+-------
-- East | 2024-01-08 | 120.00
-- West | 2024-01-15 | 80.00
-- East | 2024-01-22 | 95.00
-- East | 2024-02-03 | 60.00
-- West | 2024-02-11 | 150.00
SELECT region, EXTRACT(MONTH FROM sale_date) AS month, SUM(amount) AS total_sales
FROM sales
GROUP BY region, EXTRACT(MONTH FROM sale_date)
ORDER BY region, month;
-- Output:
-- region | month | total_sales
-- -------+-------+------------
-- East | 1 | 215.00
-- East | 2 | 60.00
-- West | 1 | 80.00
-- West | 2 | 150.00
SQL Server와 SQLite에는 EXTRACT가 없어요. SQL Server에서는 MONTH(sale_date)를, SQLite에서는 strftime('%m', sale_date)를 사용하세요. 데이터가 여러 해에 걸쳐 있다면 연도로도 그룹화하세요.
올해 재구매 고객 찾기
WHERE로 2024년 주문만 남기고, HAVING으로 그중 2건 이상인 고객만 남겨요. 고객 202는 전체 주문이 2건이지만 2024년에는 1건뿐이라 제외돼요:
-- Input: orders
-- order_id | customer_id | order_date
-- ---------+-------------+-----------
-- 1 | 201 | 2023-11-20
-- 2 | 202 | 2023-12-05
-- 3 | 201 | 2024-01-09
-- 4 | 202 | 2024-02-14
-- 5 | 201 | 2024-03-02
SELECT customer_id, COUNT(*) AS orders_2024
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
HAVING COUNT(*) >= 2;
-- Output:
-- customer_id | orders_2024
-- ------------+------------
-- 201 | 2
GROUP BY 더 알아보기
이 블로그의 관련 글:
- SQL COUNT DISTINCT 가이드: 그룹별 고유 값 개수 세기
-
SQL 중복 찾기 가이드:
HAVING COUNT(*) > 1과 함께 쓰는 GROUP BY - SQL ORDER BY 가이드: 그룹화된 결과 정렬하기
- SQL CASE 문 가이드: 집계 안에서 조건부 개수와 합계 구하기
- SQL 피벗 테이블 가이드: 그룹화된 행을 컬럼으로 바꾸기
- SQL COALESCE 가이드: NULL 그룹에 라벨 붙이기
- SQL JOIN 다이어그램 가이드: 빈 그룹을 유지하는 LEFT JOIN
공식 문서:
Beekeeper Studio는 무료 오픈 소스 데이터베이스 GUI예요
지금까지 써 본 SQL 쿼리 & 에디터 도구 중 최고예요. 데이터베이스 관리에 필요한 모든 게 다 있어요. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio는 빠르고 직관적이며 사용하기 쉬워요. 다양한 데이터베이스를 지원하고 Windows, Mac, Linux에서 모두 잘 작동해요.
Beekeeper Studio 사용자 후기
"Beekeeper Studio가 예전 SQL 작업 방식을 완전히 대체했어요. 빠르고 직관적이라 데이터베이스 작업이 다시 즐거워졌어요."
"여러 데이터베이스 GUI를 써 봤지만, Beekeeper는 기능과 단순함 사이의 균형이 완벽해요. 그냥 잘 돌아가요."