지금까지의 함수는 한 행을 받아 한 결과를 내는 단일행 함수였다. 이번에 다룬 다중행 함수(집계 함수)는 여러 행을 묶어 하나의 값으로 요약한다. "부서별 평균 급여", "직책별 인원 수" 같은 통계가 모두 여기서 나온다.
그리고 이 집계를 "무엇을 기준으로 묶을지" 정하는 것이 GROUP BY, 묶은 결과에 조건을 거는 것이 HAVING이다.
1. 다중행 함수 (집계 함수)
| 함수 | 설명 |
|---|---|
| SUM | 합계 |
| COUNT | 개수 |
| MAX | 최댓값 |
| MIN | 최솟값 |
| AVG | 평균 |
SELECT SUM(sal) FROM emp; -- 급여 합계
SELECT COUNT(*) FROM emp WHERE deptno=30; -- 30번 부서 인원
SELECT MAX(sal), MIN(sal) FROM emp WHERE deptno=10;
SELECT AVG(sal) FROM emp WHERE deptno=30;
- 단일 값으로 줄어든 집계 결과(SUM(SAL))와 행마다 다른 일반 열(ENAME)은 함께 출력할 수 없다. 결과의 행 수가 다르기 때문이다.
- COUNT는 NULL을 세지 않는다. 그래서 COUNT(COMM)은 수당이 있는 행만 센다.
- COUNT(*)는 NULL 포함 전체 행을, COUNT(열)은 그 열이 NULL이 아닌 행만 센다.
특히 "COUNT()와 COUNT(열)의 차이"는 실무에서 인원 집계가 틀리는 단골 원인이라고 한다. NULL을 포함해 세려면 `COUNT(), 값이 있는 행만 세려면COUNT(열)`이다. COUNT(COMM)을 인원수로 착각하면 수당 없는 사원이 통째로 빠지니 주의해야겠다.
2. GROUP BY 절
특정 열의 값을 기준으로 행을 묶고, 그룹마다 집계 결과를 낸다.
SELECT deptno, AVG(sal) FROM emp GROUP BY deptno ORDER BY deptno;
SELECT deptno, job, AVG(sal) FROM emp GROUP BY deptno, job ORDER BY deptno, job;
여러 열을 지정하면 "대그룹 안의 소그룹"으로 묶인다. 위 두 번째 쿼리는 부서로 먼저 묶고, 그 안에서 다시 직책으로 묶는다.
SELECT 절에는 GROUP BY에 명시한 열, 또는 집계 함수만 올 수 있다. 그룹으로 묶이지 않은 일반 열(예: ENAME)을 함께 쓰면 오류가 난다. "한 그룹이 한 행으로 줄어드는데, 그 그룹 안의 서로 다른 이름들 중 무엇을 보여줄 것인가?"라는 모순이 생기기 때문이다.
이 오류를 한 번 직접 내보고서야 규칙이 이해됐다. 외워서가 아니라 "한 행으로 줄어드는데 이름은 여럿"이라는 모순으로 받아들이니 잊히지 않았다.
3. HAVING 절
HAVING은 그룹으로 묶은 후, 그 그룹에 조건을 건다.
-- 부서·직책별 평균 급여가 2000 이상인 그룹만
SELECT deptno, job, AVG(sal)
FROM emp
GROUP BY deptno, job
HAVING AVG(sal) >= 2000
ORDER BY deptno, job;
WHERE vs HAVING
| 절 | 역할 | 시점 |
|---|---|---|
| WHERE | 출력 대상 행을 제한 | 그룹화 전 |
| HAVING | 그룹화된 결과를 제한 | 그룹화 후 |
집계 결과(AVG(SAL) 등)는 WHERE에 쓸 수 없다. WHERE가 평가되는 시점엔 아직 그룹이 만들어지지 않아 집계할 대상이 없기 때문이다. 그래서 "평균이 2000 이상" 같은 그룹 조건은 반드시 HAVING으로 건다.
-- WHERE로 행을 먼저 추린 뒤(급여 3000 이하), HAVING으로 그룹을 거른다
SELECT deptno, job, AVG(sal), COUNT(*)
FROM emp
WHERE sal <= 3000 -- 그룹화 전: 개별 행 제한
GROUP BY deptno, job
HAVING AVG(sal) >= 2000 -- 그룹화 후: 그룹 제한
ORDER BY deptno, job;
이 쿼리 하나에 "행 거르기(WHERE) → 묶기(GROUP BY) → 그룹 거르기(HAVING) → 정렬(ORDER BY)"라는 처리 순서가 그대로 담겨 있다.
실습 쿼리로 익히기
-- 부서별 평균·최대·최소 급여와 인원 (평균은 정수로 절삭)
SELECT DEPTNO, TRUNC(AVG(SAL)) AS AVG_SAL,
MAX(SAL) AS MAX_SAL, MIN(SAL) AS MIN_SAL, COUNT(*) AS CNT
FROM EMP GROUP BY DEPTNO ORDER BY DEPTNO;
-- 인원이 3명 이상인 직책
SELECT JOB, COUNT(*) FROM EMP GROUP BY JOB HAVING COUNT(*) >= 3;
-- 입사 연도·부서별 인원 (TO_CHAR로 연도 추출해 그룹화)
SELECT TO_CHAR(HIREDATE, 'YYYY') AS HIRE_YEAR, DEPTNO, COUNT(*) AS CNT
FROM EMP GROUP BY TO_CHAR(HIREDATE, 'YYYY'), DEPTNO;
-- 수당 유무별 인원 (NVL2로 가공한 값으로 그룹화)
SELECT NVL2(COMM, 'O', 'X') AS HAS_COMM, COUNT(*) AS CNT
FROM EMP GROUP BY NVL2(COMM, 'O', 'X');
마지막 두 예처럼 함수로 가공한 값을 기준으로 그룹화할 수도 있다. "연도별", "수당 유무별"처럼 원본 열에 없는 기준으로 묶고 싶을 때 쓰는 기법이다. 이걸 알고 나니 그룹화의 활용 폭이 확 넓어진 느낌이었다.
수업에서 직접 친 쿼리
수업에서는 부서별 평균 → 직책별 평균 → 인원수 순으로 GROUP BY를 단계적으로 쌓아갔다.
-- 각 부서의 직책별 급여 평균
SELECT DEPTNO, JOB, AVG(SAL)
FROM EMP
GROUP BY DEPTNO, JOB
ORDER BY DEPTNO, JOB;
-- 부서별 사원 수가 5를 초과하는 부서만
SELECT DEPTNO AS "부서", COUNT(*) AS "사원 수"
FROM EMP
GROUP BY DEPTNO
HAVING COUNT(*) > 5
ORDER BY DEPTNO;
WHERE와 HAVING을 한 쿼리에 같이 쓰는 문제도 풀었다. "급여 3000 이하 사원만 추린 뒤, 직책별 평균이 2000 이상인 그룹"을 구하면서 두 절의 시점 차이를 손으로 확인했다.
SELECT DEPTNO AS "부서번호", JOB AS "직책",
AVG(SAL) AS "평균급여", COUNT(*) AS "사원수"
FROM EMP
WHERE SAL <= 3000 -- 그룹화 전: 개별 행
GROUP BY DEPTNO, JOB
HAVING AVG(SAL) >= 2000 -- 그룹화 후: 그룹
ORDER BY DEPTNO, JOB;
-- 매니저들의 평균 급여가 2500 이하인 부서
SELECT DEPTNO
FROM EMP
WHERE JOB = 'MANAGER'
GROUP BY DEPTNO
HAVING AVG(SAL) <= 2500;
연습문제에서는 가공한 값으로 그룹화하는 것도 다뤘다. 입사 연도(TO_CHAR), 수당 유무(NVL2)를 기준으로 묶었다.
SELECT TO_CHAR(HIREDATE, 'YYYY') AS HIRE_YEAR, DEPTNO, COUNT(*) AS CNT
FROM EMP GROUP BY TO_CHAR(HIREDATE, 'YYYY'), DEPTNO;
오늘 느낀 점
- 집계 함수, GROUP BY, HAVING이 따로가 아니라 "행 거르기 → 묶기 → 그룹 거르기"라는 한 처리 흐름의 단계라는 게 보였다. 처리 순서를 잡으니 WHERE와 HAVING을 더는 헷갈리지 않았다.
- 그룹화 규칙(SELECT엔 묶인 열이나 집계만)을 오류로 직접 만나보니, 규칙이 자의적이 아니라 "한 행으로 줄어든다"는 사실에서 나온 필연이라는 게 이해됐다.
- 함수로 가공한 값으로도 그룹화가 된다는 점에서, GROUP BY가 단순 열 묶기를 넘어 "원하는 기준을 만들어 묶는" 도구라는 걸 알았다.
한 걸음 더
- 집계 함수는 NULL을 무시하므로
AVG(COMM)같은 평균이 직관과 다를 수 있다.AVG(COMM)은 "수당이 있는 사원들만의 평균"이지 전체 사원 평균이 아니다. 전체 기준 평균을 원하면AVG(NVL(COMM,0))처럼 NULL을 0으로 채워야 한다. 집계 전에 NULL을 어떻게 다룰지가 결과를 크게 바꾼다. - 표준 SQL에서는 GROUP BY에 없는 열을 SELECT에 쓰면 오류지만, MySQL의 일부 모드는 이를 허용해 임의의 값을 내보낸다. 편해 보여도 결과가 예측 불가능해지는 함정이라, 오라클의 엄격한 규칙에 익숙해지는 편이 안전하다.
- 부분합·총합을 한 번에 내고 싶을 땐
ROLLUP,CUBE같은 확장 그룹 함수가 있다. 예를 들어GROUP BY ROLLUP(deptno)는 부서별 합계에 더해 전체 총합 행까지 함께 만들어준다. 보고서용 집계에서 요긴하니 이런 게 있다는 것만 알아두면 좋다.
'data > oracle' 카테고리의 다른 글
| [데이터베이스실습] 05. 오라클 함수 - 숫자·날짜·변환 (0) | 2026.07.30 |
|---|---|
| [데이터베이스실습] 04. 오라클 함수 - 문자 함수 (0) | 2026.07.23 |
| [데이터베이스실습] 03. SELECT - WHERE 조건 (0) | 2026.07.16 |
| [데이터베이스실습] 02. 테이블 기초와 SELECT (0) | 2026.07.05 |
| [데이터베이스실습] 01. 데이터베이스 기초 (0) | 2026.06.26 |