본문 바로가기

DB

DISTINCT 대신 EXISTS가 유리한 경우

 

DISTINCT 대신 EXISTS가 유리한 경우?

SQL을 작성하다 보면 중복 제거를 위해 DISTINCT를 사용하는 경우가 많다. 그런데 때로는 EXISTS 서브쿼리를 사용하는 것이 성능이나 가독성 측면에서 더 유리할 수 있다. 이번 글에서는 두 방식의 차이를 정리하고, 어떤 상황에서 EXISTS가 더 적합한지를 테스트해보기로 하였다.

 

 

 

언제 DISTINCT를 EXISTS로 대체할 수 있을까?

DISTINCT는 보통 JOIN 후 중복된 결과를 제거하기 위해 사용된다다. 반면 EXISTS는 서브쿼리를 이용해

"조건을 만족하는 행이 존재하는가" 여부만 체크하므로, JOIN 없이도 동일한 효과를 얻을 수 있는 경우가 있다.

 

 

 

EXISTS가 성능적으로 유리한 시나리오

아래 조건에 해당한다면 DISTINCT + JOIN보다 EXISTS가 더 빠를 가능성이 크다.

1. 서브쿼리 대상 테이블이 큰 경우

EXISTS는 첫 일치 행을 찾으면 바로 반환되므로 전체 스캔이 필요 없다. 반면 JOIN은 모든 매칭 결과를 생성하고, 이후 DISTINCT로 중복 제거해야 하므로 리소스를 더 많이 소모한다.

 

2. 조인 결과가 중복을 많이 생성하는 경우

다대다 관계에서 JOIN은 중간 결과가 매우 커질 수 있다. 이 경우 DISTINCT가 정렬(SORT) 또는 해싱(HASH) 연산을 통해 중복 제거를 시도하는데 상당한 비용이 발생할 수 있다.

 

3. 인덱스가 잘 구성되어 있는 경우

EXISTS는 보통 서브쿼리의 WHERE 조건에서 인덱스를 잘 활용할 수 있다.
예를 들어 WHERE EXISTS (SELECT 1 FROM Orders O WHERE O.CustomerID = C.CustomerID) 조건에 대해 O.CustomerID에 인덱스가 걸려 있다면 매우 빠르게 평가된다.

 

4. 연결된 테이블의 컬럼을 조회할 필요가 없을 때

JOIN은 관련된 모든 데이터를 결합하기 때문에 불필요한 데이터까지 끌고 오게 된다. 하지만 EXISTS는 존재 여부만 체크하므로 불필요한 I/O를 줄일 수 있다.

 

 

 

샘플 데이터 생성

 

실행계획을 비교하기 위해 아래의 쿼리를 이용하여 테스트 데이터를 생성하였다.

-- 1. 고객 테이블 생성
CREATE TABLE Customers (
  CustomerID INT PRIMARY KEY,
  CustomerName VARCHAR(100)
);

-- 2. 주문 테이블 생성
CREATE TABLE Orders (
  OrderID INT PRIMARY KEY,
  CustomerID INT,
  OrderDate DATE,
  FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID)
);

-- 3. 인덱스 생성
CREATE INDEX IDX_Orders_CustomerID ON Orders(CustomerID);

-- 4. 고객 데이터 삽입
INSERT ALL
  INTO Customers (CustomerID, CustomerName) VALUES (1, 'Alice')
  INTO Customers (CustomerID, CustomerName) VALUES (2, 'Bob')
  INTO Customers (CustomerID, CustomerName) VALUES (3, 'Charlie')
  INTO Customers (CustomerID, CustomerName) VALUES (4, 'David')
  INTO Customers (CustomerID, CustomerName) VALUES (5, 'Eve')
  INTO Customers (CustomerID, CustomerName) VALUES (6, 'Frank')
  INTO Customers (CustomerID, CustomerName) VALUES (7, 'Grace')
  INTO Customers (CustomerID, CustomerName) VALUES (8, 'Heidi')
  INTO Customers (CustomerID, CustomerName) VALUES (9, 'Ivan')
  INTO Customers (CustomerID, CustomerName) VALUES (10, 'Judy')
SELECT * FROM dual;

-- 5. 주문 데이터 삽입
INSERT INTO Orders (OrderID, CustomerID, OrderDate)
SELECT 1000 + LEVEL, MOD(LEVEL, 6) + 1, SYSDATE - MOD(LEVEL, 30)
FROM dual CONNECT BY LEVEL <= 100000;

 

 

