Skip to Content
자격증SQLD15. 그룹 함수와 GROUP BY/HAVING

이번 문서의 목표: 이 파일을 다 읽으면 집계 함수와 GROUP BY로 데이터를 요약할 수 있고, WHERE와 HAVING을 실행 순서에 근거해 올바른 자리에 쓸 수 있으며, GROUP BY를 쓸 때 SELECT 절에 무엇을 쓸 수 있는지 규칙을 근거와 함께 설명할 수 있다.

그룹 함수란 무엇인가

14편에서 다룬 문자·숫자·날짜 함수는 모두 행 하나에 값 하나를 대응시키는 단일행 함수였다. 이번 편에서 다루는 그룹 함수(group function, 집계 함수(aggregate function)라고도 부른다) 는 정반대다. 여러 행의 값을 하나로 요약해 결과 하나만 낸다.

쉽게 말하면: 단일행 함수는 “각 행마다” 계산하고, 그룹 함수는 “여러 행을 묶어서” 계산한다.

함수NULL 처리
COUNT(*)행의 개수NULL이 포함된 행도 카운트에 포함
COUNT(컬럼)그 컬럼 값이 NULL이 아닌 행의 개수NULL인 값은 세지 않음
SUM(컬럼)합계NULL은 계산에서 제외(0으로 취급하지 않고 아예 무시)
AVG(컬럼)평균NULL인 행은 평균을 구하는 분모(행 수)에서도 제외
MAX(컬럼)최댓값NULL 제외하고 비교
MIN(컬럼)최솟값NULL 제외하고 비교

작은 예시로 직접 계산해 보기

다음과 같은 사원 테이블이 있다고 하자.

사원명부서보너스
김민준영업100
이서연영업200
박도윤홍보NULL
최하은홍보150
SELECT COUNT(*) AS 전체행수, COUNT(보너스) AS 보너스있는행수, SUM(보너스) AS 보너스합계, AVG(보너스) AS 보너스평균 FROM 사원;

이 쿼리의 결과는 다음과 같다.

전체행수보너스있는행수보너스합계보너스평균
43450150

결과 해석: COUNT(*)는 박도윤의 보너스가 NULL이어도 행 자체는 존재하므로 4를 센다. 반면 COUNT(보너스)는 NULL인 박도윤의 행을 세지 않아 3이다. SUM(보너스)는 NULL을 아예 없는 값처럼 취급해 100+200+150=450만 더한다(NULL을 0으로 바꿔 더하는 것이 아니라 계산 대상에서 완전히 빼버린다는 점이 중요하다). AVG(보너스)도 마찬가지로 NULL인 행을 분모에서 제외해 450을 4가 아니라 3으로 나눠 150이 된다. 만약 NULL을 0으로 착각해 4로 나누면 112.5가 나와야 하는데 그렇지 않다는 점이 핵심 함정이다.

자주 틀리는 점: “테이블에 행이 하나도 없으면 COUNT(*)는 몇을 반환하는가”라는 질문에 NULL이라고 답하는 실수가 잦다. 실제로는 행이 0건이어도 COUNT(*)는 그룹 자체가 사라지는 것이 아니라 0이라는 값을 정상적으로 반환한다(단, GROUP BY 없이 그룹 함수만 쓸 때는 테이블 전체가 하나의 그룹으로 취급되므로, 행이 없어도 “빈 그룹”에 대한 COUNT 결과인 0이 한 행으로 나온다). 이는 HAVING 조건 때문에 그룹 자체가 걸러져 결과가 아예 없는 것과는 다른 이야기다. 뒤에서 이 둘을 구분해서 다룬다.

GROUP BY: 값이 같은 행끼리 묶기

GROUP BY는 지정한 컬럼(들)의 값이 같은 행들을 하나의 그룹으로 묶는다. 그룹 함수는 이 그룹 하나하나에 대해 별도로 계산된다.

