본문 바로가기

HAVING과 실행 순서

원래의 행을 거르는 WHERE와 그룹의 결과를 거르는 HAVING을 나누어, 조건에 맞는 회원 그룹만 고릅니다. FROM부터 LIMIT까지의 논리적 실행 순서로 별칭과 집계 함수를 쓸 수 있는 절을 설명하고, 단계마다 남는 행과 그룹의 수를 세어 확인합니다.

"7월에 작성한 공개 글만 모아 회원별 합계를 만들고, 총 조회수가 120회 이상인 회원만 보여 달라"는 질문에는 두 종류의 필터가 있습니다. 공개 상태와 작성 날짜는 원래의 게시글 행에, 120회라는 기준은 그룹의 합계에 적용됩니다. 둘을 같은 WHERE에 넣으면 집계 함수를 쓸 수 없다는 오류가 납니다.

이 글에서는 FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT으로 이어지는 논리적 순서를 회원 게시판의 통계 하나에 적용합니다. 실제의 물리적 실행은 옵티마이저가 바꿀 수 있지만, 결과의 뜻은 이 순서로 읽습니다.

HAVING 예제 데이터

원래의 행, 필터를 지난 행, 그룹, 결과의 수를 단계마다 확인할 수 있도록 12행짜리 전용 테이블을 씁니다. 6월과 8월에 작성된 글, 초안과 숨김 글이 하나씩 섞여 있습니다.

ch4-4 HAVING fixture
DROP TABLE IF EXISTS ch4_having_post_demo;
CREATE TABLE ch4_having_post_demo (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  author_id BIGINT UNSIGNED NOT NULL,
  status VARCHAR(16) NOT NULL,
  view_count BIGINT UNSIGNED NOT NULL,
  created_at DATETIME(6) NOT NULL
) ENGINE = InnoDB;

INSERT INTO ch4_having_post_demo
  (id, author_id, status, view_count, created_at)
VALUES
  (1, 1, 'PUBLISHED', 50, '2026-07-02 09:00:00'),
  (2, 1, 'PUBLISHED', 55, '2026-07-10 09:00:00'),
  (3, 1, 'PUBLISHED', 60, '2026-07-20 09:00:00'),
  (4, 2, 'PUBLISHED', 45, '2026-07-03 09:00:00'),
  (5, 2, 'PUBLISHED', 55, '2026-07-11 09:00:00'),
  (6, 3, 'PUBLISHED', 50, '2026-07-12 09:00:00'),
  (7, 4, 'PUBLISHED', 60, '2026-07-13 09:00:00'),
  (8, 4, 'PUBLISHED', 70, '2026-07-14 09:00:00'),
  (9, 1, 'DRAFT', 100, '2026-07-15 09:00:00'),
  (10, 2, 'PUBLISHED', 100, '2026-06-30 09:00:00'),
  (11, 3, 'PUBLISHED', 100, '2026-08-01 09:00:00'),
  (12, 4, 'HIDDEN', 100, '2026-07-16 09:00:00');

WHERE에 집계식을 넣으면

총 조회수가 120회 이상인 회원을 찾으려고 WHERE에 SUM을 넣어 봅니다. WHERE는 아직 행을 하나씩 평가하는 단계여서, 회원의 그룹도 그 합계도 아직 없습니다.

집계 전에 SUM을 참조하는 조건
SELECT author_id, SUM(view_count) AS total_views
FROM ch4_having_post_demo
WHERE status = 'PUBLISHED'
  AND SUM(view_count) >= 120
GROUP BY author_id;
ERROR 1111 (HY000): Invalid use of group function

WHERE가 원래 행의 상태와 날짜로 행을 줄이고, GROUP BY가 남은 행을 회원별로 묶은 뒤에야 SUM이 계산됩니다. 그래서 합계에 대한 조건은 그 뒤의 HAVING에서 평가해야 합니다. 반대로 날짜와 상태처럼 그룹 안에서 값이 하나로 정해지지 않는 조건은 HAVING으로 옮길 수 없으므로, 원래 행의 모집단과 그룹의 조건을 나누어 적습니다.