예제 비교

 

1. DISTINCT + JOIN 방식

-- 1. DISTINCT + JOIN 방식
EXPLAIN plan FOR
SELECT DISTINCT C.CustomerName
FROM Customers C
JOIN Orders O ON C.CustomerID = O.CustomerID;

SELECT * FROM TABLE(dbms_xplan.display);

 

 

중간 결과가 많아 HASH UNIQUE로 중복 제거하였고 TEMP 공간을 소모하였다. 예상 Cost와 Time도 높게 나왔다.

 

2. EXISTS 방식

-- 2. EXISTS 방식
EXPLAIN plan FOR
SELECT C.CustomerName
FROM Customers C
WHERE EXISTS (
  SELECT 1 FROM Orders O
  WHERE O.CustomerID = C.CustomerID
);

SELECT * FROM TABLE(dbms_xplan.display);

 

 

옵티마이저가 EXISTS를 SEMI JOIN으로 최적화하여 조건 충족 시 바로 평가 종료하였기 때문에 Distinct를 사용한 결과보다 빠른 성능을 보여주었다. 불필요한 연산이 없기 때문이다.

 

 

항목 Distinct + Join Extist
중간 조인 결과 수 많음 (93,874건) 없음
중복 제거 방식 HASH UNIQUE 존재 여부만 확인
TEMP 공간 사용 7.5MB 없음
인덱스 활용 보조적 적극 활용
총 Cost 1709 3
예상 실행 시간 21초 1초 미만

 

 

 

주의사항

  • EXISTS가 항상 빠른 것은 아니다.
  • 데이터 양, 인덱스 구성, 옵티마이저 전략에 따라 다르므로 실행 계획(EXPLAIN PLAN) 확인이 중요.
  • 경우에 따라서는 JOIN + GROUP BY나 IN, SEMI JOIN이 더 나은 성능을 낼 수도 있다. (다음에 정리해보기로)

 

 

결론

구분 DISTINCT + JOIN EXISTS
사용 목적 중복 제거 존재 여부 판단
중간 결과 크기 클 수 있음 작음
인덱스 활용 일부만 활용 WHERE 조건에 따라 효율적
추천 상황 비교적 소량 데이터, JOIN 컬럼이 필요한 경우 대용량 데이터, JOIN 대상 컬럼이 필요 없는 경우

 

 

DISTINCT와 JOIN을 함께 사용하는 방식은 자주 사용되는 패턴이다.

그러나 데이터 양이 많거나 조인 결과에 중복이 많이 발생하는 구조에서는 성능 저하가 크게 발생할 수 있다. 특히 다대다 관계에서는 조인 결과가 기하급수적으로 증가하며, 이로 인해 DISTINCT는 중복 제거를 위한 정렬(SORT) 또는 해시(HASH UNIQUE) 연산을 수행하게 되고, 이는 TEMP 공간 사용 증가와 함께 전체 쿼리 비용을 상승시킨다.

 

반면 EXISTS는 조건을 만족하는 레코드의 존재 여부만 판단하는 방식으로 동작하며, 다음과 같은 특징을 가진다.

  • 조인 결과와 같은 중간 집합을 만들지 않는다.
  • 조건을 만족하는 첫 번째 행만 발견하면 평가를 종료한다.
  • 인덱스가 적절히 구성되어 있다면 매우 빠르게 수행된다.

Oracle에서는 EXISTS 구문을 내부적으로 NESTED LOOPS SEMI JOIN으로 최적화하며 이는 "존재하는가?"를 판단할 때 가장 효율적인 방식이다.

그러므로 JOIN 후 DISTINCT로 중복 제거를 하고 있다면 동일한 결과를 EXISTS로 얻을 수 있는지 확인해보는 것이 좋다.

특히 조인 대상 테이블의 컬럼이 SELECT 절에 포함되지 않고, 단순히 존재 여부만 필요한 경우라면 EXISTS가 훨씬 적합하다.

 

물론 가장 중요한 것은, 쿼리 최적화 여부를 확인할 때엔 항상 실행 계획(EXPLAIN PLAN)을 통해 비교하고 판단해야 한다는 것이다. 이론보다 실제 실행 계획이 더욱 정확한 기준이 되어야 하기 때문이다.