왼쪽 표의 행을 모두 남기면서 조건에 맞는 오른쪽 행만 붙이려면 그 조건을 ON에서 검사합니다. 조인 결과 가운데 조건에 맞는 행만 남기려면 WHERE에서 검사합니다. LEFT JOIN이라는 이름만으로 최종 결과에 왼쪽 행이 전부 남는 것은 아닙니다.
아래 예시는 PostgreSQL 18 문서의 의미를 기준으로 만든 교육용 표입니다. 실제 기출문제나 판매 카드 문항을 옮긴 것이 아니며, 한 쿼리가 언제나 더 빠르다는 설명도 아닙니다.
조건을 옮기기 전에 결과의 목적부터 적습니다
강좌 목록에 결제한 신청자만 붙이되 신청자가 없는 강좌도 보고 싶다고 가정해 보세요. 이 요구에는 서로 다른 두 조건이 들어 있습니다. 하나는 강좌 자체를 남기는 것이고, 다른 하나는 어떤 신청자를 연결할지 정하는 것입니다. 둘을 구분하지 않고 결제 조건을 WHERE로 보내면 강좌를 남기려던 목적까지 바뀔 수 있습니다.
문제를 풀 때는 먼저 ‘모든 강좌’와 ‘결제한 신청자가 있는 강좌’ 중 무엇을 요구하는지 밑줄을 긋습니다. 전자는 연결할 신청자를 고르는 문제이고, 후자는 결과에 남길 강좌를 고르는 문제입니다. SQL을 외우기 전에 기대하는 결과를 한국어 한 문장으로 적어 보면 두 요구가 같지 않다는 점이 드러납니다.
강좌 세 개를 같은 입력으로 비교합니다
| 강좌 ID | 이름 | 신청 ID | 결제 여부 |
|---|---|---|---|
| 1 | 입문 | 11 | TRUE |
| 2 | 심화 | 12 | FALSE |
| 3 | 실습 | 신청 없음 | 해당 없음 |
WITH course(id, label) AS (
VALUES (1, '입문'), (2, '심화'), (3, '실습')
), signup(id, course_id, paid) AS (
VALUES (11, 1, true), (12, 2, false)
)
SELECT c.id AS course_id, s.id AS signup_id
FROM course AS c
LEFT JOIN signup AS s
ON c.id = s.course_id AND s.paid = true
ORDER BY c.id;입문 강좌는 결제한 신청 11과 연결됩니다. 심화 강좌의 신청 12는 ON 조건을 만족하지 않으므로 연결되는 오른쪽 행이 없습니다. 따라서 심화와 신청 자체가 없는 실습 모두 오른쪽 열을 NULL로 채운 행이 남습니다. 신청 12가 데이터에서 삭제된 것은 아닙니다. 이번 조인의 연결 대상이 되지 않았다는 뜻입니다.
SELECT c.id AS course_id, s.id AS signup_id
FROM course AS c
LEFT JOIN signup AS s ON c.id = s.course_id
WHERE s.paid = true
ORDER BY c.id;| 강좌 | A: ON에서 결제 조건 | B: WHERE에서 결제 조건 |
|---|---|---|
| 입문 | (1, 11) | (1, 11) |
| 심화 | (2, NULL) | 제외 |
| 실습 | (3, NULL) | 제외 |
B에서는 먼저 강좌 ID로 연결합니다. 심화는 결제하지 않은 신청 12가 붙고, 실습만 NULL 행이 됩니다. 이후 WHERE에서 심화의 조건은 FALSE, 실습의 조건은 UNKNOWN이 됩니다. 어느 쪽도 TRUE가 아니므로 결과에 남지 않습니다. 이 순서는 결과의 의미를 손으로 추적하는 순서이며, 데이터베이스가 물리적으로 처리하는 실행 계획의 순서를 보장하지는 않습니다.
OR IS NULL을 붙이는 보정도 같은 결과는 아닙니다
B의 조건을 ‘s.paid = true OR s.id IS NULL’로 바꾸면 A와 같아질까요? 실습은 되살아나지만 심화는 여전히 빠집니다. 심화에는 이미 신청 ID 12가 연결되어 있어 식별자 값이 NULL이 아닙니다. 결제 조건도 거짓이어서 두 조건 중 어느 것도 만족하지 못합니다. 여기서는 신청 ID가 항상 값이 있는 식별자라는 전제를 두었습니다.
이 반례 때문에 ‘LEFT JOIN 뒤에는 IS NULL을 붙이면 된다’는 문장을 암기하면 위험합니다. 오른쪽 행이 아예 없는 경우와, 오른쪽 행은 있지만 원하는 조건에 맞지 않는 경우는 다릅니다. 결과가 틀렸을 때 조건을 덧붙이기보다, 처음 적어 둔 요구가 연결 조건인지 결과 필터인지 다시 확인하세요.
틀린 문제를 세 종류의 입력으로 바꿔 복습합니다
- 조건을 만족하는 오른쪽 행이 있는 왼쪽 행을 하나 만듭니다.
- 오른쪽 행은 있지만 조건을 만족하지 않는 왼쪽 행을 하나 추가합니다.
- 오른쪽 행이 전혀 없는 왼쪽 행을 하나 추가합니다.
- 각 행이 조인 직후와 WHERE 적용 뒤에 남는지 따로 표시합니다.
- 다음에는 조건을 만족하는 신청자 두 명을 붙여 왼쪽 ID가 중복될 수 있는지도 확인합니다.
새 신청자를 추가하는 마지막 변형은 행 보존과 행 개수가 다른 문제라는 점을 보여 줍니다. 왼쪽 행이 하나여도 연결 조건을 만족하는 신청자가 둘이면 결과는 두 행이 될 수 있습니다. 최종 출력에 강좌 ID만 보인다고 해서 중복 행이 자동으로 사라지는 것은 아닙니다. 요청에 없는 DISTINCT를 임의로 추가하지 마세요.
정답을 가리고 확인해 보세요
이 글을 위해 만든 학습 예시이며 공식 기출문제나 판매 카드의 인용이 아닙니다.
강좌 4에는 결제하지 않은 신청 21만 있습니다. ‘모든 강좌를 남기고 결제한 신청만 연결’하려고 결제 조건을 ON에 넣으면 강좌 4의 신청 ID는 어떻게 표시되나요? 같은 조건을 WHERE로 옮기면 어떻게 되나요?
정답과 해설 확인
ON에서는 강좌 4가 남고 신청 ID는 NULL입니다. WHERE에서는 강좌 4가 결과에서 제외됩니다.ON에서는 결제 조건을 만족하는 연결 행이 없어 NULL로 보충합니다. WHERE에서는 이미 연결된 미결제 신청이 조건을 만족하지 못해 그 결과 행이 제거됩니다.