SQL의 논리적 실행 순서

논리적으로는 FROM이 입력의 집합을 만들고, WHERE가 행을 하나씩 거릅니다. GROUP BY가 그룹을 만들면 집계 함수가 그룹의 값을 계산하고, HAVING이 그룹의 결과를 거릅니다. 그다음 SELECT가 결과의 표현식과 별칭을 만들고, ORDER BY가 순서를 정한 뒤 LIMIT이 범위를 자릅니다.

이 순서를 알면 별칭을 WHERE에서 쓸 수 없는 이유와, 집계에 대한 조건이 HAVING에 있어야 하는 이유를 한 모델로 설명할 수 있습니다. WHERE에서는 원래의 열을, HAVING에서는 그룹의 키와 집계를, ORDER BY에서는 결과의 별칭까지 쓸 수 있습니다.

WHERE와 GROUP BY, HAVING, ORDER BY는 표준 SQL입니다. MySQL은 HAVING에서도 SELECT의 별칭을 쓸 수 있게 표준을 넓혔지만 제품마다 차이가 있으므로, 이 교재는 읽기 쉬운 별칭을 쓰되 WHERE에는 원래 열의 조건을 둡니다. 옵티마이저가 조건을 안전하게 앞당겨 실행하더라도, SQL의 뜻은 논리적 순서로 설명합니다.

더 깊이: 옵티마이저가 조건을 실제로 옮기는 경우

논리적 순서와 물리적 실행이 갈라지는 대표적인 예가 조건의 이동입니다. MySQL은 GROUP BY도 집계 함수도 없는 쿼리의 HAVING을 WHERE와 합쳐서 처리합니다. 그룹이 없다면 HAVING의 조건도 행 하나에 대한 조건이 되기 때문입니다. HAVING이 남는 쿼리에서는 그룹을 하나씩 내보내기 전에 HAVING에 맞지 않는 그룹을 건너뜁니다.

MySQL 8.0.22부터는 바깥 쿼리의 WHERE 조건을 파생 테이블 안으로 밀어 넣는 파생 조건 푸시다운(derived condition pushdown)도 합니다. 파생 테이블이 GROUP BY로 집계한다면, 그룹 키에 대한 조건은 안쪽의 WHERE로, 집계 결과에 대한 조건은 안쪽의 HAVING으로 옮겨집니다. 예를 들어 회원별 합계를 파생 테이블로 만든 뒤 바깥에서 author_id > 10 AND total_views >= 120으로 거르면, 앞 조건은 그룹을 만들기 전에 원래 행을 줄이고 뒤 조건은 안쪽의 HAVING이 됩니다. 이 글에서 WHERE와 HAVING을 나눈 방식을 옵티마이저가 대신 적용하는 셈입니다.

다만 이 변환에는 조건이 있습니다. MySQL 8.4 매뉴얼에 따르면 파생 테이블에 LIMIT이 있거나, 조건에 서브쿼리나 비결정적인 식이 들어 있거나, 파생 테이블이 외부 조인의 안쪽 테이블이면 조건을 밀어 넣지 않습니다. 이 최적화는 optimizer_switch의 derived_condition_pushdown 플래그로 켜고 끄며 기본값은 on입니다. 그러니 결과의 뜻은 논리적 순서로 정하고, 어느 단계에서 행이 걸러지는지는 실행 계획으로 확인합니다.

WHERE와 HAVING을 나눈다

7월에 작성되었고 지금 공개 상태인 행이라는 조건은 WHERE에, 회원별 조회수 120회라는 그룹의 기준은 HAVING에 둡니다. 7월에 작성되었다는 조건은 게시 시각이 아니라 작성 시각인 created_at으로 검사하고, 정렬은 합계와 회원 id로 완전하게 만듭니다.

