공공부하자개발 · 영어 학습 노트
SQL
SQL 중급서브쿼리·조인·함수·DDL·인덱스0/11 완료
  • 01서브쿼리: 스칼라·인라인 뷰·상관 서브쿼리, DB별 차이
  • 02EXISTS·IN·NOT IN 과 NULL 함정
  • 03조인 심화: INNER·OUTER·SELF·CROSS, Oracle (+)
  • 04집합 연산: UNION·UNION ALL·INTERSECT·MINUS/EXCEPT
  • 05조건 로직: CASE·DECODE·IIF
  • 06NULL 처리 함수
  • 07문자·날짜 함수 DB별 비교
  • 08DDL 과 제약 조건
  • 09인덱스 기초
  • 10집계와 GROUP BY·HAVING
  • 11뷰·시퀀스·자동 증가
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › SQL 중급 › 10 / 11

집계와 GROUP BY·HAVING

통계 쿼리의 기초
섹션 6진행 0 / 11
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6정리‹ 이전다음 ›

3. 코드 예제

소스: sql-src/mid_10_group_having/01_group_having.sql. 직원(emp) 6명(경영 1·개발 2·영업 3)과 주문(orders) 10건을 준비했습니다.

예제 1: 집계 함수 6종

sql
SELECT
       e.name
     , COUNT(*)             AS order_cnt
     , COUNT(DISTINCT o.cust) AS cust_cnt
     , SUM(o.amt)           AS total_amt
     , AVG(o.amt)           AS avg_amt
     , MIN(o.amt)           AS min_amt
     , MAX(o.amt)           AS max_amt
  FROM emp e
  JOIN orders o ON o.emp_id = e.id
 GROUP BY e.name
 ORDER BY e.name;
text
NAME   | ORDER_CNT | CUST_CNT | TOTAL_AMT | AVG_AMT | MIN_AMT | MAX_AMT
-------+-----------+----------+-----------+---------+---------+--------
강사원 | 1         | 1        | 400       | 400.0   | 400     | 400
박과장 | 2         | 1        | 600       | 300.0   | 100     | 500
이부장 | 4         | 2        | 1000      | 250.0   | 150     | 350
정사원 | 2         | 1        | 500       | 250.0   | 250     | 250
조사원 | 1         | 1        | 150       | 150.0   | 150     | 150
(5행)

이부장은 주문 4건 중 고객이 2명(김민준·이서연)뿐이라 CUST_CNT 가 2입니다. COUNT(*) 는 행 수 그대로 4지만, COUNT(DISTINCT o.cust) 는 중복 고객을 빼고 세어 값이 달라집니다.

예제 2: 여러 컬럼 GROUP BY

sql
SELECT
       e.dept
     , o.status
     , COUNT(*)   AS cnt
     , SUM(o.amt) AS total_amt
  FROM emp e
  JOIN orders o ON o.emp_id = e.id
 GROUP BY e.dept, o.status
 ORDER BY e.dept, o.status;
text
DEPT | STATUS | CNT | TOTAL_AMT
-----+--------+-----+----------
개발 | 완료   | 5   | 1400
개발 | 취소   | 1   | 200
영업 | 대기   | 1   | 250
영업 | 완료   | 3   | 800
(4행)

GROUP BY e.dept, o.status 처럼 컬럼을 여러 개 나열하면 두 컬럼의 조합이 같은 행끼리 한 그룹이 됩니다. 개발 부서만 봐도 완료·취소 상태가 다른 그룹으로 나뉘어 집계됩니다.

예제 3: WHERE·HAVING 평가 순서 비교

sql
SELECT
       e.dept
     , COUNT(*)   AS cnt
     , SUM(o.amt) AS total_amt
  FROM emp e
  JOIN orders o ON o.emp_id = e.id
 WHERE o.status = '완료'
 GROUP BY e.dept
 ORDER BY e.dept;
text
DEPT | CNT | TOTAL_AMT
-----+-----+----------
개발 | 5   | 1400
영업 | 3   | 800
(2행)

WHERE o.status = '완료' 가 그룹을 짓기 전에 취소·대기 건을 먼저 빼버려서, 개발 부서 합계가 완료 건만 반영한 1400 입니다. WHERE 없이 HAVING SUM(o.amt) > 700 만 쓰면 취소·대기 건까지 포함한 합계(개발 1600, 영업 1050)에 조건을 대입하므로 값 자체가 달라집니다.

sql
SELECT
       e.dept
     , COUNT(*)   AS cnt
     , SUM(o.amt) AS total_amt
  FROM emp e
  JOIN orders o ON o.emp_id = e.id
 WHERE o.status = '완료'
 GROUP BY e.dept
HAVING COUNT(*) >= 2
 ORDER BY e.dept;
text
DEPT | CNT | TOTAL_AMT
-----+-----+----------
개발 | 5   | 1400
영업 | 3   | 800
(2행)

WHERE 가 먼저 완료 건만 남기고, 그렇게 만들어진 그룹의 건수(COUNT(*))가 2 이상인지 HAVING 이 다시 확인합니다. 2.2절 평가 순서대로 WHERE 가 먼저, HAVING 이 나중입니다.

예제 4: GROUP BY 없는 HAVING

sql
SELECT
       COUNT(*)   AS cnt
     , SUM(amt)   AS total_amt
  FROM orders
HAVING SUM(amt) > 2000;
text
CNT | TOTAL_AMT
----+----------
10  | 2650
(1행)

GROUP BY 가 없으니 orders 전체가 하나의 그룹이고, 합계 2650 이 조건(2000 초과)을 만족해 1행이 나옵니다. 조건을 3000 초과로 바꾸면 "값이 0인 1행"이 아니라 "행 자체가 없음(0행)"이 됩니다. HAVING 조건을 만족하지 않는 그룹은 결과에서 아예 빠지기 때문입니다.

예제 5: 빈 테이블 집계

sql
SELECT
       COUNT(*) AS cnt
     , SUM(amt) AS total_amt
  FROM empty_sales;
text
CNT | TOTAL_AMT
----+----------
0   | NULL
(1행)
sql
SELECT
       dept
     , COUNT(*) AS cnt
  FROM empty_sales
 GROUP BY dept;
text
DEPT | CNT
-----+----
(0행)

empty_sales 는 행이 하나도 없는 테이블입니다. GROUP BY 없이 조회하면 COUNT(*) 는 0, SUM 은 NULL 인 1행이 나오지만, GROUP BY dept 를 붙이면 나눌 행이 아예 없어 0행입니다. 2.4절에서 설명한 차이가 그대로 재현됩니다.

예제 직접 실행

아래 폴더의 SQL 파일을 Git Bash 에서 H2 메모리 DB 로 실행합니다. 방언은 파일 안의 SET MODE 로 바꿉니다.

cd sql-src/mid_10_group_having
ls *.sql
bash ../run.sh <파일>.sql
코드 예제
  • 예제 1: 집계 함수 6종
  • 예제 2: 여러 컬럼 GROUP BY
  • 예제 3: WHERE·HAVING 평가 순서 비교
  • 예제 4: GROUP BY 없는 HAVING
  • 예제 5: 빈 테이블 집계
이전 섹션2 핵심 원리3 / 6다음 섹션4 응용 변형 예제