공공부하자개발 · 영어 학습 노트
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정리‹ 이전다음 ›

4. 응용 변형 예제

소스: sql-src/mid_10_group_having/02_stats.sql, 03_dialects.sql. 통계 리포트에서 자주 쓰는 패턴과 GROUP BY 규칙의 DB 별 차이를 확인합니다.

변형 1: 월별·요일별 집계

sql
SELECT
       EXTRACT(MONTH FROM sale_date) AS mon
     , COUNT(*)                      AS cnt
     , SUM(amt)                      AS total_amt
  FROM sales
 GROUP BY EXTRACT(MONTH FROM sale_date)
 ORDER BY mon;
text
MON | CNT | TOTAL_AMT
----+-----+----------
1   | 4   | 1150
2   | 4   | 900
3   | 2   | 500
(3행)

EXTRACT(MONTH FROM ...) 는 Oracle·MySQL 이 공통으로 쓰는 함수로, GROUP BY 절에도 SELECT 와 똑같이 식을 반복해서 씁니다. MSSQL 은 같은 일을 MONTH(...) 로 합니다(4절 변형5).

소스 파일은 같은 방식으로 FORMATDATETIME(sale_date, 'EEEE') 로 요일 이름별 집계도 실행합니다(H2 전용 함수). 요일을 숫자로 표현하면 몇 요일이 1인지·시작 요일이 무엇인지가 DB 마다 달라(Oracle TO_CHAR(d,'DY'), MySQL DAYNAME(d), MSSQL DATENAME(WEEKDAY, d)), 숫자 대신 요일명 함수를 쓰는 편이 안전합니다.

변형 2: 데이터 없는 날짜를 0으로 채우는 달력 조인

sql
WITH RECURSIVE cal(d) AS (
      SELECT DATE '2024-01-01'
       UNION ALL
      SELECT DATEADD('DAY', 1, d)
        FROM cal
       WHERE d < DATE '2024-01-10'
)
SELECT
       cal.d                     AS sale_day
     , COALESCE(SUM(s.amt), 0)   AS total_amt
  FROM cal
  LEFT JOIN sales s ON s.sale_date = cal.d
 GROUP BY cal.d
 ORDER BY cal.d;
text
SALE_DAY   | TOTAL_AMT
-----------+----------
2024-01-01 | 0
2024-01-02 | 0
2024-01-03 | 0
2024-01-04 | 0
2024-01-05 | 300
2024-01-06 | 0
2024-01-07 | 0
2024-01-08 | 150
2024-01-09 | 0
2024-01-10 | 0
(10행)

sales 만 GROUP BY sale_date 로 집계하면 주문이 있던 날짜만 나와 "매출 0원인 날"이 리포트에서 빠집니다. 재귀 CTE cal 로 1월 1~10일을 모두 만든 뒤 LEFT JOIN 하고 COALESCE(SUM(...), 0) 으로 NULL 을 0 으로 바꾸면 날짜가 빠짐없이 나옵니다.

재귀 CTE 문법은 MySQL 8.0·H2·Oracle(11gR2 부터)은 WITH RECURSIVE, MSSQL 은 RECURSIVE 없이 WITH 만 씁니다. 기본 재귀 횟수도 달라 MSSQL 은 기본 100 회이며 OPTION (MAXRECURSION n) 으로 늘리고, Oracle 은 CONNECT BY LEVEL <= n 방식도 있지만 H2 는 지원하지 않습니다(문법 검토만).

변형 3: 구성비 %와 정수 나눗셈 주의

sql
SELECT
       cust
     , SUM(amt)                                     AS cust_total
     , SUM(amt) * 100 / (SELECT SUM(amt) FROM sales) AS pct_wrong
  FROM sales
 GROUP BY cust
 ORDER BY cust_total DESC;
text
CUST   | CUST_TOTAL | PCT_WRONG
-------+------------+----------
김민준 | 700        | 27
박서준 | 700        | 27
최도윤 | 500        | 19
이서연 | 300        | 11
오지훈 | 250        | 9
정하윤 | 100        | 3
(6행)

SUM(amt) * 100 도 (SELECT SUM(amt) FROM sales) 도 정수라서, 나눗셈 결과가 소수점 이하를 버린 정수(27, 19, 9 ...)로 나와 실제 비율(27.5%, 19.6% ...)과 어긋납니다.

sql
SELECT
       cust
     , SUM(amt)                                             AS cust_total
     , ROUND(SUM(amt) * 100.0 / SUM(SUM(amt)) OVER (), 1)    AS pct
  FROM sales
 GROUP BY cust
 ORDER BY cust_total DESC;
text
CUST   | CUST_TOTAL | PCT
-------+------------+-----
김민준 | 700        | 27.5
박서준 | 700        | 27.5
최도윤 | 500        | 19.6
이서연 | 300        | 11.8
오지훈 | 250        | 9.8
정하윤 | 100        | 3.9
(6행)

100.0 처럼 소수 리터럴을 곱하면 결과가 소수로 계산됩니다. SUM(SUM(amt)) OVER () 는 그룹별 합계를 창 함수로 전체 합계로 다시 묶어 서브쿼리 없이 구성비를 구하는 패턴입니다. 실제 DB 도 갈립니다. MSSQL 은 INT / INT 가 항상 정수지만 Oracle·MySQL 은 소수 결과를 줍니다.