-- 부서별로 묶어서, 부서마다 보너스 합계를 구한다 SELECT 부서, SUM(보너스) AS 부서별보너스합계 FROM 사원 GROUP BY 부서;
부서부서별보너스합계
영업300
홍보150

쉽게 말하면: GROUP BY는 “부서별로”, “월별로”처럼 같은 값끼리 한 덩어리로 묶어주는 역할만 한다. 그 묶음 안에서 실제로 더하고 세고 평균 내는 일은 SUM, COUNT, AVG 같은 그룹 함수가 한다.

GROUP BY가 없을 때: GROUP BY 절 자체가 없어도 SELECT에 그룹 함수가 있으면, 오라클은 테이블 전체를 하나의 그룹으로 취급해 계산한다. 앞서 본 SELECT COUNT(*), SUM(보너스) FROM 사원 예시가 바로 이 경우다.

SQL 실행 순서 복습: WHERE와 HAVING이 각각 언제 실행되는가

13편에서 SQL의 실제 실행 순서를 다뤘다. 이번 편의 핵심 규칙 대부분이 바로 이 순서에서 나오므로 다시 한번 그림으로 짚고 넘어간다.

이 순서에서 알 수 있듯, WHERE는 그룹이 만들어지기 전에 실행되고 HAVING은 그룹이 만들어진 후에 실행된다. 이 실행 시점의 차이가 두 절의 역할을 완전히 갈라놓는다.

WHERE vs HAVING 대조표

구분WHEREHAVING
실행 시점GROUP BY보다 먼저GROUP BY보다 나중
거르는 대상개별 행(가공되지 않은 원본 데이터)그룹으로 묶인 집계 결과
집계 함수 사용불가능(오류)가능(오히려 이것이 주된 용도)
없는 경우모든 행이 그룹화 대상이 됨모든 그룹이 결과에 포함됨
조건 예시WHERE 연봉 > 3000HAVING SUM(보너스) > 200

집계 함수를 WHERE에 쓰면 왜 오류인가

-- 오류: WHERE 절에서 집계 함수 사용 SELECT 부서, SUM(보너스) FROM 사원 WHERE SUM(보너스) > 200 GROUP BY 부서;

이 문장이 오류인 이유는 실행 순서를 따라가 보면 분명해진다. SUM(보너스)라는 값은 여러 행을 하나로 합쳐야만 계산할 수 있는데, WHERE가 실행되는 시점은 GROUP BY보다 먼저다. 즉 WHERE가 실행되는 순간에는 아직 어떤 행들을 하나의 그룹으로 묶을지조차 정해지지 않았다. 합칠 그룹이 아직 존재하지 않는데 그 그룹의 합계를 미리 알아내 조건으로 쓰겠다는 것은 논리적으로 성립하지 않는다. 그래서 데이터베이스는 이 문장을 아예 오류로 처리한다.

반대로 HAVING은 GROUP BY 다음에 실행되므로, 그 시점에는 이미 각 그룹의 SUM(보너스) 값이 계산되어 있다. 그래서 HAVING은 이 계산된 합계를 조건으로 쓸 수 있다.

-- 정상: HAVING 절에서 집계 함수로 그룹 결과 필터링 SELECT 부서, SUM(보너스) AS 부서별보너스합계 FROM 사원 GROUP BY 부서 HAVING SUM(보너스) > 200;
부서부서별보너스합계
영업300

결과 해석: 홍보 부서의 합계(150)는 200을 넘지 못해 걸러지고, 영업 부서(300)만 결과에 남는다. 만약 이 조건을 WHERE로 옮겨 개별 행에 걸려고 하면, “보너스 값 자체”가 200 초과인 행(이서연 200은 넘지 못함, 사실상 아무도 해당 없음)을 찾는 것이 되어 부서 합계라는 원래 의도와 전혀 다른 결과가 나온다.