WHERE와 HAVING의 역할 분리
SELECT
  author_id,
  COUNT(*) AS published_posts,
  SUM(view_count) AS total_views
FROM ch4_having_post_demo
WHERE status = 'PUBLISHED'
  AND created_at >= '2026-07-01 00:00:00'
  AND created_at <  '2026-08-01 00:00:00'
GROUP BY author_id
HAVING SUM(view_count) >= 120
ORDER BY total_views DESC, author_id;
author_id | published_posts | total_views
1         | 3               | 165
4         | 2               | 130

WHERE를 지난 8행이 네 회원의 그룹이 되고, 합계가 각각 165, 100, 50, 130인 그룹 가운데 120 이상인 회원 1과 4만 남았습니다. 6월의 10번과 8월 1일의 11번은 작성 시각의 조건에서, 초안인 9번과 숨김 글인 12번은 상태의 조건에서 빠졌습니다.

HAVING에 SUM(...)을 다시 적으면 표준 SQL에서도 뜻이 분명하고, MySQL에서는 HAVING total_views >= 120도 됩니다. 팀에서 어느 쪽으로 쓸지 하나로 정해 둡니다. 그룹의 기준값을 사용자가 입력한다면 음수나 지나치게 큰 값이 들어오지 않는지도 애플리케이션에서 검사합니다.

단계마다 남는 수를 센다

최종 결과의 두 행만 보면 어느 단계에서 데이터가 줄었는지 알기 어렵습니다. 원래의 행, WHERE를 지난 행, 그룹, HAVING을 지난 그룹의 수를 따로 세어 기록합니다.

논리 단계별 규모 확인
WITH filtered AS (
  SELECT author_id, view_count
  FROM ch4_having_post_demo
  WHERE status = 'PUBLISHED'
    AND created_at >= '2026-07-01'
    AND created_at <  '2026-08-01'
), grouped AS (
  SELECT author_id, SUM(view_count) AS total_views
  FROM filtered
  GROUP BY author_id
)
SELECT
  (SELECT COUNT(*) FROM ch4_having_post_demo) AS source_rows,
  (SELECT COUNT(*) FROM filtered) AS filtered_rows,
  (SELECT COUNT(*) FROM grouped) AS group_rows,
  (SELECT COUNT(*) FROM grouped WHERE total_views >= 120) AS result_rows;
source_rows | filtered_rows | group_rows | result_rows
12          | 8             | 4          | 2

WITH로 시작하는 CTE는 WITH 이름 AS (SELECT ...)로 이 문장 안에서만 쓰는 이름 붙은 임시 결과를 만들고, 뒤의 SELECT가 그 이름을 테이블처럼 읽게 하는 문법입니다. 위 코드처럼 쉼표로 여러 개를 이어 정의하면 뒤의 grouped가 앞의 filtered를 읽을 수 있으며, 여기서는 단계를 나누어 보는 도구로만 씁니다. 자기 자신을 다시 읽는 재귀 CTE는 14장 2편에서 다룹니다. 12행이 8행으로 줄고, 행의 수가 그룹의 수로 단위를 바꾸어 4그룹이 된 뒤, 최종 2그룹이 남습니다. 다음 도표는 이 과정을 단계별로 보여 주며, 연결선은 실제 물리적 연산의 순서가 아니라 논리적인 관계를 나타냅니다.

입력 8개 행이 네 회원 그룹과 최종 두 그룹이 되는 과정

고정 게시글을 시작점으로 둔다. 합계 120 이상을 고른다. 각 단계의 값과 전환 이유를 비교합니다.

QUERY · ROWS → GROUPS

WHERE의 8행을 묶고 HAVING에서 2그룹을 남긴다

고정 fixture의 원본 12행 중 7월 작성·PUBLISHED인 8행만 그룹 입력입니다. 합계 120 이상인 회원 1과 4가 최종 결과에 남습니다.

