Skip to Content
독학사독학사 4단계데이터베이스11. SQL 중급: 조인·중첩 질의·집합 연산

이번 문서의 목표: 이 파일을 다 읽으면 여러 테이블에 흩어진 데이터를 조인으로 다시 합치고, 서브쿼리로 조건을 중첩하고, 두 질의 결과를 집합 연산으로 비교하는 SQL을 직접 작성·해석하고, “옳지 않은 것을 고르시오” 유형에서 자주 나오는 함정(NULL 처리, OUTER JOIN이 조건절 때문에 INNER JOIN으로 되돌아가는 현상)을 스스로 짚어낼 수 있게 된다.

왜 조인·서브쿼리·집합 연산이 필요한가

정규화(자세한 원리는 15·16편)를 거친 관계형 데이터베이스는 하나의 정보를 여러 테이블에 나누어 저장합니다. 학생 정보는 학생 테이블에, 과목 정보는 과목 테이블에, “누가 어떤 과목을 들었는지”는 수강 테이블에 따로 있습니다. 그런데 실제로 필요한 답(“컴퓨터공학과 학생 중 데이터베이스 과목을 들은 사람은?”)은 이 테이블들을 다시 이어 붙여야 나옵니다. 이 “다시 이어 붙이는” 연산이 조인(join)입니다. 조인만으로 표현하기 번거로운 조건(예: “한 번도 수강하지 않은 학생”)은 서브쿼리(subquery, 중첩 질의)로 풀고, “두 조건을 모두 만족하는 것”이나 “한쪽에만 있는 것”은 집합 연산(set operation)으로 풉니다.

쉽게 말하면: 조인은 “테이블을 옆으로 이어 붙이기”, 서브쿼리는 “질의 안에 또 다른 질의를 넣어 조건으로 쓰기”, 집합 연산은 “두 질의 결과표를 위아래로 비교하기”입니다.

이 편의 SQL 예제는 이 사이트의 SQLD 과목과 문법을 맞추기 위해 Oracle 문법을 기준으로 씁니다. 표준 SQL과 이름이 다른 부분(대표적으로 MINUS)은 그때그때 짚어 드립니다.

예제로 쓸 세 테이블

이 편과 다음 편(14편)에서 계속 사용할 예제 스키마입니다. 학번·과목코드에는 일부러 정합성이 깨진 값(존재하지 않는 과목코드를 참조하는 수강 기록)을 하나 심어 두었는데, 이는 OUTER JOIN과 NOT IN의 함정을 실제 데이터로 보여주기 위한 의도적 장치입니다(실제 시험 문제에서도 “다음 데이터가 주어졌을 때”라는 형태로 이런 예외 행을 넣어 이해도를 시험합니다).

STUDENT(학번, 이름, 학과)

학번이름학과
S1김민준컴퓨터공학
S2이서연컴퓨터공학
S3박도윤전자공학
S4최지우컴퓨터공학
S5정하윤산업공학
S6강서준전자공학

COURSE(과목코드, 과목명, 학점)

과목코드과목명학점
C001데이터베이스3
C002운영체제3
C003네트워크2
C004인공지능3

ENROLL(학번, 과목코드, 성적)

학번과목코드성적
S1C00190
S1C00285
S2C00178
S3C001NULL
S3C00392
S4C00288
S5C00570

S6(강서준)은 어떤 과목도 신청한 적이 없고, S5의 마지막 행은 COURSE 테이블에 없는 C005를 참조합니다(과목이 폐강되었거나 데이터 오류로 남은 행이라고 가정합니다). S3C001 성적은 아직 채점 전이라 NULL입니다.

1. INNER JOIN — 양쪽 모두에 있는 것만

내부 조인(INNER JOIN)은 두 테이블에서 조인 조건을 만족하는 행, 즉 양쪽 모두에 대응하는 값이 있는 행만 결과에 남깁니다.

SELECT e.학번, e.과목코드, c.과목명 FROM ENROLL e INNER JOIN COURSE c ON e.과목코드 = c.과목코드;