자주 틀리는 점: “WHERE와 HAVING 둘 다 조건절인데 왜 구분되어 있는가”에 “문법이 오래됐다”, “HAVING이 더 엄격하다” 같은 근거 없는 설명을 정답으로 착각하는 경우가 많다. 정답 근거는 항상 실행 순서(WHERE는 그룹화 이전의 개별 행, HAVING은 그룹화 이후의 집계 결과)여야 한다. 실제 기출에서도 “그룹 함수를 활용한 필터링이 가능한 절”을 묻는 문제의 정답은 항상 HAVING이며, 오답 해설 역시 “WHERE는 그룹화 이전 단계라 집계 함수를 조건으로 쓸 수 없다”는 실행 순서 논리로 설명된다.

COUNT(*)와 HAVING이 함께 만드는 헷갈리는 결과

SELECT COUNT(*) FROM DUAL HAVING COUNT(*) > 4;

DUAL은 오라클이 기본으로 제공하는, 행이 딱 1개뿐인 특수한 테이블이다. 이 쿼리에서 GROUP BY가 없으므로 DUAL 테이블 전체(1행)가 하나의 그룹으로 취급되고, COUNT(*)는 1이 된다. 그런데 HAVING COUNT(*) > 4, 즉 1 > 4는 거짓이다. HAVING 조건이 거짓이면 그 그룹 자체가 결과에서 제거된다.

결과 해석: 이 쿼리의 실행 결과는 “0”이 아니라 행이 하나도 없는 공집합(No rows selected) 이다. “COUNT(*)가 조건에 안 맞으니 0을 반환하겠지”라고 생각하기 쉽지만, HAVING은 조건을 만족하지 못한 그룹을 아예 삭제하는 것이지, 그 자리에 0이라는 값을 채워 넣는 것이 아니다. “값이 0인 행 1개”와 “행 자체가 없음”은 완전히 다른 결과다.

GROUP BY를 쓸 때 SELECT에 올 수 있는 것

이 규칙은 SQLD에서 가장 자주 출제되는 함정 중 하나다. GROUP BY를 쓴 SELECT 절에는 다음 두 가지만 올 수 있다.

  1. GROUP BY에 나열된 컬럼 (그대로)
  2. 그룹 함수(COUNT, SUM, AVG, MAX, MIN 등)로 감싼 컬럼
-- 정상: 부서(GROUP BY에 나열됨)와 SUM(그룹 함수)만 SELECT에 사용 SELECT 부서, SUM(보너스) FROM 사원 GROUP BY 부서; -- 오류: 사원명은 GROUP BY에 없고 그룹 함수로 감싸지도 않았다 SELECT 부서, 사원명, SUM(보너스) FROM 사원 GROUP BY 부서;

이 규칙이 존재하는 이유를 정확히 이해해야 한다. GROUP BY 부서로 묶으면 영업 부서에 속한 김민준, 이서연 두 사람의 행이 하나의 그룹(영업)으로 합쳐진다. 그런데 SELECT에 사원명을 그대로 쓰면, 데이터베이스는 이 한 줄짜리 결과 행에 “김민준”을 넣어야 할지 “이서연”을 넣어야 할지 결정할 방법이 없다. 그룹으로 묶는 순간 그 그룹 안의 개별 행 정보는 원칙적으로 사라지고 오직 그 그룹을 대표하는 값(그룹핑 컬럼 자체, 또는 그룹 함수로 요약한 값)만 의미를 가지기 때문이다. 그래서 그룹 함수로 명시적으로 요약하지 않은 컬럼은 SELECT에 쓸 수 없고, 이를 어기면 “GROUP BY 표현식이 아니다”라는 오류가 발생한다.

쉽게 말하면: GROUP BY로 여러 사람을 한 그룹으로 묶었으면, 그 그룹 안의 개인 이름처럼 사람마다 다른 값은 더 이상 하나로 답할 수 없다. 그룹 전체를 대표하는 값(그룹 기준 컬럼이나 합계·평균 같은 요약값)만 보여줄 수 있다.

