data/oracle

[데이터베이스실습] 06. 다중행 함수와 그룹화

렁치 2026. 8. 6. 01:45

지금까지의 함수는 한 행을 받아 한 결과를 내는 단일행 함수였다. 이번에 다룬 다중행 함수(집계 함수)는 여러 행을 묶어 하나의 값으로 요약한다. "부서별 평균 급여", "직책별 인원 수" 같은 통계가 모두 여기서 나온다.

그리고 이 집계를 "무엇을 기준으로 묶을지" 정하는 것이 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)는 부서별 합계에 더해 전체 총합 행까지 함께 만들어준다. 보고서용 집계에서 요긴하니 이런 게 있다는 것만 알아두면 좋다.