백엔드 데이터 인프라 31편 — 쿼리 종합 WHERE GROUP BY HAVING ORDER BY

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 31편. 쿼리 절의 종합 깊이 — WHERE·GROUP BY·HAVING·ORDER BY·LIMIT 운영 함정과 최적 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 31편 — 쿼리 종합 WHERE GROUP BY HAVING ORDER BY

이 글은 백엔드 데이터 인프라 시리즈 70편 중 31편이에요. 29편 SELECT 절 ·30편 FROM 절 까지 SELECT 9절 중 2개 깊이. 이번 31편은 나머지 — WHERE·GROUP BY·HAVING·ORDER BY·LIMIT 종합.

WHERE — 행 필터의 깊이

WHERE 조건1 AND 조건2 OR 조건3

비교 12가지

=  <>  !=  >  <  >=  <=                  -- 표준
IS NULL  IS NOT NULL                      -- NULL
IS DISTINCT FROM  IS NOT DISTINCT FROM    -- NULL safe
BETWEEN a AND b                            -- 범위
IN (...)                                   -- 여러 값
LIKE / NOT LIKE                            -- 패턴
ILIKE / NOT ILIKE                          -- PG 대소문자 무시
SIMILAR TO                                 -- SQL 표준 패턴
~ / ~*  / !~ / !~*                         -- PG 정규식

인덱스 활용 조건

WHERE 가 "인덱스 가능한 형태" 인지에 따라 성능이 갈린다.

WHERE col = 1               -- ✅ 인덱스 사용 (Equality)
WHERE col > 100             -- ✅ Range 스캔
WHERE col IN (1, 2, 3)      -- ✅
WHERE col LIKE 'A%'         -- ✅ Prefix 매칭
WHERE col LIKE '%abc'       -- ❌ 풀스캔
WHERE LOWER(col) = 'abc'    -- ❌ 함수 인덱스 필요
WHERE col + 1 = 100         -- ❌ 표현식 — 인덱스 X

36편 인덱스 에서 깊이.

단축 회로

WHERE col1 = 1 AND col2 = 2
-- PG = col1 조건 먼저 평가, col1 가 false 면 col2 안 평가 (단축)

성능 면에서는 "가장 선별적인 조건을 먼저" 두는 게 정석이지만, PG 계획자가 알아서 최적화한다.

GROUP BY 깊이

표준 GROUP BY

SELECT category, COUNT(*), AVG(price)
FROM products
GROUP BY category;

SELECT 의 비집계 컬럼은 GROUP BY 에 모두 들어가야 한다.

여러 컬럼

SELECT country, city, COUNT(*)
FROM users
GROUP BY country, city;
-- 또는 GROUP BY 1, 2 (위치)

ROLLUP — 계층 집계

SELECT country, city, SUM(amount)
FROM orders
GROUP BY ROLLUP (country, city);

결과:

country  | city   | sum
---------+--------+------
Korea    | Seoul  | 1000   ← Korea·Seoul
Korea    | Busan  |  500   ← Korea·Busan
Korea    | NULL   | 1500   ← Korea 소계
Japan    | Tokyo  |  800
Japan    | NULL   |  800   ← Japan 소계
NULL     | NULL   | 2300   ← 전체 합계

소계와 합계가 한 쿼리에 나온다.

CUBE — 모든 조합

GROUP BY CUBE (category, brand);
-- (category, brand) + (category) + (brand) + ()

ROLLUP 보다 차원이 더 넓다. 말 그대로 데이터 큐브.

GROUPING SETS — 명시적

GROUP BY GROUPING SETS (
    (category, brand),
    (category),
    (region),
    ()
);

원하는 그룹 조합만 골라서 집계한다.

GROUPING() 함수

SELECT
    category, brand,
    GROUPING(category) AS g_cat,
    GROUPING(brand) AS g_brand,
    SUM(amount)
FROM orders
GROUP BY ROLLUP (category, brand);

GROUPING(col) 은 "이 행의 그 컬럼이 ROLLUP 의 NULL 인가" 를 알려줘 소계·합계를 구분해준다.

HAVING — 그룹 필터

SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 10
   AND SUM(amount) > 1000000;

WHERE 와 차이: - WHERE = 그룹 전 (인덱스 활용 가능) - HAVING = 그룹 후 (집계 결과 필터)

— WHERE 로 박을 수 있으면 WHERE 로 박는다.

-- 비효율
SELECT user_id, COUNT(*) FROM orders
GROUP BY user_id
HAVING user_id > 100;     -- ❌ user_id 필터를 HAVING에

-- 효율
SELECT user_id, COUNT(*) FROM orders
WHERE user_id > 100        -- ✅
GROUP BY user_id;

ORDER BY 깊이

ORDER BY col1 ASC NULLS LAST, col2 DESC, ...

인덱스 활용

CREATE INDEX idx_orders_created ON orders(created_at DESC);

SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- 인덱스 그대로 사용 — 정렬 비용 0

ORDER BY 와 같은 방향의 인덱스를 두면 정렬 비용이 0이 된다. 운영의 표준 패턴.

다중 컬럼 인덱스

CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);

SELECT * FROM orders
WHERE user_id = 1
ORDER BY created_at DESC LIMIT 10;
-- 인덱스 그대로

WHEREORDER BY 가 한 인덱스에 같이 들어가면 가장 빠르다.

LIMIT·OFFSET 깊이

LIMIT 10 OFFSET 1000   -- 1010번째까지 스캔 후 처음 1000개 버림 — 느림

커서 페이지네이션

-- 첫 페이지
SELECT * FROM orders WHERE user_id = 1
ORDER BY id ASC LIMIT 10;
-- 마지막 id = 100