같은 원리로, HAVING 절에도 이 규칙이 그대로 적용된다. HAVING 조건 역시 그룹 단위로 판정되므로, GROUP BY에 나열된 컬럼이나 그룹 함수만 조건에 쓸 수 있다.

자주 틀리는 점: GROUP BY 부서, 직급처럼 여러 컬럼으로 묶었을 때 SELECT에 부서만 쓰고 직급은 빠뜨려도 문법 오류는 아니다. 다만 이 경우 같은 부서라도 직급이 다르면 서로 다른 그룹으로 나뉘어 있으므로, 결과 화면에는 같은 부서 이름이 여러 행에 걸쳐 중복 출력될 수 있다는 점에 유의해야 한다.

집계 함수가 포함된 뷰(VIEW)도 같은 제약을 받는다

이 규칙은 참고로만 알아두면 좋은 실무 연결점인데, GROUP BY와 집계 함수로 정의된 뷰(view, 자주 쓰는 SELECT문에 이름을 붙여 테이블처럼 쓰게 해주는 객체)는 조회(SELECT)는 자유롭게 할 수 있지만 INSERT, UPDATE, DELETE 같은 DML은 원칙적으로 허용되지 않는다(갱신 불가능한 뷰). 그 뷰의 각 행이 실제 원본 테이블의 행 하나와 1대1로 대응하지 않고, 여러 행을 요약한 결과이기 때문에 값을 거꾸로 써넣을 방법이 없기 때문이다. 이는 결국 “그룹으로 묶은 결과에는 개별 행 정보가 남아있지 않다”는 지금까지의 논리와 정확히 같은 원리다.

SELECT 절 작성 규칙 정리

GROUP BY가 있는 쿼리의 SELECT 절을 작성할 때 스스로 점검할 순서는 다음과 같다.

  1. GROUP BY에 어떤 컬럼을 나열했는지 먼저 확인한다.
  2. SELECT에 쓰려는 컬럼이 GROUP BY 목록에 그대로 있는지 본다. 있으면 바로 써도 된다.
  3. 없다면, 그 컬럼을 반드시 그룹 함수(SUM·COUNT·AVG·MAX·MIN 등)로 감쌌는지 확인한다. 감쌌으면 문제없다.
  4. 둘 다 아니라면 그 컬럼은 SELECT에 쓸 수 없다. GROUP BY에 그 컬럼을 추가하거나, 그룹 함수로 요약하거나, 애초에 SELECT에서 빼야 한다.

직접 해보기

00편에서 만든 환경에서 진행합니다. 아직 준비하지 않았다면 00편을 먼저 보세요.

직접 해보기 1: WHERE와 HAVING에 같은 조건을 넣으면 결과가 어떻게 다른가

“200 초과”라는 조건을 WHERE 자리와 HAVING 자리에 각각 넣고, 어느 쪽이 개별 행을 거르고 어느 쪽이 부서 합계를 거르는지 실행해서 비교한다.

-- 예상: 보너스 값 자체가 200을 넘는 사람이 몇 명 나올까? SELECT 사원명, 부서, 보너스 FROM 사원 WHERE 보너스 > 200;
-- 예상: 부서별 보너스 합계가 200을 넘는 부서가 몇 개 나올까? SELECT 부서, SUM(보너스) AS 부서별보너스합계 FROM 사원 GROUP BY 부서 HAVING SUM(보너스) > 200;

무엇을 보아야 하나: 첫 쿼리는 몇 행이 나오고 누가 남는지, 두 번째 쿼리는 어떤 부서가 남는지 확인한다. 두 결과가 완전히 다른 질문에 답하고 있다는 것이 핵심이다.

왜 이걸 해보나: WHERE와 HAVING을 “둘 다 조건절”로 뭉뚱그려 외우면 실전에서 자리를 바꿔 쓰는 실수가 나온다. 직접 결과를 비교해보면 WHERE는 개별 행을, HAVING은 그룹 합계를 거른다는 차이가 몸에 남는다.

