Menu

SQLite GROUP BY와 HAVING으로 그룹 필터링하기

SQLite에서 GROUP BY로 행을 묶고 HAVING으로 집계 결과를 걸러내는 방법, 그리고 WHERE와 HAVING이 어떻게 다른지 예제로 확실히 짚어봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

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 차이, 그리고 함께 쓰기

WHEREHAVING은 둘 중 하나만 골라 쓰는 게 아닙니다. WHERE는 그룹을 만들기 전에 어떤 행을 포함할지 거르는 역할이고, HAVING은 만들어진 그룹 중에서 어떤 그룹을 결과로 내보낼지 거르는 역할입니다. 실무 쿼리에서는 보통 두 절을 같이 씁니다.

실행 순서대로 위에서 아래로 읽으면 다음과 같습니다:

  1. WHERE status = 'paid' — 환불된 행은 아예 제거합니다.
  2. GROUP BY customer — 남은 행들을 고객별로 묶습니다.
  3. 각 그룹마다 SUM(amount)이 계산됩니다.
  4. 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으로 바꿔보면 아무 행도 반환되지 않죠. 실무에서는 거의 항상 HAVINGGROUP 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을 넘는 그룹만 남깁니다. COUNTSUM 같은 집계 함수는 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)도 그대로 쓸 수 있습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기