8개의 입력 행을 네 회원 그룹으로 묶어 합계120 이상인두그룹을 남긴다7월 작성된 공개8행은 회원별 네 그룹의 영역으로 들어갑니다. 회원1 합계165와회원4 합계130은 결과로 연결되고 회원2 합계100과회원3 합계50은 제외됩니다. 실제 물리 실행 계획은 아닙니다.GROUP BY · 4그룹WHERE · 8행원본 12행에서7월 작성PUBLISHED회원1 · 3행SUM 165HAVING 통과회원2 · 2행SUM 100120 미만 · 제외회원3 · 1행SUM 50120 미만 · 제외회원4 · 2행SUM 130HAVING 통과결과 · 2그룹회원 1 · 4합계 120 이상
8개의 입력 행을 네 회원 그룹으로 묶어 합계120 이상인두그룹을 남긴다7월 작성된 공개8행은 회원별 네 그룹의 영역으로 들어갑니다. 회원1 합계165와회원4 합계130은 결과로 연결되고 회원2 합계100과회원3 합계50은 제외됩니다. 실제 물리 실행 계획은 아닙니다.GROUP BY · 4그룹WHERE · 8행원본 12행에서7월 작성PUBLISHED회원1 · 3행SUM 165HAVING 통과회원2 · 2행SUM 100120 미만 · 제외회원3 · 1행SUM 50120 미만 · 제외회원4 · 2행SUM 130HAVING 통과결과 · 2그룹회원 1 · 4합계 120 이상통과 그룹만 결과로

GROUP BY 영역 안의 네 상자는 서로 다른 회원 그룹입니다. 영역으로 들어오는 화살표는 입력8행을 묶는 관계이고, 결과로 나가는 두 화살표는 합계 조건을 통과한 그룹만 선택하는 관계입니다.

행 수에서 그룹 수로 단위가 바뀝니다. 화살표는 논리 관계이며 실제 물리 실행 계획이나 계산 순서를 고정하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 원본부터 HAVING까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목입력 8개 행이 네 회원 그룹과 최종 두 그룹이 되는 과정
제목8개의 입력 행을 네 회원 그룹으로 묶어 합계 120 이상인 두 그룹을 남긴다
관계 설명7월 작성된 공개 8행은 회원별 네 그룹의 영역으로 들어갑니다. 회원 1 합계 165와 회원 4 합계 130은 결과로 연결되고 회원 2 합계 100과 회원 3 합계 50은 제외됩니다. 실제 물리 실행 계획은 아닙니다.
노드·선 표기회원 1 · 3행
노드·선 표기회원 2 · 2행
노드·선 표기회원 3 · 1행
노드·선 표기회원 4 · 2행
설명·조건GROUP BY 영역 안의 네 상자는 서로 다른 회원 그룹입니다. 영역으로 들어오는 화살표는 입력 8행을 묶는 관계이고, 결과로 나가는 두 화살표는 합계 조건을 통과한 그룹만 선택하는 관계입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
원본

고정 게시글을 시작점으로 둔다.

  • 행·회원 그룹 1: 12행
  • 집계 전 행 단위
WHERE

7월 작성·PUBLISHED 행만 선택한다.

  • 행·회원 그룹 1: 8행
  • 그룹 입력 결정
GROUP BY

회원별로 묶고 더한다.

  • 행·회원 그룹 1: 4그룹
  • 행·회원 그룹 2: 회원 1 165/2 100/3 50/4 130
  • 행 수에서 그룹 수로 단위 변경
HAVING

합계 120 이상을 고른다.

  • 행·회원 그룹 1: 2그룹
  • 행·회원 그룹 2: 회원 1·4
  • 논리 선택; 실제 물리 연산 순서 아님

입력 8개 행이 네 회원 그룹과 최종 두 그룹이 되는 과정