-- 다음 페이지
SELECT * FROM orders WHERE user_id = 1 AND id > 100
ORDER BY id ASC LIMIT 10;

OFFSET 에 의존하지 않으니 속도가 일정하다. 대용량에서 권장.

키셋 페이지네이션 (커서 + 다중 컬럼)

WHERE (created_at, id) > ('2026-05-17 10:00:00', 100)
ORDER BY created_at, id LIMIT 10;

(a, b) > (a0, b0) 는 행 비교라서 효율적이다.

DISTINCT 와 GROUP BY

SELECT DISTINCT category FROM products;
-- ≡ SELECT category FROM products GROUP BY category;

둘은 거의 같다. 가독성은 DISTINCT 가 낫고 PG 계획자가 만드는 실행 계획도 비슷하다.

집합 연산 종합

28편 쿼리 개요 의 집합 연산 깊이.

-- 합집합 (중복 제거)
SELECT user_id FROM premium UNION SELECT user_id FROM vip;

-- 합집합 (빠름)
SELECT user_id FROM premium UNION ALL SELECT user_id FROM vip;

-- 교집합
SELECT user_id FROM active INTERSECT SELECT user_id FROM premium;

-- 차집합
SELECT user_id FROM users EXCEPT SELECT user_id FROM banned;

규칙: - 컬럼 수·타입 같아야 - ORDER BY 는 마지막 한 번 - 인덱스 활용 가능 시 빠름

운영 표준 — 페이지네이션

-- ❌ 옛 스타일 — OFFSET 깊으면 폭주
SELECT * FROM orders
WHERE user_id = 1
ORDER BY created_at DESC
LIMIT 20 OFFSET 1000;

-- ✅ 커서 페이지네이션
SELECT * FROM orders
WHERE user_id = 1 AND created_at < $cursor
ORDER BY created_at DESC
LIMIT 20;

Spring 백엔드에서는 Pageable(Spring Data 의 페이지 요청 인터페이스) 대신 "마지막 created_at 을 다음 요청 cursor 로" 넘기는 방식을 쓴다.

함정 5가지

(1) HAVING 에 WHERE 가능 조건

HAVING category = 'A'   -- ❌
WHERE category = 'A' ... GROUP BY ... HAVING SUM(...) > 100   -- ✅

(2) ORDER BY 의 인덱스 방향 불일치

CREATE INDEX idx ON orders(created_at ASC);
SELECT ... ORDER BY created_at DESC;   -- 인덱스 역방향 스캔 (가능하지만 일부 느림)

자주 쓰는 방향에 맞춰 인덱스를 만든다.

(3) GROUPING SETS 미활용

대시보드가 복잡해지면 SELECT 를 여러 번 조합하기 쉬운데, 한 SQL 안에서 GROUPING SETS 로 묶으면 N배 빨라진다.

(4) ROLLUP NULL 혼동

ROLLUP 의 NULL 은 "소계·합계 행" 을 가리킨다. 원본 NULL 과 헷갈리지 않도록 GROUPING() 함수로 구분한다.

(5) UNION DISTINCT 비용

UNION       -- 중복 제거 (해시 또는 정렬 필요 — 비용)
UNION ALL   -- 중복 안 제거 (빠름)

중복이 없다고 확신할 수 있으면 UNION ALL.

🎯 쿼리 절 5룰

(1) WHERE 인덱스 활용. (2) GROUP BY = ROLLUP/CUBE/GROUPING SETS 한 SQL 다차원. (3) HAVING ≠ WHERE. (4) ORDER BY 인덱스 방향 일치. (5) 커서 페이지네이션 — OFFSET 회피.

한 줄 정리 — WHERE 인덱스 활용·GROUP BY ROLLUP/CUBE/GROUPING SETS 다차원·HAVING 그룹 후 필터·ORDER BY 인덱스 방향·LIMIT 커서. 페이지네이션은 OFFSET 대신 커서. UNION ALL 빠름.

시험 직전 한 번 더 — 쿼리 절 종합 입문자가 매번 헷갈리는 것

  • WHERE 비교 12가지 = =·<>·>·<·>=·<=·IN·BETWEEN·LIKE·ILIKE·SIMILAR TO·~·IS NULL
  • IS DISTINCT FROM = NULL safe
  • 인덱스 활용 = =·<·>·LIKE 'A%'·IN
  • 인덱스 X = LIKE '%X'·LOWER(col)·col + 1
  • GROUP BY = 비집계 컬럼 모두
  • ROLLUP = 계층 소계·합계
  • CUBE = 모든 차원 조합
  • GROUPING SETS = 명시적 조합
  • GROUPING(col) = ROLLUP NULL 구분
  • HAVING = 그룹 후 필터
  • WHERE 가능하면 WHERE (인덱스)
  • ORDER BY + 인덱스 = 정렬 비용 0
  • 인덱스 방향 일치 중요
  • LIMIT OFFSET 깊으면 폭주
  • 커서 페이지네이션 = WHERE id > cursor LIMIT N
  • 다중 컬럼 키셋 = (a, b) > (a0, b0)
  • DISTINCT ≈ GROUP BY
  • UNION ALL = 빠름 (중복 제거 X)
  • INTERSECT·EXCEPT = 교집합·차집합
  • 집합 연산 = 컬럼 수·타입 일치
  • FILTER 집계 = COUNT(*) FILTER (WHERE ...)
  • WHERE 단축 회로 = 선별적 조건 먼저
  • 자주 쓰는 ORDER 방향 = 인덱스 같은 방향
  • Spring Pageable = 커서로 대체 권장
  • 운영 페이지네이션 = WHERE col > $last 표준

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!