왜 DISTINCT가 예상대로 안 될까? 큰일나기 전에 알아두기
"분명히 SELECT DISTINCT를 썼는데 왜 중복이 그대로 나오지?" SAP HANA 콘솔 앞에서 이런 의문을 가져본 적 있다면, 이 글은 정확히 그 답을 다룹니다. DISTINCT는 SQL 입문자가 가장 먼저 배우는 키워드 중 하나지만, 동시에 가장 많이 오해받는 키워드이기도 합니다. 특히 SAP HANA처럼 대용량 트랜잭션 데이터를 다루는 환경에서는 DISTINCT의 잘못된 사용이 잘못된 KPI 리포트, 회계 마감 오류, 재고 불일치로 이어질 수 있습니다.
이 글에서 확인할 내용은 다음과 같습니다.
- DISTINCT가 내부적으로 어떻게 행을 비교하는지 (해시 vs 정렬 기반)
- NULL이 끼었을 때 DISTINCT가 어떻게 동작하는지
- 다중 컬럼 DISTINCT가 "컬럼별 중복 제거"가 아닌 이유
- ORDER BY, GROUP BY, 집계함수와 혼용할 때 생기는 함정
- SAP HANA에서 DISTINCT 대신 GROUP BY를 써야 하는 시점
DISTINCT의 동작 원리 — 행 전체를 비교한다
가장 먼저 짚고 넘어가야 할 사실은, DISTINCT는 "컬럼"이 아니라 "SELECT 목록에 나열된 모든 컬럼의 조합"을 단위로 중복을 제거한다는 점입니다. 많은 입문자가 SELECT DISTINCT customer_id, order_date FROM SalesOrder를 "customer_id의 중복을 제거한다"라고 읽지만, 실제로는 "(customer_id, order_date) 튜플의 중복을 제거"합니다.
SAP HANA는 컬럼 저장(column store) 기반이므로 DISTINCT 연산을 처리할 때 일반적으로 다음 두 가지 전략 중 하나를 선택합니다.
- 해시 기반 DISTINCT: 각 행의 SELECT 컬럼 조합에 해시값을 계산해 해시 테이블에 저장. 중복된 해시는 제거. 정렬이 필요 없을 때 효율적입니다.
- 정렬 기반 DISTINCT: ORDER BY가 함께 있거나 인덱스를 활용할 수 있을 때, 정렬된 결과에서 연속된 동일 행을 제거.
비유하자면, DISTINCT는 "사진 속 인물 중에서 옷·머리·표정이 완전히 똑같은 사람만 한 명만 남기는" 작업입니다. 옷만 같다고 같은 사람이 아니듯이, 일부 컬럼만 같다고 중복으로 처리되지 않습니다.
핵심: DISTINCT는 SELECT 절에 나열한 컬럼 집합 전체를 비교 키로 사용합니다. 이를 "row-level distinct"라고도 부릅니다.
착각 케이스 1 — NULL은 같은 값으로 취급된다
일반적인 SQL 비교 연산에서 NULL = NULL은 UNKNOWN을 반환합니다. 그래서 많은 개발자가 "DISTINCT를 써도 NULL 행은 여러 개 남겠지"라고 생각합니다. 그러나 DISTINCT는 NULL을 동일한 값으로 간주해 중복 제거합니다. SAP HANA를 포함한 대부분의 SQL 엔진이 이렇게 동작합니다.
-- Invoice 테이블에 supplier_code가 NULL인 행이 5건 있다고 가정
SELECT DISTINCT supplier_code
FROM Invoice;
-- 결과 예시:
-- supplier_code
-- --------------
-- SUP-1001
-- SUP-1002
-- NULL <- 5건이 1건으로 합쳐짐
반대로 WHERE supplier_code = NULL은 절대 매칭되지 않고, WHERE supplier_code IS NULL을 써야 한다는 점과 대비됩니다. 이 일관성 없는 NULL 처리가 혼란의 원천입니다.
착각 케이스 2 — 다중 컬럼 DISTINCT는 "컬럼별"이 아니다
실제 컨설팅 현장에서 자주 마주치는 잘못된 코드입니다.
-- 의도: 고객별로 한 번씩, 그리고 주문일자별로 한 번씩 보고 싶다
SELECT DISTINCT customer_id, order_date, total_amount
FROM SalesOrder
WHERE order_date BETWEEN '2026-01-01' AND '2026-03-31';
이 쿼리를 "고객 중복 제거 + 일자 중복 제거"로 읽으면 큰일입니다. 실제로는 (customer_id, order_date, total_amount) 세 컬럼이 동시에 같은 행만 중복으로 봅니다. 같은 고객이 같은 날 금액이 1원이라도 다르면 모두 별도 행으로 남습니다.
고객별로 유니크하게 한 명씩 뽑고 싶다면 다음과 같이 써야 합니다.
-- 고객 ID만 유니크하게
SELECT DISTINCT customer_id
FROM SalesOrder;
-- 고객별 최신 주문일자가 필요하면 집계로 접근
SELECT customer_id, MAX(order_date) AS last_order_date
FROM SalesOrder
GROUP BY customer_id;
착각 케이스 3 — ORDER BY, GROUP BY, 윈도우 함수와 혼용
DISTINCT는 SELECT 절이 정해진 후에 적용되는 연산이기 때문에, ORDER BY나 윈도우 함수와 함께 쓸 때 예상치 못한 결과가 나옵니다.
-- 위험한 패턴: DISTINCT + ORDER BY (집계 컬럼 참조)
SELECT DISTINCT product_id, category_id
FROM Product
ORDER BY price DESC; -- 오류: price는 SELECT 목록에 없음
SAP HANA에서 이런 쿼리는 일반적으로 오류를 반환하거나, 모호한 결과를 만듭니다. DISTINCT가 적용된 결과 집합에는 price 컬럼이 존재하지 않기 때문입니다. 또한 윈도우 함수(ROW_NUMBER(), RANK())와 DISTINCT를 함께 쓰면, 윈도우 함수가 먼저 계산되어 모든 행에 고유 번호가 매겨진 뒤 DISTINCT가 실행되므로 중복 제거 효과가 사라집니다.
-- 의도와 다르게 모든 행이 살아남는 케이스
SELECT DISTINCT
customer_id,
ROW_NUMBER() OVER (ORDER BY order_date) AS rn
FROM SalesOrder;
-- rn이 모든 행마다 다르므로 사실상 DISTINCT는 무효
실전 예제: SAP HANA에서 DISTINCT 올바르게 쓰기
이제 실제 시나리오 3단계로 DISTINCT를 안전하게 다루는 방법을 살펴봅니다. 환경은 SAP HANA Cloud 2026 QRC2 또는 HANA 2.0 SPS 07 이상을 기준으로 합니다.
1단계 — 기본: 유니크한 카테고리 목록
-- Product 테이블에서 사용 중인 카테고리 코드만 추출
SELECT DISTINCT category_code
FROM Product
WHERE is_active = 'X';
2단계 — 실무: 중복 주문 의심 건 탐지 (로깅 포함)
-- 같은 고객이 같은 날 동일 금액으로 여러 번 주문한 의심 케이스
WITH suspect AS (
SELECT customer_id, order_date, total_amount, COUNT(*) AS dup_cnt
FROM SalesOrder
GROUP BY customer_id, order_date, total_amount
HAVING COUNT(*) > 1
)
SELECT s.customer_id,
s.order_date,
s.total_amount,
s.dup_cnt,
CURRENT_TIMESTAMP AS detected_at
FROM suspect s
ORDER BY s.dup_cnt DESC, s.order_date DESC;
여기서 핵심은 DISTINCT 대신 GROUP BY + HAVING COUNT(*) > 1로 "몇 건이 중복되었는가"라는 정보까지 함께 얻었다는 점입니다. DISTINCT는 중복을 숨기지만, GROUP BY는 중복을 측정합니다.
3단계 — 프로덕션: 대용량 테이블에서 DISTINCT 성능 최적화
-- 1억 건 규모 SalesOrder에서 활성 고객 ID만 추출
-- (1) DISTINCT 방식
SELECT DISTINCT customer_id
FROM SalesOrder
WHERE order_date >= ADD_MONTHS(CURRENT_DATE, -12);
-- (2) GROUP BY 방식 (HANA에서 일반적으로 동등 성능, 때로 더 유리)
SELECT customer_id
FROM SalesOrder
WHERE order_date >= ADD_MONTHS(CURRENT_DATE, -12)
GROUP BY customer_id;
-- (3) 인덱스/파티션이 잘 설계된 경우 추천 패턴
SELECT customer_id
FROM SalesOrder
WHERE order_date >= ADD_MONTHS(CURRENT_DATE, -12)
GROUP BY customer_id
HAVING COUNT(*) > 0;
HANA 옵티마이저는 DISTINCT와 GROUP BY를 내부적으로 비슷한 실행 계획으로 변환하는 경우가 많지만, EXPLAIN PLAN FOR ...으로 실제 비용을 확인하는 습관이 권장됩니다.
EXPLAIN PLAN FOR
SELECT DISTINCT customer_id FROM SalesOrder;
SELECT * FROM EXPLANATION_PLAN_TABLE
WHERE STATEMENT_NAME IS NULL
ORDER BY OPERATOR_ID;
흔한 실수와 트러블슈팅 FAQ
Q1. DISTINCT를 썼는데 여전히 중복으로 보이는 행이 있어요.
SELECT 목록을 다시 확인하세요. 보이지 않는 컬럼(예: 타임스탬프, ID, 자동 증가 컬럼)이 포함되어 있으면 "사람 눈에는 같지만 DB에는 다른 행"이 됩니다. 또한 문자열 컬럼 끝에 보이지 않는 공백·탭이 있으면 다른 값으로 취급됩니다. TRIM()이나 UPPER()로 정규화 후 DISTINCT를 적용하세요.
Q2. DISTINCT 쿼리가 너무 느려요.
DISTINCT는 전체 결과 집합을 메모리에 올려 비교해야 하므로 대용량 테이블에서 비싸집니다. 필터링(WHERE)을 먼저 강하게 걸어 데이터 양을 줄이고, 가능하면 인덱스가 있는 단일 컬럼에만 DISTINCT를 적용하세요. SAP HANA 컬럼 스토어는 단일 컬럼 DISTINCT에 매우 강하지만, 여러 컬럼이 섞이면 해시 비용이 급증합니다.
Q3. DISTINCT와 COUNT(DISTINCT col)는 같나요?
거의 같지만 NULL 처리에서 다릅니다. COUNT(DISTINCT col)은 NULL을 카운트에서 제외합니다. 반면 SELECT DISTINCT col의 결과 행 수에는 NULL 행이 1건 포함됩니다. 따라서 COUNT(*) FROM (SELECT DISTINCT col FROM t)와 COUNT(DISTINCT col) FROM t는 NULL이 있는 컬럼에서 1만큼 차이날 수 있습니다.
DISTINCT vs GROUP BY — 언제 무엇을 쓸까
| 상황 | 권장 | 이유 |
|---|---|---|
| 단순히 유니크 목록만 필요 | DISTINCT | 의도가 명확하고 읽기 쉬움 |
| 중복 개수, 합계 등 집계 필요 | GROUP BY | HAVING, 집계함수 활용 가능 |
| 성능 튜닝 여지가 필요 | GROUP BY | 옵티마이저 힌트, 인덱스 활용도 일반적으로 더 유연 |
| NULL 그룹을 분리 처리해야 함 | GROUP BY + COALESCE | NULL을 별도 토큰으로 치환 가능 |
실무 권장 사항으로, "표시용 유니크 목록"은 DISTINCT, "분석/리포팅용 유니크 집합"은 GROUP BY를 기본으로 삼고, 두 방식 모두 EXPLAIN PLAN으로 검증하는 습관을 들이는 것이 좋습니다.
핵심 정리와 더 알아볼 주제
DISTINCT는 단순한 키워드처럼 보이지만, SELECT 목록 전체를 키로 사용하고 NULL을 동일 값으로 취급하며 ORDER BY·윈도우 함수와의 상호작용에서 함정이 많은 연산자입니다. 다음 3가지만 기억하면 큰 사고를 막을 수 있습니다.
- DISTINCT는 "컬럼별"이 아니라 "행 전체" 중복 제거다.
- NULL은 DISTINCT 입장에서는 같은 값이다.
- 중복 측정·집계가 필요하면 GROUP BY로 갈아타라.
더 깊이 학습하고 싶다면 SAP HANA의 EXPLAIN PLAN과 PlanViz, UNION vs UNION ALL의 중복 제거 차이, ROW_NUMBER() OVER (PARTITION BY ...)를 활용한 "그룹 내 첫 행만 남기기" 패턴, 그리고 SAP HANA Calculation View에서 DISTINCT 처리를 모델링하는 방법을 추천합니다.
댓글 0
아직 댓글이 없습니다.