고정 게시글을 시작점으로 둔다. 합계 120 이상을 고른다. 각 단계의 값과 전환 이유를 비교합니다.

QUERY · ROWS → GROUPS

WHERE의 8행을 묶고 HAVING에서 2그룹을 남긴다

고정 fixture의 원본 12행 중 7월 작성·PUBLISHED인 8행만 그룹 입력입니다. 합계 120 이상인 회원 1과 4가 최종 결과에 남습니다.

8개의 입력 행을 네 회원 그룹으로 묶어 합계120 이상인두그룹을 남긴다7월 작성된 공개8행은 회원별 네 그룹의 영역으로 들어갑니다. 회원1 합계165와회원4 합계130은 결과로 연결되고 회원2 합계100과회원3 합계50은 제외됩니다. 실제 물리 실행 계획은 아닙니다.GROUP BY · 4그룹WHERE · 8행원본 12행에서7월 작성PUBLISHED회원1 · 3행SUM 165HAVING 통과회원2 · 2행SUM 100120 미만 · 제외회원3 · 1행SUM 50120 미만 · 제외회원4 · 2행SUM 130HAVING 통과결과 · 2그룹회원 1 · 4합계 120 이상
8개의 입력 행을 네 회원 그룹으로 묶어 합계120 이상인두그룹을 남긴다7월 작성된 공개8행은 회원별 네 그룹의 영역으로 들어갑니다. 회원1 합계165와회원4 합계130은 결과로 연결되고 회원2 합계100과회원3 합계50은 제외됩니다. 실제 물리 실행 계획은 아닙니다.GROUP BY · 4그룹WHERE · 8행원본 12행에서7월 작성PUBLISHED회원1 · 3행SUM 165HAVING 통과회원2 · 2행SUM 100120 미만 · 제외회원3 · 1행SUM 50120 미만 · 제외회원4 · 2행SUM 130HAVING 통과결과 · 2그룹회원 1 · 4합계 120 이상통과 그룹만 결과로

GROUP BY 영역 안의 네 상자는 서로 다른 회원 그룹입니다. 영역으로 들어오는 화살표는 입력8행을 묶는 관계이고, 결과로 나가는 두 화살표는 합계 조건을 통과한 그룹만 선택하는 관계입니다.

행 수에서 그룹 수로 단위가 바뀝니다. 화살표는 논리 관계이며 실제 물리 실행 계획이나 계산 순서를 고정하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 원본부터 HAVING까지 진행합니다.

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목입력 8개 행이 네 회원 그룹과 최종 두 그룹이 되는 과정
제목8개의 입력 행을 네 회원 그룹으로 묶어 합계 120 이상인 두 그룹을 남긴다
관계 설명7월 작성된 공개 8행은 회원별 네 그룹의 영역으로 들어갑니다. 회원 1 합계 165와 회원 4 합계 130은 결과로 연결되고 회원 2 합계 100과 회원 3 합계 50은 제외됩니다. 실제 물리 실행 계획은 아닙니다.
노드·선 표기회원 1 · 3행
노드·선 표기회원 2 · 2행
노드·선 표기회원 3 · 1행
노드·선 표기회원 4 · 2행
설명·조건GROUP BY 영역 안의 네 상자는 서로 다른 회원 그룹입니다. 영역으로 들어오는 화살표는 입력 8행을 묶는 관계이고, 결과로 나가는 두 화살표는 합계 조건을 통과한 그룹만 선택하는 관계입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
원본

고정 게시글을 시작점으로 둔다.

  • 행·회원 그룹 1: 12행
  • 집계 전 행 단위
WHERE

7월 작성·PUBLISHED 행만 선택한다.

  • 행·회원 그룹 1: 8행
  • 그룹 입력 결정
GROUP BY

회원별로 묶고 더한다.

  • 행·회원 그룹 1: 4그룹
  • 행·회원 그룹 2: 회원 1 165/2 100/3 50/4 130
  • 행 수에서 그룹 수로 단위 변경
