직원마다 “이 사람의 부양가족이 한 명이라도 있는가”만 알고 싶을 때가 있다. 누구인지, 몇 명인지는 필요 없고 있다, 없다만 본다. 직원을 한 명씩 짚으면서 그 사람에게 딸린 행을 찾아보고, 하나라도 나오면 뽑는 것이 EXISTS이고 하나도 안 나와야 뽑는 것이 NOT EXISTS다. “모두 ~이다”는 “~가 아닌 것이 없다”로 바꿔 NOT EXISTS로 쓰는데, 이때 견줄 것이 아예 없는 사람도 함께 뽑힌다는 점을 놓치기 쉽다.
예시로 보기
“부양가족이 없는 직원의 이름을 구하라.” IN은 “쪽지에 있는 값”을 고르는 데 맞고, 여기서 필요한 것은 “딸린 행이 하나도 없다”이다. 이 질문은 이렇게 쓴다.
SELECT Fname, Lname
FROM EMPLOYEE
WHERE NOT EXISTS ( SELECT *
FROM DEPENDENT
WHERE Ssn = Essn );
안쪽 질의의 Ssn은 DEPENDENT에 없는 열이다. 바깥 질의가 지금 짚고 있는 직원의 Ssn이다. 그래서 안쪽 질의는 직원이 바뀔 때마다 다른 답을 낸다. 직원 여덟 명을 차례로 짚으면 이렇다1.
| 바깥에서 짚은 직원 | 안쪽 질의가 돌려준 행 | EXISTS |
NOT EXISTS |
|---|---|---|---|
| Smith | Michael, Alice, Elizabeth | 참 | 거짓 |
| Wong | Alice, Theodore, Joy | 참 | 거짓 |
| Zelaya | 없다 | 거짓 | 참 |
| Wallace | Abner | 참 | 거짓 |
| Narayan | 없다 | 거짓 | 참 |
| English | 없다 | 거짓 | 참 |
| Jabbar | 없다 | 거짓 | 참 |
| Borg | 없다 | 거짓 | 참 |
NOT EXISTS가 참인 다섯 명 Zelaya, Narayan, English, Jabbar, Borg가 결과다. NOT을 떼면 나머지 세 명 Smith, Wong, Wallace가 나온다.
UNIQUE(부질의)는 두 제품 모두 거부한다 — 62_sql-exists_verify.py, 62_sql-exists_verify.sql정의
EXISTS와 NOT EXISTS는 안쪽 질의의 결과가 비어 있는지 아닌지만 본다. 주로 WHERE 절의 조건으로 쓴다.
EXISTS ( Q ): 안쪽 질의Q의 결과에 행이 적어도 하나 있으면 참이다.NOT EXISTS ( Q ):Q의 결과에 행이 하나도 없으면 참이다.
기호로 쓰면, 바깥에서 짚은 행을 \(t\), 그때 안쪽 질의의 결과를 \(Q(t)\)라 할 때 EXISTS는 \(Q(t) \ne \emptyset\), NOT EXISTS는 \(Q(t) = \emptyset\)이다. \(\emptyset\)은 원소가 하나도 없는 빈 집합이다.
돌리는 차례는 이렇다. EMPLOYEE의 행마다 (1) 안쪽 질의를 돌려 조건에 맞는 DEPENDENT의 행을 모두 꺼내고 (2) 결과에 행이 하나라도 있으면(NOT EXISTS는 하나도 없으면) 그 EMPLOYEE의 행을 고른다.
행이 있는지만 보므로 안쪽 질의의 SELECT 뒤에 무엇을 적든 결과는 같다. 그래서 흔히 SELECT *로 쓴다. SELECT Essn으로 바꿔도 위 질의는 같은 다섯 명을 낸다1.
바깥 행마다 다시 돌리는 안쪽 질의
안쪽 질의가 바깥 행의 값을 쓰면, 바깥 행 하나에 안쪽 질의를 한 번씩 돌려야 한다. 위 질의의 Ssn = Essn이 그렇다. 이런 질의를 상관 중첩 질의(correlated nested query)라 부른다2. 중첩 질의와 IN의 첫 질의는 안쪽 질의가 바깥 행의 값을 쓰지 않는다. 그런 쪽지는 한 번만 만들어 두면 된다.
EXISTS의 안쪽 질의가 바깥 행을 쓰지 않으면 모든 바깥 행에 같은 답이 나온다. WHERE EXISTS ( SELECT * FROM DEPENDENT )는 DEPENDENT에 행이 있기만 하면 참이라 직원 8명을 모두 고르고, NOT EXISTS로 바꾸면 0명을 고른다1. Ssn = Essn 같은 연결 조건을 빠뜨리면 이렇게 “전부 아니면 전무”가 된다.
같은 질문을 IN으로도 EXISTS로도 쓸 수 있다. “자기와 이름도 성별도 같은 부양가족이 있는 직원”을 EXISTS로 쓴 판은 이렇다.
SELECT E.Fname, E.Lname
FROM EMPLOYEE AS E
WHERE EXISTS ( SELECT *
FROM DEPENDENT AS D
WHERE E.Ssn = D.Essn AND E.Sex = D.Sex
AND E.Fname = D.Dependent_name );
IN 판에서 E.Ssn IN ( SELECT D.Essn … )으로 맞추던 것을 안쪽 WHERE의 E.Ssn = D.Essn으로 옮겼다. 결과는 IN 판과 같은 0행이다.
“모두”를 NOT EXISTS로
SQL에는 “모든 행이 조건을 지킨다”를 묻는 EXISTS의 짝이 없다. 그래서 “모두 ~이다”를 “~가 아닌 것이 없다”로 바꿔 쓴다. 예를 들어 “성적이 모두 A인 학생”은 “A가 아닌 성적이 없는 학생”으로 쓴다. COMPANY에서 “부양가족이 모두 여성인 직원”을 구하면 이렇다3.
SELECT E.Fname, E.Lname
FROM EMPLOYEE AS E
WHERE NOT EXISTS ( SELECT *
FROM DEPENDENT AS D
WHERE D.Essn = E.Ssn AND D.Sex <> 'F' );
기호로 쓰면, 직원 \(e\)의 부양가족 모음을 \(D_e\)라 할 때 다음 두 식이 같은 말이다. \(\forall\)은 “모든 ~에 대해”, \(\exists\)는 “~가 있다”, \(\neg\)는 “아니다”로 읽는다(술어와 한정기호).
\[\forall d \in D_e\ (\text{Sex}(d) = \text{F}) \iff \neg\,\exists d \in D_e\ (\text{Sex}(d) \ne \text{F})\]결과는 Zelaya, Narayan, English, Jabbar, Borg 다섯 명이다. 그런데 이 다섯은 부양가족이 한 명도 없는 사람들이다. 부양가족이 있는 Smith, Wong, Wallace는 모두 남성 부양가족이 있어 빠졌다. 부양가족이 없으면 “여성이 아닌 부양가족”도 없으므로 조건이 참이 된다. 부양가족이 있는 사람만 원하면 AND EXISTS ( SELECT * FROM DEPENDENT AS D WHERE D.Essn = E.Ssn )을 더한다. 그러면 0명이다.
UNIQUE
참, 거짓을 돌려주는 함수가 하나 더 있다. UNIQUE ( Q )는 Q의 결과에 똑같은 행이 두 번 나오지 않으면 참이다.
SELECT Fname, Lname
FROM EMPLOYEE
WHERE UNIQUE ( SELECT *
FROM DEPENDENT
WHERE Ssn = Essn );
이 문장은 SQLite에서 구문 오류이고, PostgreSQL 16은 “UNIQUE predicate is not yet implemented”라며 거부한다. 어느 제품에서도 적힌 그대로는 실행을 확인하지 못했다. 뜻대로라면 DEPENDENT에는 기본 키가 있어 똑같은 행이 둘일 수 없으니 직원 8명이 모두 뽑혀야 한다. “똑같은 행이 둘 이상인 묶음이 없다”로 바꿔 쓴 판은 8명을 낸다4.
스스로 설명해 보기
1. 첫 질의의 안쪽 WHERE Ssn = Essn에서 Ssn은 어느 표의 열인가? 근거는?
바깥 질의의 EMPLOYEE다. 이름만 쓴 열은 가장 안쪽 표(DEPENDENT)에서 먼저 찾는데, DEPENDENT에는 Ssn이라는 열이 없다. 그래서 한 겹 바깥의 EMPLOYEE에서 찾는다. Essn은 DEPENDENT에 있으니 DEPENDENT의 열이다.
2. Borg 행에서 안쪽 질의의 결과와 NOT EXISTS의 값은?
DEPENDENT에 Essn이 888665555인 행이 없어 결과가 비어 있다. 빈 결과에 대한 NOT EXISTS는 참이라 Borg가 뽑힌다.
3. 안쪽 질의의 SELECT *를 SELECT Essn으로 바꿔도 결과가 같은 까닭은?
EXISTS는 결과에 행이 있는지만 본다. SELECT 뒤는 행마다 어느 열을 보여 줄지만 정하고, 행의 수는 FROM과 WHERE가 정한다.
이 개념의 핵심 아이디어는?
바깥 행 하나를 붙잡아 두고 “이 행에 딸린 행이 있는가”를 묻는다. 안쪽 질의는 바깥 행의 값을 받아 도는 작은 검사다.
같은 생각을 쓰는 다른 상황은?
이중 반복문에서 바깥 값마다 안쪽을 훑어 “하나라도 있으면 멈춘다”를 하는 것(파이썬의 any)과 같다. 주장 CREATE ASSERTION은 “제약을 어기는 행이 없다”를 NOT EXISTS로 적는다. 짝 없는 행을 찾는 일은 외부 조인으로도 한다.
활용
- 고르는 기준: 값을 쪽지와 견주면
IN, 딸린 행이 있는지만 물으면EXISTS, 없어야 하면NOT EXISTS다. - “모두 ~이다”는
NOT EXISTS ( ~가 아닌 것 )으로 쓴다. 견줄 것이 없는 행을 빼려면EXISTS를 하나 더한다. - 흔한 실수: 안쪽 질의에서 바깥 행과 잇는 조건을 빠뜨린다. 오류는 나지 않고 결과가 전부 아니면 전무가 된다.
- 연습: 중첩 질의·집계 연습의 p1(부양가족이 있는 관리자), 복잡한 검색 연습의 p2(성적이 모두 A인 학생), 중첩 질의 쓰기 예제 사다리
연결
- 선수: 중첩 질의와 IN
EXISTS는 “있다”,NOT EXISTS로 쓴 “모두”는 “아닌 것이 없다”이다: 술어와 한정기호- 짝 없는 행을 찾는 다른 길: 외부 조인의
LEFT OUTER JOIN … IS NULL - 제약을
NOT EXISTS로 적기: 주장 CREATE ASSERTION
자주 하는 오해
틀렸다. “성적이 모두 A인 학생”이라는 말을 들으면 A를 받은 적이 있는 학생만 떠올리게 되어 그럴듯해 보인다. 하지만 NOT EXISTS가 보는 것은 “어긴 것이 있는가”뿐이다. 견줄 것이 하나도 없는 행은 어긴 것도 없으니 참이다. 위의 “부양가족이 모두 여성인 직원”에서 뽑힌 다섯 명은 모두 부양가족이 없는 사람이었다. “성적이 모두 A인 학생”도 같다. 성적이 한 줄도 없는 학생을 넣어 보면 NOT EXISTS로 쓴 풀이는 그 학생을 뽑고, 조인과 묶기로 쓴 풀이는 뽑지 않는다(복잡한 검색 연습). 확인하는 법: 딸린 행이 하나도 없는 행을 넣고 돌려 본다. 그런 행을 빼려면 AND EXISTS ( … )를 더한다.
확인 문제
C1 EXISTS ( Q )와 NOT EXISTS ( Q )가 참이 되는 조건을 쓰고, 바깥 행을 쓰는 안쪽 질의를 돌리는 차례를 쓰라.
답: EXISTS는 Q의 결과에 행이 하나라도 있으면 참, NOT EXISTS는 하나도 없으면 참이다. 바깥 표의 행마다 (1) 그 행의 값으로 안쪽 질의를 돌리고 (2) 결과가 비었는지에 따라 그 바깥 행을 고르거나 버린다.
C2 PROJECT의 행이 (ProductX, 1, Bellaire, 5), (ProductY, 2, Sugarland, 5), (ProductZ, 3, Houston, 5), (Computerization, 10, Stafford, 4), (Reorganization, 20, Houston, 1), (Newbenefits, 30, Stafford, 4)이고 열은 차례로 Pname, Pnumber, Plocation, Dnum이다. SELECT D.Dname FROM DEPARTMENT AS D WHERE NOT EXISTS ( SELECT * FROM PROJECT AS P WHERE P.Dnum = D.Dnumber AND P.Plocation = 'Houston' );의 결과는?
답: Administration.
이유: 부서를 하나씩 짚는다. Research(5번)는 ProductZ가 Houston에 있어 안쪽 결과가 1행이다. Headquarters(1번)도 Reorganization 1행이다. Administration(4번)의 프로젝트는 둘 다 Stafford라 안쪽 결과가 비어 있고, NOT EXISTS가 참이 된다3.
C3 (a) WHERE Pnumber IN ( SELECT Pno FROM WORKS_ON, EMPLOYEE WHERE Essn = Ssn AND Lname = 'Smith' ) (b) WHERE NOT EXISTS ( SELECT * FROM DEPENDENT WHERE Ssn = Essn ). 바깥 행마다 안쪽 질의를 다시 돌려야 하는 것은 어느 쪽인가? 또 (b)에서 WHERE Ssn = Essn을 지우면 바깥의 EMPLOYEE 8행 가운데 몇 행이 뽑히는가?
답: (b)다. WHERE Ssn = Essn을 지우면 0행이 뽑힌다.
이유: (a)의 안쪽 질의에 나오는 열은 모두 안쪽 표의 것이라 바깥 행과 상관없이 답이 같다. (b)의 Ssn은 바깥 EMPLOYEE의 열이라 직원마다 답이 다르다. 연결 조건을 지우면 안쪽 질의는 “DEPENDENT에 행이 있는가”가 되어 모든 직원에게 같은 답(있다)을 주고, NOT EXISTS는 모두 거짓이 된다.
C4 “부양가족이 모두 여성인 직원”을 NOT EXISTS ( 여성이 아닌 부양가족 )으로 썼더니 다섯 명이 나왔다. 이 결과가 질문의 뜻과 어긋나 보이는 까닭과 고치는 법을 쓰라.
답: 다섯 명(Zelaya, Narayan, English, Jabbar, Borg)은 모두 부양가족이 한 명도 없는 직원이다. 부양가족이 없으면 “여성이 아닌 부양가족”도 없어서 조건이 참이 된다. 부양가족이 있는 직원만 원하면 AND EXISTS ( 그 직원의 부양가족 )을 더한다. 이 상태에서는 0명이 된다.
이유: “모두 ~이다”는 견줄 것이 하나도 없을 때도 참이다. 논리에서는 이것을 공허한 참이라 부른다.
C5 SELECT Lname FROM EMPLOYEE WHERE Ssn IN ( SELECT Essn FROM DEPENDENT );를 EXISTS로 바꿔 쓰고 결과를 쓰라.
답: SELECT Lname FROM EMPLOYEE WHERE EXISTS ( SELECT * FROM DEPENDENT WHERE Ssn = Essn ); 결과는 Smith, Wong, Wallace.
이유: IN이 바깥의 Ssn과 안쪽 결과의 Essn을 견주던 것을, 안쪽 WHERE의 Ssn = Essn으로 옮긴다. 그러면 안쪽 결과가 “이 직원의 부양가족”이 되고, 하나라도 있으면 뽑힌다.