지난 초급 9편에서 바깥 행마다 다시 계산되는 상관 서브쿼리를 배웠습니다. 이번 편의 주인공 EXISTS는 그 상관 서브쿼리가 가장 자연스럽게 쓰이는 자리입니다. 같은 질문을 IN과 EXISTS 양쪽으로 풀어 결과가 똑같음을 확인하고, 실무에서 조용히 사고를 내는 NOT IN + NULL 함정을 직접 재현해 보겠습니다.
SQL·MES 전체 글 보기 →
복습 — "주문한 회원"을 IN으로
초급 8편에서 배운 IN 서브쿼리부터 다시 꺼내 보겠습니다. "주문한 적 있는 회원"을 조회해 볼까요?
SELECT MemberID, Name, City
FROM Member
WHERE MemberID IN (SELECT MemberID FROM Orders)
ORDER BY MemberID;

Orders에 등장하는 MemberID는 1, 2, 3, 5, 6, 8 — 회원 8명 중 6명이 나왔습니다. 김민준(1)은 주문이 3건이지만 결과에는 한 번만 나옵니다. IN은 "목록에 있는가"만 판단하므로 주문 건수와 무관하게 회원당 한 행입니다.
같은 질문을 EXISTS로
이번에는 똑같은 질문을 EXISTS로 풀어 보겠습니다. EXISTS의 문법은 단순합니다 — 괄호 안 서브쿼리가 행을 하나라도 반환하면 TRUE, 한 행도 없으면 FALSE입니다.
SELECT MemberID, Name, City
FROM Member AS M
WHERE EXISTS (SELECT 1 FROM Orders AS O
WHERE O.MemberID = M.MemberID)
ORDER BY MemberID;

결과를 확인해 볼까요? IN으로 풀었을 때와 완전히 같은 6행입니다. 동작 순서는 이렇습니다. 바깥 쿼리가 Member의 행을 하나 잡으면, 서브쿼리가 O.MemberID = M.MemberID인 주문을 찾아봅니다. 한 행이라도 찾는 순간 TRUE가 확정되어 더 뒤지지 않고 멈춥니다. SELECT 1이라고 쓴 이유는 EXISTS가 행의 존재만 볼 뿐 열 내용은 보지 않기 때문입니다(SELECT *도 결과는 같습니다).
두 형태의 변환 규칙은 기계적입니다.
- IN: 바깥열 IN (SELECT 안쪽열 FROM ...)
- EXISTS: EXISTS (SELECT 1 FROM ... WHERE 안쪽열 = 바깥열)
IN에서 비교하던 두 열이, EXISTS에서는 서브쿼리 안쪽의 상관 조건으로 자리를 옮긴 것뿐입니다.
NOT IN의 함정 — 재현 준비
여기까지는 평화롭습니다. 문제는 부정형에서 터집니다. BookMart에 "비회원(게스트) 주문" 기능이 생겼다고 가정해 보겠습니다. 회원이 아닌 손님의 주문은 MemberID가 NULL로 남는 상황입니다. 원본 Orders는 건드리지 않도록, 연습용 복사 테이블 OrdersPractice에 이 시나리오를 만들어 보겠습니다.
SELECT * INTO OrdersPractice FROM Orders;
ALTER TABLE OrdersPractice ALTER COLUMN MemberID int NULL;
INSERT INTO OrdersPractice (OrderID, MemberID, OrderDate, Status)
VALUES (1011, NULL, '2025-07-05 15:20:00', N'주문접수');

복사본의 MemberID는 원본을 따라 NOT NULL로 만들어지기 때문에, 초급 1편에서 배운 ALTER COLUMN으로 NULL 허용으로 바꾼 뒤 게스트 주문 한 건을 넣었습니다. 상태를 확인해 보겠습니다.
SELECT OrderID, MemberID, Status
FROM OrdersPractice
WHERE OrderID >= 1009
ORDER BY OrderID;
OrderIDMemberIDStatus
| 1009 | 8 | 배송중 |
| 1010 | 5 | 주문접수 |
| 1011 | NULL | 주문접수 |
주문 11건 중 1011번의 MemberID가 NULL — 준비 완료입니다.
함정 발동 — 0행이 반환됩니다
이제 "주문하지 않은 회원"을 NOT IN으로 구해 보겠습니다. 주문 이력이 없는 최수아(4)·윤지호(7) 두 명이 나와야 정상입니다.
SELECT MemberID, Name, City
FROM Member
WHERE MemberID NOT IN (SELECT MemberID FROM OrdersPractice)
ORDER BY MemberID;

