GROUP BY는 여러 행을 묶음 단위로 압축한다
COUNT, SUM, AVG 같은 집계 함수는 수많은 행을 하나의 숫자로 줄여준다. 여기에 GROUP BY를 더하면 카테고리별로 집계할 수 있다. 고객별, 월별, 상태별로 숫자 하나씩 뽑아내는 식이다. 결과적으로 고유한 값(또는 값들의 조합) 하나가 결과 테이블의 한 행이 된다.
고객 세 명, 결과는 세 행. 원래 있던 여섯 행은 사라지고, 고객별 묶음으로 합쳐진 거죠. 그 안에서 COUNT(*)와 SUM(amount)가 각각 계산됩니다.
이해하는 요령은 이렇습니다. GROUP BY customer는 "같은 customer 값을 가진 행들을 하나의 그룹으로 묶어라"라는 뜻이에요. 그다음 집계 함수가 그룹마다 따로 작동합니다.
SELECT 절에 넣을 수 있는 컬럼
여기서 많이들 헷갈립니다. GROUP BY를 쓸 때 SELECT 절에 들어가는 컬럼은 반드시 GROUP BY 절에 포함되어 있거나, 아니면 집계 함수 안에 들어 있어야 합니다. 그렇지 않으면 값이 애매해지거든요. 그룹 안의 여러 행 중에서 도대체 어느 행 값을 가져와야 할까요?
SELECT region, rep, SUM(amount)을 GROUP BY region과 함께 작성하면, SQLite는 (다른 DB라면 거부할 쿼리지만) 너그럽게 실행해 줍니다. 다만 rep 값은 그룹 안에서 아무거나 골라서 보여줍니다. 지역마다 영업사원 한 명이 표시되긴 하는데, 어떤 사람이 뽑힐지는 보장되지 않죠. 이런 동작에 의존하지 마세요. 결과에 표시할 비집계 컬럼은 전부 GROUP BY에 넣는 게 원칙입니다.
HAVING 절: 집계 후 그룹을 필터링하기
WHERE는 그룹화하기 전에 행을 거르고, HAVING은 그룹화한 다음에 그룹을 거릅니다. where having 차이는 결국 이 한 줄이 전부입니다. 그래서 COUNT(*) > 1 같은 조건을 WHERE에 넣을 수 없는 거예요. WHERE가 실행되는 시점에는 아직 카운트 값 자체가 존재하지 않으니까요.
Cleo는 주문을 한 번밖에 하지 않았기 때문에 해당 그룹은 걸러집니다. Ada와 Boris만 남게 되죠. 조건은 개별 행이 아니라 각 그룹의 집계 값을 기준으로 평가된다는 점이 핵심입니다.
SELECT 절에서 지정한 컬럼 별칭을 HAVING 절에서 그대로 참조할 수도 있는데, SQLite는 이를 허용합니다:
HAVING 절에서 SUM(amount)을 또 적는 것보다 이 방식이 훨씬 읽기 좋습니다.
WHERE와 HAVING 차이, 그리고 함께 쓰기
WHERE와 HAVING은 둘 중 하나만 골라 쓰는 게 아닙니다. WHERE는 그룹을 만들기 전에 어떤 행을 포함할지 거르는 역할이고, HAVING은 만들어진 그룹 중에서 어떤 그룹을 결과로 내보낼지 거르는 역할입니다. 실무 쿼리에서는 보통 두 절을 같이 씁니다.
실행 순서대로 위에서 아래로 읽으면 다음과 같습니다:
WHERE status = 'paid'— 환불된 행은 아예 제거합니다.GROUP BY customer— 남은 행들을 고객별로 묶습니다.- 각 그룹마다
SUM(amount)이 계산됩니다. HAVING SUM(amount) > 75— 조건을 통과한 그룹만 남깁니다.
결과적으로 Boris(80 + 20 = 100)와 Cleo(200.00)만 살아남습니다. Ada는 결제 완료된 주문이 50.00 하나뿐이라 기준에 미달합니다.
HAVING 다중 조건과 다중 그룹 컬럼
HAVING 절은 WHERE와 동일한 불리언 연산자(AND, OR, NOT)를 그대로 쓸 수 있고, 두 개 이상의 컬럼으로 묶어서 더 세분화된 그룹을 만들 수도 있습니다:
(region, quarter) 조합 하나하나가 각각 별개의 그룹이 됩니다. 이 HAVING 절은 합계가 100을 넘으면서 동시에 거래 건수가 2건 이상이어야 한다는 두 조건을 모두 요구하죠. 결과적으로 ('North', 'Q1')과 ('South', 'Q2')만 통과합니다.
실전 패턴: 중복 데이터 찾아내기
특정 컬럼에서 중복된 값을 찾을 때 가장 많이 쓰는 방법이 바로 GROUP BY ... HAVING COUNT(*) > 1 쿼리입니다:
중복 두 건이 드러납니다. 이 시점에서 보통은 계정을 병합할지, UNIQUE 제약을 추가할지, 아니면 데이터를 정리할지 결정하게 되는데요. 어떤 길을 택하든 중복을 찾아내는 쿼리의 모양은 늘 똑같습니다.
GROUP BY 없이 HAVING만 쓰기
흔한 패턴은 아니지만 문법적으로는 허용됩니다. GROUP BY가 없으면 결과 집합 전체가 하나의 그룹으로 취급되고, HAVING이 이 그룹 전체를 한 번에 필터링하죠. 즉, 집계 결과가 조건을 만족하면 전부 나오고 그렇지 않으면 아무것도 나오지 않습니다:
합계가 160이기 때문에 결과 행이 하나 나옵니다. 임계값을 > 200으로 바꿔보면 아무 행도 반환되지 않죠. 실무에서는 거의 항상 HAVING을 GROUP BY와 함께 쓰지만, 문법적으로 꼭 그래야 하는 건 아니라는 점은 알아두면 좋습니다.
핵심 정리
GROUP BY는 키 단위로 행을 묶어주고, 집계 함수는 각 묶음 안에서 실행됩니다.SELECT에 들어간 비집계 컬럼은 모두GROUP BY에도 등장해야 합니다.WHERE는 그룹화 이전에 행을 거르고,HAVING은 그룹화 이후에 그룹을 거릅니다.COUNT(*)나SUM(...)같은 집계 함수는WHERE가 아니라HAVING에 써야 합니다.HAVING은 여러 조건을 조합할 수 있고,SELECT에서 정의한 별칭도 참조할 수 있습니다.
다음 주제: 외래 키
테이블 하나를 집계하는 것만으로도 꽤 쓸모가 있지만, 실제 스키마는 데이터가 여러 테이블에 흩어져 있는 경우가 대부분입니다. 주문은 여기, 고객은 저기, 상품은 또 다른 곳에 있죠. 외래 키는 이런 테이블들을 서로 연결해서 관계를 일관되게 유지해 주는 장치입니다. 다음 장에서 다뤄볼 내용이에요.
자주 묻는 질문
SQLite에서 WHERE랑 HAVING은 뭐가 다른가요?
WHERE는 그룹으로 묶기 전 단계에서 개별 행을 걸러내고, HAVING은 집계가 끝난 후 그룹 단위로 필터링합니다. 예를 들어 WHERE amount > 100은 금액이 100을 넘는 행만 남기지만, HAVING SUM(amount) > 100은 합계가 100을 넘는 그룹만 남깁니다. COUNT나 SUM 같은 집계 함수는 WHERE에서 못 쓰는데, 바로 그럴 때 쓰라고 있는 게 HAVING이에요.
SQLite에서 GROUP BY 없이 HAVING만 써도 되나요?
네, 가능합니다. GROUP BY가 없으면 SQLite는 결과 전체를 하나의 그룹으로 보고 HAVING으로 그 그룹을 통째로 필터링해요. 결과는 한 행 또는 0행이 나오죠. 실무에서는 거의 안 쓰는 패턴이고, 보통은 HAVING이 있으면 GROUP BY도 같이 따라옵니다.
COUNT 결과로 그룹을 필터링하려면 어떻게 하나요?
집계 함수는 WHERE가 아니라 HAVING에 넣어야 합니다. 예를 들어 SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 이렇게 쓰면 주문을 두 번 이상 한 고객만 나와요. SQLite에서는 HAVING 안에서 SELECT 절의 컬럼 별칭(alias)도 그대로 쓸 수 있습니다.