행 요약하기: GROUP BY

집계는 여러 행을 수나 합계처럼 요약합니다. 주문 하나하나를 보는 대신, 상태별 주문 수나 카테고리별 매출처럼 그룹 단위의 결과를 만듭니다.

그룹마다 숫자 하나

GROUP BY는 같은 값을 가진 행을 그룹으로 묶고, 집계 함수는 각 그룹을 숫자 하나로 요약합니다.

1

SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
Planche에서 실행

결과 — 7행 중 처음 5행

statusorders
delivered1351
paid695
shipped641
pending349
refunded327

브라우저의 새 탭에서 Planche가 열리고, 샘플 데이터베이스가 로드된 채로 이 쿼리가 들어갑니다. 이 페이지의 다른 예제도 같은 탭을 다시 씁니다. F9를 누르기 전까지는 아무것도 실행되지 않습니다.

상태가 일곱 가지라 결과도 일곱 행입니다. SELECT에는 그룹 기준 컬럼이나 집계 결과만 둘 수 있습니다. 예를 들어 order_number를 함께 고르면 배송 완료 주문 1,351건 가운데 어느 주문번호를 보여줘야 할지 정할 수 없습니다.

COUNT(*)와 COUNT(컬럼)은 다릅니다

2

SELECT COUNT(*) AS customers,
       COUNT(phone) AS with_phone,
       COUNT(*) - COUNT(phone) AS without_phone
FROM customers;
Planche에서 실행

결과 — 1행

customerswith_phonewithout_phone
15001221279

COUNT(*)는 모든 행을 셉니다. COUNT(phone)은 phone이 NULL이 아닌 행만 셉니다. 두 값의 차이가 전화번호가 없는 고객 수이며, 별도 쿼리 없이 빠진 값의 개수를 구할 수 있습니다.

다른 집계 함수도 NULL을 건너뜁니다. 예를 들어 10, 20, NULL의 평균은 10이 아니라 15입니다. NULL을 0으로 계산하는 것이 아니라 평균 대상에서 제외하기 때문입니다. 0으로 취급하려면 COALESCE(컬럼, 0)처럼 명시해야 합니다.

집계 여러 개를 한 번에

3

SELECT cat.name AS category,
       COUNT(*) AS products,
       ROUND(MIN(p.price), 2) AS cheapest,
       ROUND(AVG(p.price), 2) AS average,
       ROUND(MAX(p.price), 2) AS dearest
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
ORDER BY average DESC
LIMIT 20;
Planche에서 실행

결과 — 20행 중 처음 5행

categoryproductscheapestaveragedearest
Wearables11417.31672.292433.73
Programming106.891585.672420.02
Tablets17314.761529.452412.93
Audio11593.81482.892334.28
Kitchen12670.541443.212293.8

이 쿼리는 테이블을 한 번 읽고 카테고리마다 다섯 값을 계산합니다. 앱에서 직접 처리하려면 상품 행을 모두 가져와 반복해야 하지만, 데이터베이스에서는 요약 결과만 받으면 됩니다.

조인 결과를 그룹 짓기

집계와 조인을 함께 사용할 수도 있습니다. 먼저 테이블을 조인하고 그 결과를 그룹으로 묶습니다.

4

SELECT cat.name AS category,
       COUNT(DISTINCT o.id) AS orders,
       SUM(oi.quantity) AS units,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
WHERE o.status IN ('paid', 'shipped', 'delivered')
GROUP BY cat.id
ORDER BY revenue DESC;
Planche에서 실행

결과 — 27행 중 처음 5행

categoryordersunitsrevenue
Watches395815933900.92
Board Games395801929534.55
Laptops366768900778.09
Science340709888542.78
Cycling405823858478.88

WHERE는 앞에서, HAVING은 뒤에서

둘 다 결과를 거르지만 적용되는 시점이 다릅니다.

5