실행 과정을 손으로 따라가 봅니다. ENROLL의 각 행이 COURSE에서 같은 과목코드를 가진 행과 만나는지 하나씩 확인합니다.

ENROLL 행COURSE에 매칭?결과 포함
S1, C001C001 있음포함
S1, C002C002 있음포함
S2, C001C001 있음포함
S3, C001C001 있음포함
S3, C003C003 있음포함
S4, C002C002 있음포함
S5, C005없음(C005는 COURSE에 없음)제외

결과는 6행이며, C004(인공지능)는 ENROLL에 신청한 사람이 없으므로 처음부터 등장하지 않습니다. INNER JOIN은 양쪽 모두 짝이 있는 행만 남기므로, 짝이 없는 쪽의 정보는 통째로 사라진다는 점이 핵심입니다.

2. OUTER JOIN — 한쪽(또는 양쪽) 정보를 보존

외부 조인(OUTER JOIN)은 짝이 없는 행도 버리지 않고, 짝이 없는 쪽 컬럼을 NULL로 채워 결과에 남깁니다. “어느 쪽을 보존하는가”에 따라 세 가지로 나뉩니다.

LEFT OUTER JOIN — 왼쪽 테이블 전부 보존

SELECT e.학번, e.과목코드, c.과목명 FROM ENROLL e LEFT OUTER JOIN COURSE c ON e.과목코드 = c.과목코드;

ENROLL(왼쪽)의 모든 행을 남기고, 짝이 없는 S5, C005 행은 과목명NULL로 채웁니다.

학번과목코드과목명
S1C001데이터베이스
S1C002운영체제
S2C001데이터베이스
S3C001데이터베이스
S3C003네트워크
S4C002운영체제
S5C005NULL

INNER JOIN 결과(6행)에 S5, C005, NULL 한 행이 더 붙어 7행이 됩니다.

RIGHT OUTER JOIN — 오른쪽 테이블 전부 보존

SELECT e.학번, e.과목코드, c.과목명 FROM ENROLL e RIGHT OUTER JOIN COURSE c ON e.과목코드 = c.과목코드;

이번에는 COURSE(오른쪽)의 모든 행을 남깁니다. 신청자가 없는 C004(인공지능)가 학번 NULL로 등장합니다.

학번과목코드과목명
S1C001데이터베이스
S1C002운영체제
S2C001데이터베이스
S3C001데이터베이스
S3C003네트워크
S4C002운영체제
NULLC004인공지능

S5, C005 행은 COURSEC005가 없으므로 이번에는 결과에서 빠집니다. RIGHT JOIN은 “오른쪽 기준”이라는 점을 거꾸로 외우기 쉬우므로, FROM A RIGHT JOIN B는 “B를 다 살린다”고 소리 내어 확인하는 습관이 유용합니다.

FULL OUTER JOIN — 양쪽 모두 보존

SELECT e.학번, e.과목코드, c.과목명 FROM ENROLL e FULL OUTER JOIN COURSE c ON e.과목코드 = c.과목코드;

LEFT JOIN 결과와 RIGHT JOIN 결과를 합친 것과 같습니다. S5, C005, NULL(왼쪽만 있던 행)과 NULL, C004, 인공지능(오른쪽만 있던 행)이 모두 포함되어 총 8행이 됩니다.

자주 틀리는 점: LEFT OUTER JOIN을 걸어 놓고 WHERE c.과목명 = '데이터베이스'처럼 오른쪽 테이블 컬럼에 조건을 거는 경우입니다. 짝이 없는 행은 c.과목명NULL이므로 이 조건을 만족하지 못해 걸러지고, 결과적으로 OUTER JOIN이 사실상 INNER JOIN처럼 동작합니다. 오른쪽 테이블이 없는 행까지 보고 싶다면 조건을 ON 절 안에 넣거나 c.과목명 = '데이터베이스' OR c.과목명 IS NULL처럼 명시적으로 풀어야 합니다. “다음 SQL의 실행 결과로 옳은 것은?” 유형에서 자주 등장하는 함정입니다.