직접 해보기 2: 집계 함수를 WHERE에, 낯선 컬럼을 SELECT에 넣으면 실제로 어떤 오류가 나는가

본문에서 “안 된다”고 설명한 두 가지를 예상만 하지 말고 직접 실행해 오류 메시지를 눈으로 확인한다.

-- 예상: 오류가 난다면 어떤 문구일까? SELECT 부서, SUM(보너스) FROM 사원 WHERE SUM(보너스) > 200 GROUP BY 부서;
-- GROUP BY에 없는 컬럼(사원명)을 SELECT에 그대로 넣으면? SELECT 부서, 사원명, SUM(보너스) FROM 사원 GROUP BY 부서;

무엇을 보아야 하나: 두 쿼리 모두 오류가 나야 정상이다. DBeaver 하단 오류창에 뜨는 ORA 코드와 문구를 실제로 읽어본다.

왜 이걸 해보나: “실행 순서상 안 된다”는 설명을 글로만 읽으면 시험장에서 “이 SQL이 정상 실행되는가”를 묻는 문제 앞에서 헷갈린다. 오류 메시지를 직접 본 사람은 왜 안 되는지를 근거와 함께 기억한다.

핵심 정리

  • 그룹 함수(COUNT·SUM·AVG·MAX·MIN)는 여러 행을 하나의 값으로 요약한다. COUNT(*)는 NULL 포함 모든 행을, COUNT(컬럼)은 그 컬럼이 NULL이 아닌 행만 센다.
  • SUM과 AVG는 NULL을 0으로 바꾸는 것이 아니라 계산 대상에서 완전히 제외한다.
  • WHERE는 GROUP BY 이전의 개별 행을, HAVING은 GROUP BY 이후의 그룹(집계) 결과를 거른다. 이 차이는 SQL 실행 순서(FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY)에서 나온다.
  • 집계 함수를 WHERE에 쓰면 오류다. 그룹이 아직 만들어지지 않은 시점이라 집계값 자체가 존재하지 않기 때문이다.
  • HAVING 조건이 거짓이면 그 그룹은 결과에서 제거된다(“값 0”이 아니라 “행 없음”이 된다).
  • GROUP BY가 있는 SELECT 절에는 GROUP BY에 나열된 컬럼과 그룹 함수로 감싼 표현식만 쓸 수 있다. 그룹으로 묶는 순간 개별 행의 정보는 대표값 없이는 확정할 수 없기 때문이다.

마무리 복습

문제 14지선다
사원 테이블에 4행이 있고 그중 보너스 컬럼 값이 NULL인 행이 1개 있을 때, COUNT(*)와 COUNT(보너스)의 결과를 순서대로 나열한 것은?
문제 24지선다
다음 중 WHERE 절에 집계 함수(SUM, COUNT 등)를 사용할 수 없는 이유로 가장 타당한 것은?
문제 34지선다
GROUP BY로 묶은 쿼리에서 SELECT 절에 올 수 있는 것으로 옳은 것은?
문제 44지선다
GROUP BY 부서로 묶었는데 SELECT 절에 사원명 컬럼을 그룹 함수 없이 그대로 쓰면 오류가 나는 이유는?
문제 54지선다
SELECT COUNT(*) FROM DUAL HAVING COUNT(*) 초과 4 를 실행하면 어떤 결과가 나오는가?
문제 64지선다
다음 중 HAVING 절에 대한 설명으로 옳은 것은?
문제 74지선다
부서별 보너스 합계가 200을 초과하는 부서만 조회하려 할 때 올바른 SQL은?
문제 84지선다
GROUP BY와 집계 함수로 정의된 뷰(VIEW)에 INSERT 문을 실행하면 어떻게 되는가?

참고 자료

Last updated on