프로젝트마다 일하는 사람 수를 센 뒤 “세 명 이상인 프로젝트만” 보고 싶다면, 묶음을 다 만든 다음 묶음 단위로 한 번 더 걸러야 한다. 행을 하나씩 거르는 조건은 묶기 전에 쓰이므로 “이 묶음에 몇 행이 있는가” 같은 값을 볼 수 없다. 묶음마다 계산한 값으로 묶음을 통째로 남기거나 버리는 조건이 HAVING이다. 묶음 안의 행 몇 개만 골라내지는 못한다.
예시로 보기
GROUP BY로 프로젝트마다 일하는 직원 수를 구했다. 여기서 “직원이 둘보다 많은 프로젝트만” 남기려 한다. WHERE COUNT(*) > 2라고 쓰고 싶지만, WHERE는 묶기 전에 행을 하나씩 보는 자리라 묶음의 행 수를 알 수 없다. 묶은 뒤에 거르는 자리가 따로 있다.
SELECT Pnumber, Pname, COUNT(*)
FROM PROJECT, WORKS_ON
WHERE Pnumber = Pno
GROUP BY Pnumber, Pname
HAVING COUNT(*) > 2;
| Pnumber | Pname | 묶음의 행 수 | HAVING COUNT(*) > 2 |
|---|---|---|---|
| 1 | ProductX | 2 | 버린다 |
| 2 | ProductY | 3 | 남긴다 |
| 3 | ProductZ | 2 | 버린다 |
| 10 | Computerization | 3 | 남긴다 |
| 20 | Reorganization | 3 | 남긴다 |
| 30 | Newbenefits | 3 | 남긴다 |
행이 둘뿐인 ProductX와 ProductZ의 묶음이 통째로 빠지고, 결과는 ProductY, Computerization, Reorganization, Newbenefits 4행이다(모두 3).
WHERE에 쓴 집계 함수의 오류, HAVING Dno > 1, GROUP BY 없는 HAVING, 카드 C2. SQLite 3.50.4와 PostgreSQL 16에서 같은 값이 나온다. 묶지 않은 열을 쓴 HAVING Salary > 30000만 다르다(PostgreSQL은 오류, SQLite는 받는다) — 70_having-clause_verify.py, 70_having-clause_verify.sql정의
HAVING 절은 묶음을 요약한 값에 조건을 건다. 조건이 참인 묶음만 결과에 나온다. GROUP BY 절과 함께 쓴다.
GROUP BY 묶는 속성
HAVING 묶음 조건
기호로 쓰면, 묶음을 \(G_1, \dots, G_m\), 묶음 조건을 \(c\)라 할 때 결과에 남는 묶음은 \(\{\,G_i : c(G_i)\text{가 참}\,\}\)이다. \(c(G_i)\)는 “묶음 \(G_i\) 전체를 보고 계산한 참, 거짓”으로 읽는다. 조건이 행이 아니라 묶음을 받는다는 점이 WHERE와 다르다.
HAVING에 쓸 수 있는 것은 SELECT에 쓸 수 있는 것과 같다. 묶음마다 값이 하나로 정해지는 것, 곧 집계 함수와 묶는 속성이다1.
| 조건 | 되는가 | 까닭 |
|---|---|---|
HAVING COUNT(*) > 2 |
된다 | 집계 함수는 묶음을 값 하나로 줄인다 |
HAVING SUM(Hours) > 40 |
된다 | SELECT에 적지 않은 집계 함수도 쓸 수 있다 |
HAVING Dno > 1 (Dno로 묶었을 때) |
된다 | 묶는 속성은 묶음 안에서 값이 같다 |
HAVING Salary > 30000 (Dno로 묶었을 때) |
PostgreSQL 16은 오류, SQLite는 받는다 | Salary는 묶음 안에서 행마다 다르다 |
거꾸로 WHERE에는 집계 함수를 쓸 수 없다. WHERE COUNT(*) > 2는 PostgreSQL에서 “aggregate functions are not allowed in WHERE”, SQLite에서 “misuse of aggregate” 오류다1.
예제
일한 시간의 합이 40을 넘는 프로젝트의 번호와 합을 구하라.1
- 묶는 속성: “프로젝트마다”이므로
Pno. - 묶음마다 구할 값:
SUM(Hours). - 묶음 조건: 합이 40을 넘어야 하므로
HAVING SUM(Hours) > 40. - 질의:
SELECT Pno, SUM(Hours) FROM WORKS_ON GROUP BY Pno HAVING SUM(Hours) > 40;
| Pno | SUM(Hours) | 남는가 |
|---|---|---|
| 1 | 52.5 | 남는다 |
| 2 | 37.5 | 빠진다 |
| 3 | 50.0 | 남는다 |
| 10 | 55.0 | 남는다 |
| 20 | 25.0 | 빠진다 |
| 30 | 55.0 | 남는다 |
프로젝트 20의 합 25.0은 시간이 적힌 두 행(10.0, 15.0)만 더한 값이다. NULL인 행은 합에서 빠진다.
활용
- 문제 글의 신호는 “~가 … 이상인 부서만”, “평균이 …를 넘는 ~에 대해”처럼 묶음의 요약값에 거는 조건이다.
GROUP BY없이HAVING만 쓰면 표 전체가 한 묶음이다.SELECT COUNT(*) FROM EMPLOYEE HAVING COUNT(*) > 5는 8을 한 행으로 내고,> 10으로 바꾸면 0행이다1.- 행에 거는 조건과 섞일 때의 가르기: WHERE와 HAVING 비교
- 연습: 중첩 질의·집계 연습의 p3, 복잡한 검색 연습의 p1, GROUP BY 질의 쓰기 예제 사다리
연결
- 선수: GROUP BY
- 헷갈리는 짝: WHERE와 HAVING 비교
- 여섯 절 가운데
HAVING이 놓이는 자리: ORDER BY와 질의의 절 순서
확인 문제
C1 HAVING 절이 하는 일을 쓰고, 조건에 쓸 수 있는 두 가지를 쓰라.
답: 묶음을 요약한 값에 조건을 걸어, 조건이 참인 묶음만 결과에 남긴다. 조건에는 집계 함수와 묶는 속성을 쓸 수 있다.
C2 부서별 직원 수는 5번 4명, 4번 3명, 1번 1명이다. SELECT Dno, COUNT(*) FROM EMPLOYEE GROUP BY Dno HAVING COUNT(*) >= 3;의 결과는?
답: (5, 4), (4, 3).
이유: 세 묶음 가운데 행 수가 3 이상인 것은 5번과 4번이다. 1번 묶음(Borg 한 명)은 통째로 빠진다.
C3 “직원이 둘보다 많은 프로젝트”를 구하면서 WHERE COUNT(*) > 2라고 쓰면 오류가 난다. 까닭을 질의가 돌아가는 차례로 설명하라.
답: WHERE는 묶기 전에 행을 하나씩 보고 남길지 정한다. 그때는 묶음이 아직 없어서 “묶음의 행 수”라는 값이 없다. 묶음이 만들어진 뒤에 쓰이는 HAVING에 적어야 한다.
흔한 오답: “COUNT는 SELECT에서만 쓸 수 있어서”라고 답하는 것. HAVING에서도 쓸 수 있다. 문제는 자리가 아니라 차례다.