3. CROSS JOIN — 조건 없는 카티전 곱

교차 조인(CROSS JOIN)은 조인 조건 없이 두 테이블의 모든 행 조합을 만드는 카티전 곱(Cartesian product)입니다. STUDENT(6행)와 COURSE(4행)를 CROSS JOIN하면 6 × 4 = 24행이 나옵니다.

SELECT s.이름, c.과목명 FROM STUDENT s CROSS JOIN COURSE c;

이 24행에는 “김민준이 실제로 데이터베이스를 들었는가”와 무관하게 모든 학생-과목 조합이 다 들어갑니다. 실무에서 CROSS JOIN을 의도적으로 쓰는 경우는 드물지만, 시험에서는 다음 함정으로 자주 나옵니다.

자주 틀리는 점: FROM STUDENT s, COURSE c WHERE ...처럼 콤마로 테이블을 나열하는 옛날 방식(암묵적 조인) 문법에서 WHERE 절에 조인 조건을 빠뜨리면 그대로 CROSS JOIN이 되어 버립니다. 두 테이블에 각각 100행만 있어도 결과가 10,000행으로 폭증하므로, “이 질의의 실행 결과 행 수는?”을 묻는 문제에서 조인 조건 누락 여부를 먼저 확인해야 합니다.

JOIN 종류 비교

종류남기는 행짝 없는 쪽 처리결과 행 수(예제 기준)
INNER JOIN양쪽 모두 짝이 있는 행만버림6
LEFT OUTER JOIN왼쪽 테이블 전부오른쪽을 NULL로 채움7
RIGHT OUTER JOIN오른쪽 테이블 전부왼쪽을 NULL로 채움7
FULL OUTER JOIN양쪽 테이블 전부없는 쪽을 NULL로 채움8
CROSS JOIN조건 없는 모든 조합해당 없음6 × 4 = 24

4. 서브쿼리 — 질의 안의 질의

서브쿼리(subquery, 하위 질의·중첩 질의)는 다른 SQL문 안에 괄호로 둘러싸여 들어가는 SELECT문입니다. 바깥 질의(outer query)의 조건절이나 SELECT 목록 안에서 사용됩니다.

스칼라 서브쿼리 — 값 하나를 반환

스칼라(scalar)는 “값 하나”라는 뜻입니다. 스칼라 서브쿼리는 정확히 한 행, 한 컬럼(즉 값 하나)을 반환해야 하며, 주로 SELECT 목록이나 비교 연산자(=, > 등) 오른쪽에 씁니다.

SELECT s.이름, (SELECT COUNT(*) FROM ENROLL e WHERE e.학번 = s.학번) AS 수강과목수 FROM STUDENT s;

학생마다 몇 과목을 신청했는지 세는 상관 서브쿼리(correlated subquery, 바깥 질의의 값 s.학번을 안쪽에서 참조하는 서브쿼리)입니다. 계산 결과는 다음과 같습니다.

이름수강과목수
김민준2
이서연1
박도윤2
최지우1
정하윤1
강서준0

IN 서브쿼리 — 값 목록에 포함되는지

SELECT 이름 FROM STUDENT WHERE 학번 IN (SELECT 학번 FROM ENROLL WHERE 과목코드 = 'C001');

ENROLL에서 과목코드 = 'C001'인 행의 학번 목록은 {S1, S2, S3}입니다. 이 집합에 속하는 학생 이름을 뽑으면 김민준·이서연·박도윤 3명입니다.

EXISTS / NOT EXISTS — 존재 여부만 확인

EXISTS는 서브쿼리가 행을 하나라도 반환하면 참이 되는 연산자로, 값 자체가 아니라 “존재하는가”만 봅니다. 보통 상관 서브쿼리와 함께 씁니다.

SELECT 이름 FROM STUDENT s WHERE EXISTS ( SELECT 1 FROM ENROLL e WHERE e.학번 = s.학번 AND e.과목코드 = 'C001' );