SELECT cat.name AS category,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
HAVING SUM(oi.quantity * oi.unit_price) > 1000000
ORDER BY revenue DESC;
Planche에서 실행

결과 — 8행 중 처음 5행

categoryrevenue
Board Games1377454.99
Tablets1348572.94
Science1347046.09
Watches1325717.58
Cycling1288143.46

WHERE는 그룹을 만들기 전에 어떤 행을 포함할지 정합니다. HAVING은 그룹을 만든 뒤 어떤 그룹을 남길지 정합니다. 원본 컬럼 조건은 WHERE에, 집계 조건은 HAVING에 써야 하는 이유입니다. WHERE가 평가될 때는 집계값이 아직 만들어지지 않았습니다.

SELECT 레슨의 절 순서를 보면 이 규칙을 이해하기 쉽습니다.

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY  ->  LIMIT

계산한 값으로 그룹 짓기

컬럼만이 아니라 표현식으로도 그룹을 지을 수 있습니다. 타임스탬프의 앞 일곱 글자를 자르면 2025-01이 되고, 이 값이 월이 됩니다.

6

SELECT substr(ordered_at, 1, 7) AS month,
       COUNT(*) AS orders,
       ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
  AND ordered_at >= '2025-01-01'
GROUP BY month
ORDER BY month;
Planche에서 실행

결과 — 12행 중 처음 5행

monthordersrevenue
2025-0152311707.73
2025-0253350775.62
2025-0358351184.96
2025-0452374465.94
2025-0553381541.09

월별·주별 리포트도 대개 이렇게 만든 값을 기준으로 그룹을 짓습니다. 여기서는 GROUP BY month가 SELECT의 별칭을 다시 씁니다. 다만 이 허용 범위는 엔진마다 다를 수 있으므로, 이식성이 필요하면 표현식을 그대로 반복하는 편이 안전합니다.

여기까지로 할 수 있는 것

이제 그룹별 건수, 합계, 평균 같은 리포트를 만들 수 있습니다. 다음 레슨에서는 행 하나를 전체 요약과 비교하거나, 카테고리별 상위 몇 개를 찾는 데 쓰는 서브쿼리를 다룹니다.

직접 해보기

  1. 국가마다 고객이 몇 명인가요? 100명 이상인 국가만, 많은 순으로 보여주세요.

    정답 보기

    정답 보기

    SELECT country, COUNT(*) AS customers
    FROM customers
    GROUP BY country
    HAVING COUNT(*) >= 100
    ORDER BY customers DESC;
    Planche에서 실행

    결과 — 12행 중 처음 5행

    countrycustomers
    KR155
    AU132
    IT130
    ES126
    GB126

    GROUP BY가 국가마다 한 행을 만들고, 조건이 개수에 걸리므로 WHERE가 아니라 HAVING에 들어갑니다. 별칭 대신 표현식을 다시 쓰면 어느 엔진에서나 동작합니다.

  2. 평점이 가장 높은 상품 15개를, 리뷰가 8개 이상인 상품만 대상으로 보여주세요.

    정답 보기

    정답 보기

    SELECT p.name,
           COUNT(r.id) AS reviews,
           ROUND(AVG(r.rating), 2) AS avg_rating
    FROM products p
    JOIN reviews r ON r.product_id = p.id
    GROUP BY p.id
    HAVING COUNT(r.id) >= 8
    ORDER BY avg_rating DESC
    LIMIT 15;
    Planche에서 실행

    결과 — 15행 중 처음 5행

    namereviewsavg_rating
    Pulse Yoga Mat84.63
    A Winter Letter (3rd ed.)94.56
    Terra Planter84.5
    Nimbus Cosmos84.5
    Ivory Toaster94.33

    같은 그룹에 집계 둘입니다. 하나는 리뷰를 세고 하나는 별점을 평균 냅니다. 별 다섯짜리 리뷰 하나가 목록 맨 위를 차지하는 것을 막는 게 HAVING 필터입니다.