HAVING

합계 120 이상을 고른다.

  • 행·회원 그룹 1: 2그룹
  • 행·회원 그룹 2: 회원 1·4
  • 논리 선택; 실제 물리 연산 순서 아님

조건을 둘 절을 고르는 법

HAVING은 GROUP BY 없이 입력 전체를 한 그룹으로 집계하는 쿼리에서도 쓸 수 있지만, 일반적인 필터를 HAVING에 몰아넣는 습관은 피합니다. WHERE에 둘 수 있는 조건은 원래의 행을 일찍 줄이고 의도도 분명하게 합니다. 반면 집계값이나 그룹별 비율처럼 그룹이 있어야 정의되는 조건은 HAVING에 남깁니다.

HAVING의 기준값은 바로 아래와 같은 값, 바로 위의 그룹을 만들어 >=와 >의 차이를 확인합니다. 비율에 대한 조건이라면 분모가 0인 그룹을 어떻게 다룰지 정하고 NULLIF를 씁니다. HAVING에서 반올림한 표시용 별칭과 비교하면 원래의 정밀도와 기준이 달라지므로, 거르는 데 쓰는 값과 보여 주는 값을 나눕니다.

통계 API에서는 조건의 순서가 권한과도 이어집니다. 소유자나 조직의 범위는 반드시 원래 행의 단계에서 적용해, 다른 사용자의 행이 그룹에 섞이지 않게 합니다. 결과를 가려도 집계값으로 다른 집단의 규모를 짐작할 수 있다는 점도 함께 검토합니다. 대시보드의 월 단위 필터에 시간대를 적용할 때는 서버의 날짜 문자열을 그대로 쓰지 말고 기준 시각으로 바꿉니다.

합계만으로 정렬하면 같은 합계의 그룹끼리 순서가 정해지지 않으므로 그룹의 키를 정렬에 더합니다. LIMIT으로 상위 N개를 고를 때도, 같은 값의 그룹을 모두 넣을지 정확히 N행만 줄지는 제품이 정할 규칙입니다.

논리적 실행 순서는 복잡한 SQL을 디버깅할 때의 공통 언어입니다. 결과가 너무 많으면 WHERE의 모집단과 그룹의 수, HAVING의 기준을 차례로 점검하고, 결과가 비면 같은 순서로 어느 단계에서 0이 되었는지 찾습니다. 나중에 7장 3편에서 EXPLAIN ANALYZE를 읽을 때도 뜻의 단계와 물리적 연산을 나란히 놓고 보면, 최적화 때문에 SQL의 결과가 바뀐다고 오해하지 않습니다.

따져 볼 것질문
행 조건집계 전에 게시글 하나만 보고 판단할 수 있는가
그룹 조건합계나 건수, 평균이 있어야 판단할 수 있는가
모집단필터가 넣고 빼는 행의 집합을 정확히 설명할 수 있는가
이름별칭을 쓰는 범위가 DBMS의 지원과 팀의 기준에 맞는가
순서같은 값의 정렬과 LIMIT이 마지막에 적용되는가

조건을 쓰기 전에 요구 사항의 문장에서 "게시글 가운데"와 "회원별 합계 가운데"를 표시해 봅니다. 앞의 것은 WHERE로, 뒤의 것은 HAVING으로 옮기면 긴 집계 쿼리도 구조가 흔들리지 않습니다.

정리

  • WHERE는 그룹을 만들기 전의 원래 행을, HAVING은 계산된 그룹을 거릅니다.
  • 집계 함수는 WHERE에 쓸 수 없으며, 쓰면 오류 1111(Invalid use of group function)이 납니다.
  • 논리적 실행 순서로 별칭과 집계 함수가 어느 절에서 보이는지 설명할 수 있습니다.
  • 조건은 업무의 뜻을 바꾸지 않는 범위에서 가장 이른 단계에 둡니다.
  • 단계마다 남는 행과 그룹의 수를 기록해 최종 통계를 확인합니다.