위 IN 서브쿼리와 동일하게 김민준·이서연·박도윤이 나옵니다. SELECT 1처럼 아무 값이나 넣는 이유는 EXISTS가 행의 존재 여부만 보고 실제 값은 보지 않기 때문입니다.

NOT IN / NOT EXISTS의 함정 — 한 번도 신청하지 않은 학생 찾기

“한 번도 수강 신청을 하지 않은 학생”을 찾는 문제로 NOT INNOT EXISTS의 차이를 직접 비교해 보겠습니다.

-- 방법 A: NOT IN SELECT 이름 FROM STUDENT WHERE 학번 NOT IN (SELECT 학번 FROM ENROLL);

기대하는 답은 강서준(S6) 한 명입니다. 그런데 ENROLL.학번NULL이 하나라도 섞여 있으면 이 질의는 아무 행도 반환하지 않습니다. NOT IN은 내부적으로 학번 <> 값1 AND 학번 <> 값2 AND ...처럼 풀리는데, 목록에 NULL이 있으면 그 비교(학번 <> NULL)는 참도 거짓도 아닌 UNKNOWN이 되어 AND로 묶인 전체 조건이 참이 될 수 없기 때문입니다.

-- 방법 B: NOT EXISTS (안전) SELECT 이름 FROM STUDENT s WHERE NOT EXISTS ( SELECT 1 FROM ENROLL e WHERE e.학번 = s.학번 );

NOT EXISTS는 값 목록을 만들어 비교하는 방식이 아니라 상관 서브쥐리로 “이 학생의 학번과 일치하는 행이 하나도 없는가”를 직접 확인하므로, ENROLL.학번NULL이 섞여 있어도 영향을 받지 않고 정확히 강서준 한 명을 반환합니다.

비교 기준IN / NOT INEXISTS / NOT EXISTS
판단 방식서브쿼리 결과 값 목록과 비교서브쿼리가 행을 반환하는지만 확인
NULL이 서브쿼리 결과에 섞인 경우NOT IN은 전체가 빈 결과가 될 수 있음(위험)영향받지 않음(안전)
주로 쓰는 상황값 목록이 짧고 NULL이 없다고 확신할 때NULL 가능성이 있거나 존재 여부만 필요할 때

자주 틀리는 점: “다음 중 NOT IN과 NOT EXISTS가 항상 같은 결과를 낸다”는 보기는 옳지 않은 진술입니다. 서브쿼리 결과에 NULL이 하나라도 있으면 NOT IN은 빈 결과를, NOT EXISTS는 정상적인 결과를 낼 수 있어 둘은 항상 같지 않습니다.

5. 집합 연산 — UNION / UNION ALL / INTERSECT / MINUS

집합 연산(set operation)은 두 SELECT문의 결과(각각 같은 개수·같은 순서의 호환되는 자료형 컬럼을 가져야 합니다)를 위아래로 비교합니다. 컴퓨터공학과 학생 목록과 C001(데이터베이스) 수강생 목록으로 예를 들어 보겠습니다.

  • 컴퓨터공학과 학생: {S1, S2, S4} (김민준·이서연·최지우)
  • C001 수강생: {S1, S2, S3} (김민준·이서연·박도윤)
SELECT 학번 FROM STUDENT WHERE 학과 = '컴퓨터공학' UNION SELECT 학번 FROM ENROLL WHERE 과목코드 = 'C001';

UNION은 두 결과를 합치고 중복을 제거합니다. 결과는 {S1, S2, S3, S4} 4행입니다. 중복을 제거하지 않고 그대로 다 합치려면 UNION ALL을 쓰며, 이 경우 S1, S2가 두 번씩 나와 총 6행이 됩니다. UNION은 중복 제거를 위해 내부적으로 정렬·비교 작업을 하므로 UNION ALL보다 비용이 크며, 중복이 문제되지 않는 상황이라면 UNION ALL이 더 효율적입니다.

SELECT 학번 FROM STUDENT WHERE 학과 = '컴퓨터공학' INTERSECT SELECT 학번 FROM ENROLL WHERE 과목코드 = 'C001';