0행입니다. 최수아도 윤지호도 사라졌습니다. 더 무서운 점은 오류도 경고도 없다는 것입니다. 쿼리는 "성공"했고, 그저 조용히 빈 결과를 돌려줍니다. "주문 안 한 회원이 없나 보다"라고 잘못 결론 내리기 딱 좋은 상황이죠.
왜 0행일까 — 3값 논리로 따져 보기
입문 9편에서 NULL과의 비교는 참도 거짓도 아닌 UNKNOWN이라고 배웠습니다. 그 원리가 여기서 그대로 작동합니다. 서브쿼리가 돌려주는 목록은 중복을 걷어내면 (1, 2, 3, 5, 6, 8, NULL)이고, NOT IN은 내부적으로 이렇게 펼쳐집니다.
MemberID <> 1 AND MemberID <> 2 AND ... AND MemberID <> NULL
최수아(MemberID=4)의 행을 따라가 보겠습니다.
비교결과
| 4 <> 1 | TRUE |
| 4 <> 2 | TRUE |
| 4 <> 3, 5, 6, 8 | 모두 TRUE |
| 4 <> NULL | UNKNOWN |
| 전체 AND | UNKNOWN |
TRUE AND UNKNOWN은 UNKNOWN이고, WHERE절은 TRUE인 행만 통과시킵니다. 최수아·윤지호는 UNKNOWN이라 탈락, 주문한 회원들은 김민준의 1 <> 1 같은 비교에서 FALSE라 탈락 — 결국 모든 행이 탈락해 0행이 되는 것입니다. 목록에 NULL이 단 하나만 있어도 NOT IN 전체가 무력화됩니다.
NOT EXISTS로 올바르게
같은 질문을 NOT EXISTS로 풀어 보겠습니다.
SELECT MemberID, Name, City
FROM Member AS M
WHERE NOT EXISTS (SELECT 1 FROM OrdersPractice AS O
WHERE O.MemberID = M.MemberID)
ORDER BY MemberID;

이번에는 최수아·윤지호 2행이 정확히 나옵니다. NOT EXISTS가 안전한 이유는 비교 방식에 있습니다. 상관 조건 O.MemberID = M.MemberID에서 게스트 주문의 NULL은 어떤 회원 번호와도 같다고 판정되지 않으므로(UNKNOWN → 매칭 안 됨), 그냥 아무에게도 속하지 않는 행으로 취급될 뿐 존재 판정을 뒤흔들지 못합니다.
NOT IN을 꼭 써야 한다면 서브쿼리에서 NULL을 걸러 주면 됩니다.
WHERE MemberID NOT IN (SELECT MemberID FROM OrdersPractice
WHERE MemberID IS NOT NULL)
이렇게 고치면 NOT EXISTS와 같은 2행이 나옵니다.
연습 테이블 정리 — 원상 복구
실습이 끝났으니 복사 테이블을 정리하겠습니다. 원본 Orders와 Member는 한 번도 손대지 않았으므로 BookMart는 처음 그대로입니다.
DROP TABLE OrdersPractice;
확인 삼아 OrdersPractice를 다시 SELECT하면 아래 오류가 납니다(문구는 버전·언어 설정에 따라 다를 수 있습니다). 테이블이 깨끗이 정리되었다는 뜻입니다.
개체 이름 'OrdersPractice'이(가) 잘못되었습니다.
실무에서는 어느 쪽을 쓸까
- 긍정형(IN vs EXISTS): 결과가 같고, SQL Server 옵티마이저도 두 형태를 비슷한 방식으로 처리하는 경우가 많아 성능보다 가독성으로 고르면 됩니다. IN (1, 3, 9)처럼 짧은 고정 목록이면 IN이 읽기 쉽고, "다른 테이블에 존재하는가"라는 의도라면 EXISTS가 뜻이 잘 드러납니다.
- 부정형은 NOT EXISTS가 기본값입니다. NOT IN은 서브쿼리 컬럼이 NULL 허용인지부터 확인해야 하고, 쓰더라도 IS NOT NULL을 습관처럼 붙이세요.
- 존재 여부만 필요한데 JOIN으로 풀면 주문이 3건인 김민준이 3행으로 불어나 DISTINCT가 필요해집니다. EXISTS는 애초에 회원당 한 행이라 그런 고민이 없습니다.
오늘의 정리
- EXISTS는 서브쿼리가 행을 하나라도 반환하는지만 확인하며, 열 내용은 보지 않으므로 SELECT 1로 충분합니다.
- IN에서 비교하던 열을 서브쿼리 안의 상관 조건으로 옮기면 EXISTS가 되고, 결과는 동일합니다.
- NOT IN은 서브쿼리 결과에 NULL이 하나라도 있으면 0행 — <> NULL이 UNKNOWN이 되고 WHERE는 TRUE만 통과시키기 때문입니다.
- 부정 조건은 NOT EXISTS가 안전하며, NOT IN을 쓸 때는 IS NOT NULL을 함께 쓰는 습관이 필요합니다.
- 긍정형은 가독성으로 선택, 부정형은 NOT EXISTS — 이것이 실무 기본 원칙입니다.
다음 편 예고
다음 초급 11편에서는 서브쿼리가 WHERE절을 넘어 어디까지 갈 수 있는지 살펴봅니다. SELECT절의 스칼라 서브쿼리로 회원 목록에 주문 건수를 붙이고, FROM절의 파생 테이블로 집계 결과를 다시 조회하는 기법을 다룹니다.
'SQL · MES' 카테고리의 다른 글
| MSSQL 쿼리 디버깅 — 오류와 잘못된 결과 점검 순서 [MSSQL 28] (0) | 2026.07.16 |
|---|---|
| MSSQL SELECT·FROM·WHERE 서브쿼리 사용법 [MSSQL 27] (0) | 2026.07.16 |
| MSSQL 상관 서브쿼리란? — 바깥 쿼리와 연결 원리 [MSSQL 25] (0) | 2026.07.16 |
| MSSQL 서브쿼리 기초 — 단일행·다중행·중첩 쿼리 [MSSQL 24] (0) | 2026.07.16 |
| MSSQL 여러 열로 그룹화하기 — 다중 열 GROUP BY와 COUNT DISTINCT [MSSQL 23] (0) | 2026.07.16 |