연습 문제

1. 건수와 평균으로 고르는 회원

7월에 작성된 글 가운데 조회수가 20회 이상인 행만 대상으로 회원별 총 조회수와 게시글 수를 구하고, 게시글이 두 개 이상이면서 평균 조회수가 45회 이상인 회원만 보여 주세요. 평균 조회수가 높은 순, 회원 id 순으로 정렬합니다.

해설 보기

작성 날짜와 게시글 하나의 조회수는 WHERE에, 게시글 수와 평균 조회수는 HAVING에 두고 두 그룹 조건을 AND로 잇습니다.

SELECT
  author_id,
  COUNT(*) AS post_count,
  SUM(view_count) AS total_views,
  ROUND(AVG(view_count), 1) AS average_views
FROM ch4_having_post_demo
WHERE created_at >= '2026-07-01'
  AND created_at <  '2026-08-01'
  AND view_count >= 20
GROUP BY author_id
HAVING COUNT(*) >= 2
  AND AVG(view_count) >= 45
ORDER BY average_views DESC, author_id;
author_id | post_count | total_views | average_views
4         | 3          | 230         | 76.7
1         | 4          | 265         | 66.3
2         | 2          | 100         | 50.0

이 문제는 공개 상태를 요구하지 않았으므로 7월의 초안인 9번과 숨김 글인 12번도 포함됩니다. 본문의 status = 'PUBLISHED'를 그대로 복사했다면 다른 모집단을 계산한 것입니다. 회원 3은 평균이 50회지만 글이 하나뿐이어서 건수의 조건으로 빠집니다. 이 데이터에는 조회수가 20회 미만인 행이 없으므로, 그 하한은 19회와 20회를 대입해 따로 확인합니다.

2. 기준값 위의 그룹, 같은 그룹

본문의 쿼리에서 HAVING의 기준을 SUM(view_count) >= 130과 SUM(view_count) > 130으로 바꾸면 결과는 각각 어떻게 되나요?

해설 보기

그룹 합계는 회원 1이 165, 회원 2가 100, 회원 3이 50, 회원 4가 130입니다. >= 130이면 합계가 기준과 같은 회원 4도 남아 회원 1과 4의 두 행이 나오고, > 130이면 회원 1 한 행만 나옵니다. 요구 사항의 "이상"과 "초과"를 연산자에 그대로 옮겼는지는, 이처럼 기준값과 같은 그룹을 넣어 시험해야 드러납니다.

3. 빈 입력의 집계와 HAVING

다음 두 쿼리의 결과는 어떻게 다른가요? 이 테이블에는 상태가 ARCHIVED인 행이 없습니다.

SELECT SUM(view_count) AS total_views
FROM ch4_having_post_demo
WHERE status = 'ARCHIVED';

SELECT SUM(view_count) AS total_views
FROM ch4_having_post_demo
WHERE status = 'ARCHIVED'
HAVING COUNT(*) > 0;
해설 보기

첫 쿼리는 GROUP BY 없이 입력 전체를 집계하므로, 입력이 비어 있어도 결과가 한 행 나오고 그 값은 NULL입니다. 둘째 쿼리는 그 한 그룹에 HAVING을 적용하는데, 행의 수가 0이어서 조건이 거짓이 되므로 결과가 아예 없습니다.

total_views
NULL

"보관된 글이 없다"를 NULL 한 행으로 받을지, 빈 결과로 받을지, COALESCE로 0을 보여 줄지는 받는 쪽이 정할 규칙입니다. GROUP BY가 있는 쿼리는 입력이 비면 처음부터 그룹이 없어 결과도 0행이라는 점과 함께 구별해 둡니다.

다음 글 INNER JOIN에서는 나뉘어 저장된 회원과 게시글의 행을 키로 이어, 하나의 질문에 답하는 방법을 다룹니다.