소스: sql-src/mid_10_group_having/02_stats.sql, 03_dialects.sql. 통계 리포트에서 자주 쓰는 패턴과 GROUP BY 규칙의 DB 별 차이를 확인합니다.
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;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)), 숫자 대신 요일명 함수를 쓰는 편이 안전합니다.
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;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 는 지원하지 않습니다(문법 검토만).
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;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% ...)과 어긋납니다.
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;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 은 소수 결과를 줍니다.
SELECT
cust
, SUM(amt) AS total_amt
FROM sales
GROUP BY cust
HAVING SUM(amt) >= 400
ORDER BY total_amt DESC;CUST | TOTAL_AMT
-------+----------
김민준 | 700
박서준 | 700
최도윤 | 500
(3행)HAVING 으로 합계 400 이상인 그룹만 남기고 ORDER BY total_amt DESC 로 큰 순서로 정렬했습니다. "기준값 이상"을 거를 때는 간단하지만, "정확히 상위 3개"처럼 순위로 자르려면 분석 함수(고급 01·02)의 RANK()·ROW_NUMBER() 가 필요합니다.
SELECT
LISTAGG(cust, ', ') WITHIN GROUP (ORDER BY cust) AS cust_list
FROM sales
WHERE sale_date < DATE '2024-02-01';CUST_LIST
------------------------------
김민준, 박서준, 이서연, 최도윤
(1행)LISTAGG 는 그룹 안의 여러 행 값을 한 문자열로 이어 붙이며, WITHIN GROUP (ORDER BY ...) 로 순서를 정합니다. DB 별 문법 차이는 변형 6 에서 비교합니다.
-- @error
SELECT id, dept, COUNT(*) FROM emp GROUP BY dept;예상 오류: Column "ID" must be in the GROUP BY list2.3절 규칙을 H2 가 실제로 거부하는지 확인했습니다. 다음은 SELECT 별칭을 GROUP BY·HAVING 에 쓰는 예입니다.
SELECT
dept AS d
, COUNT(*) AS cnt
FROM emp
GROUP BY d
HAVING cnt > 1
ORDER BY d;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(*)처럼 식을 그대로 반복해 쓰는 편이 안전합니다.
| 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) |
-- MySQL
SET MODE MySQL;
SELECT
dept
, GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') AS names
FROM emp
GROUP BY dept
ORDER BY dept;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 의 요일명 함수 방식을 씁니다.