INTERSECT(교집합)는 양쪽 모두에 있는 값만 남깁니다. {S1, S2, S4} ∩ {S1, S2, S3} = {S1, S2}이므로 결과는 2행입니다.

SELECT 학번 FROM STUDENT WHERE 학과 = '컴퓨터공학' MINUS SELECT 학번 FROM ENROLL WHERE 과목코드 = 'C001';

MINUS(차집합)는 첫 번째 결과에서 두 번째 결과에 있는 값을 뺍니다. {S1, S2, S4} − {S1, S2, S3} = {S4}이므로 최지우 한 명만 남습니다. 표준 SQL·다른 DBMS(MySQL, SQL Server 등)에서는 같은 연산을 EXCEPT라는 키워드로 씁니다.

자주 틀리는 점: Oracle 문법 문제에서 EXCEPT를 그대로 쓰면 오류가 납니다. Oracle은 차집합에 MINUS를 씁니다. 반대로 표준 SQL이나 SQL Server 기준 문제에서는 MINUS 대신 EXCEPT를 써야 합니다. 어떤 DBMS를 기준으로 하는 문제인지 먼저 확인하는 습관이 필요합니다.

집합 연산 비교

연산의미중복 처리예제 결과
UNION합집합중복 제거S1, S2, S3, S4 (4행)
UNION ALL합집합(중복 유지)중복 유지S1, S1, S2, S2, S3, S4 (6행)
INTERSECT교집합중복 제거S1, S2 (2행)
MINUS(Oracle) / EXCEPT(표준)차집합중복 제거S4 (1행)

핵심 정리

  • INNER JOIN은 양쪽 모두 짝이 있는 행만, OUTER JOIN은 짝이 없는 쪽을 NULL로 채워서라도 보존하며, LEFT·RIGHT·FULL은 어느 쪽을 보존하는지의 차이다.
  • CROSS JOIN(카티전 곱)은 조건 없는 모든 조합이며, 콤마 조인에서 조인 조건을 빠뜨리면 의도치 않게 CROSS JOIN이 된다.
  • LEFT/RIGHT OUTER JOIN 뒤에 오른쪽/왼쪽 컬럼에 대한 WHERE 조건을 걸면 NULL인 행이 걸러져 결과적으로 INNER JOIN처럼 동작하는 함정이 있다.
  • 스칼라 서브쿼리는 값 하나, IN은 목록 포함 여부, EXISTS는 행의 존재 여부를 확인하며, 서브쿼리 결과에 NULL이 섞이면 NOT IN은 빈 결과가 될 수 있지만 NOT EXISTS는 안전하다.
  • UNION(중복 제거)·UNION ALL(중복 유지)·INTERSECT(교집합)·MINUS(Oracle의 차집합, 표준 SQL은 EXCEPT)는 같은 컬럼 구조를 가진 두 질의 결과를 집합처럼 비교한다.

마무리 복습

문제 14지선다
ENROLL을 왼쪽, COURSE를 오른쪽에 두고 LEFT OUTER JOIN을 수행했을 때 결과에 대한 설명으로 옳은 것은?
문제 24지선다
다음 중 CROSS JOIN에 대한 설명으로 옳지 않은 것은?
문제 34지선다
ENROLL.학번에 NULL 값이 하나라도 포함되어 있을 때, 다음 질의의 실행 결과로 옳은 것은? — SELECT 이름 FROM STUDENT WHERE 학번 NOT IN (SELECT 학번 FROM ENROLL)
문제 44지선다
IN 서브쿼리 대신 EXISTS 서브쿼리를 쓰는 것이 더 안전한 이유로 가장 적절한 것은?
문제 54지선다
컴퓨터공학과 학생 학번 집합 {S1, S2, S4}와 C001 수강생 학번 집합 {S1, S2, S3}이 있을 때, 두 질의를 MINUS(Oracle 기준)로 연결한 결과로 옳은 것은?
문제 64지선다
UNION과 UNION ALL의 차이에 대한 설명으로 옳은 것은?

참고 자료

Last updated on