변형 4: 상위 N 그룹과 문자열 모아 붙이기

sql
SELECT
       cust
     , SUM(amt) AS total_amt
  FROM sales
 GROUP BY cust
HAVING SUM(amt) >= 400
 ORDER BY total_amt DESC;
text
CUST   | TOTAL_AMT
-------+----------
김민준 | 700
박서준 | 700
최도윤 | 500
(3행)

HAVING 으로 합계 400 이상인 그룹만 남기고 ORDER BY total_amt DESC 로 큰 순서로 정렬했습니다. "기준값 이상"을 거를 때는 간단하지만, "정확히 상위 3개"처럼 순위로 자르려면 분석 함수(고급 01·02)의 RANK()·ROW_NUMBER() 가 필요합니다.

sql
SELECT
       LISTAGG(cust, ', ') WITHIN GROUP (ORDER BY cust) AS cust_list
  FROM sales
 WHERE sale_date < DATE '2024-02-01';
text
CUST_LIST
------------------------------
김민준, 박서준, 이서연, 최도윤
(1행)

LISTAGG 는 그룹 안의 여러 행 값을 한 문자열로 이어 붙이며, WITHIN GROUP (ORDER BY ...) 로 순서를 정합니다. DB 별 문법 차이는 변형 6 에서 비교합니다.

변형 5: GROUP BY 규칙의 DB 별 차이

sql
-- @error
SELECT id, dept, COUNT(*) FROM emp GROUP BY dept;
text
예상 오류: Column "ID" must be in the GROUP BY list

2.3절 규칙을 H2 가 실제로 거부하는지 확인했습니다. 다음은 SELECT 별칭을 GROUP BY·HAVING 에 쓰는 예입니다.

sql
SELECT
       dept AS d
     , COUNT(*) AS cnt
  FROM emp
 GROUP BY d
HAVING cnt > 1
 ORDER BY d;
text
D    | CNT
-----+----
개발 | 2
(1행)

H2 는 기본 모드는 물론 SET MODE Oracle·MySQL·MSSQLServer 어디서든 별칭(d·cnt)을 GROUP BY·HAVING 에 그대로 쓸 수 있습니다.

주의

실제 DB 는 다릅니다. MySQL 은 GROUP BY·HAVING 에 SELECT 별칭을 쓸 수 있지만, MSSQL 은 쓸 수 없고 식을 반복해야 하며, Oracle 도 23ai 이전 버전은 GROUP BY 에 별칭을 쓸 수 없습니다. H2 는 모든 모드에서 별칭을 허용해 이 차이를 재현하지 못합니다(H2 에서 재현 안 됨, 문법 검토만). 여러 DB 를 오가는 코드라면 별칭 대신 dept·COUNT(*) 처럼 식을 그대로 반복해 쓰는 편이 안전합니다.

변형 6: 문자열 집계·날짜 그룹 함수 이름 비교

DB 문자열 집계 날짜 그룹 함수
Oracle·Tibero LISTAGG(col,',') WITHIN GROUP (ORDER BY ...)(11gR2 부터) EXTRACT(YEAR FROM d)
MySQL GROUP_CONCAT(col ORDER BY ... SEPARATOR ',') EXTRACT(YEAR FROM d)
MSSQL STRING_AGG(col,',') WITHIN GROUP (ORDER BY ...)(2017 부터) YEAR(d)·MONTH(d)·DATEPART(WEEKDAY, d)
sql
-- MySQL
SET MODE MySQL;
SELECT
       dept
     , GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') AS names
  FROM emp
 GROUP BY dept
 ORDER BY dept;
text
DEPT | NAMES
-----+---------------
개발 | 박과장, 이부장
경영 | 김대표
영업 | 정사원
(3행)

MySQL 의 GROUP_CONCAT 은 결과 길이가 group_concat_max_len(기본 1024바이트)을 넘으면 뒤가 잘리고, MSSQL 의 STRING_AGG 는 LISTAGG 처럼 길이 한도가 없습니다. Tibero 는 Oracle 호환이지만 LISTAGG 지원 여부는 버전별로 확인이 필요합니다.

날짜 그룹 함수는 4절 변형1 에서 EXTRACT 로 이미 확인했습니다. MSSQL 은 YEAR(d)·MONTH(d) 처럼 부분마다 전용 함수를 쓰고, 요일 번호는 DATEPART(WEEKDAY, ...) 인데 H2 가 지원하지 않습니다(문법 검토만). 요일별 집계는 변형 1 의 요일명 함수 방식을 씁니다.

응용 변형 예제
  • 변형 1: 월별·요일별 집계
  • 변형 2: 데이터 없는 날짜를 0으로 채우는 달력 조인
  • 변형 3: 구성비 %와 정수 나눗셈 주의
  • 변형 4: 상위 N 그룹과 문자열 모아 붙이기
  • 변형 5: GROUP BY 규칙의 DB 별 차이
  • 변형 6: 문자열 집계·날짜 그룹 함수 이름 비교
이전 섹션3 코드 예제4 / 6다음 섹션5 자주 하는 실수 (Tip)