Quick Reference
SELECT u.id, u.name, p.title
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id;LEFT JOIN은 왼쪽 행을 모두 남기고 오른쪽에 매칭이 없을 때만 오른쪽 열을 NULL로 채웁니다. 관계 부재를 판별할 때는 오른쪽 기본 키처럼 NOT NULL인 열을 검사합니다. 오른쪽의 필터 조건을 WHERE에 두면 NULL 행이 사라져 INNER JOIN처럼 바뀝니다.
문법
LEFT JOIN은 "없는 관계도 의미 있는 데이터"임을 선언한다
INNER JOIN은 양쪽 테이블에 모두 매칭 행이 있을 때만 결과를 만듭니다. LEFT JOIN은 왼쪽 테이블의 모든 행을 유지하되, 오른쪽에 대응되는 행이 없으면 오른쪽 컬럼 전체를 NULL로 채웁니다. "아직 글을 쓰지 않은 사용자도 목록에 포함해야 한다"처럼 관계의 부재 자체가 비즈니스 의미를 가질 때 LEFT JOIN이 옳은 선택입니다. 실제 실행 계획은 데이터 양과 정렬 상태에 따라 Nested Loop, Hash Join, Merge Join 계열 중에서 결정됩니다.
-- 글을 한 번도 쓰지 않은 사용자 포함한 전체 목록
SELECT u.name, COUNT(p.id) AS post_count
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id
GROUP BY u.id, u.name
ORDER BY post_count DESC;LEFT JOIN은 "왼쪽은 유지하고 오른쪽이 없으면 NULL"로 읽으면 된다
입문 단계에서는 LEFT JOIN을 "왼쪽 목록을 기준으로 유지한다"라고 이해하면 됩니다. 오른쪽에 매칭이 없을 때 NULL이 나오는 건 오류가 아니라, 관계가 없다는 정보입니다.
SELECT u.id, u.name, p.title
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id;오른쪽 테이블의 NULL은 JOIN 실패가 아니라 관계 부재 신호다
LEFT JOIN 결과에서 오른쪽 컬럼이 NULL이면 쿼리가 틀린 게 아니라 "해당 사용자에게 연결된 행이 없다"는 정보입니다. 이 NULL을 활용하면 "연결 없는 행만 필터링"하는 안티조인 패턴을 표현할 수 있습니다. 이 패턴은 NOT EXISTS로도 표현 가능하며, 실제로 어느 쪽이 더 읽기 쉽고 실행 계획이 안정적인지는 데이터 분포를 두고 EXPLAIN ANALYZE로 확인하는 편이 안전합니다.
-- 한 번도 글을 쓰지 않은 사용자만 추출 (안티조인 패턴)
SELECT u.id, u.name
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id
WHERE p.id IS NULL;ON 조건과 WHERE 조건의 위치가 LEFT JOIN 의미를 바꾼다
LEFT JOIN 뒤에 오른쪽 테이블의 필터 조건을 WHERE 절에 두면 NULL 행이 걸러져 사실상 INNER JOIN처럼 동작합니다. "오른쪽 테이블의 특정 행 중 조건을 만족하는 것"과만 연결하려면 해당 조건을 ON 절 안에 넣어야 합니다. 이 차이는 논리 정확성 문제이므로 실행 계획보다 먼저 짚어야 합니다.
-- 잘못된 패턴: WHERE 조건이 LEFT JOIN을 INNER JOIN으로 만듦
SELECT u.name, p.title
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id
WHERE p.published = true; -- NULL 행이 제거됨
-- 올바른 패턴: 오른쪽 테이블 조건은 ON 절에
SELECT u.name, p.title
FROM users AS u
LEFT JOIN posts AS p
ON p.user_id = u.id AND p.published = true;관계 부재 확인과 실제 값 확인은 같은 NULL이라도 읽는 기준이 다르다
LEFT JOIN 결과에서 오른쪽 값이 NULL이라고 해서 항상 "관계가 없다"는 뜻은 아닙니다. 정말 관계 부재를 확인하려면 오른쪽 기본 키나 NOT NULL이 보장된 기준 열을 봐야 하고, 실제 데이터 컬럼의 NULL은 "연결은 됐지만 값이 비어 있다"일 수 있습니다.
SELECT u.id, p.id AS post_id, p.deleted_at
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id;여러 LEFT JOIN은 중간 결과와 접근 경로를 함께 본다
오른쪽 테이블 조인 키(p.user_id) 인덱스는 큰 왼쪽 결과에서 반복 탐색할 때 도움이 될 수 있지만, 인덱스가 없으면 항상 Seq Scan이 되고 있으면 항상 Index Scan이 된다는 뜻은 아닙니다. EXPLAIN (ANALYZE, BUFFERS)로 실제 조인 방식·행 수·루프를 보고, 필요한 경우에만 인덱스를 추가합니다. 여러 테이블을 연속으로 LEFT JOIN할 때는 중간 결과 크기와 필터 위치도 계획에 영향을 줍니다.
-- 조인 키 인덱스 확보
CREATE INDEX idx_posts_user_id ON posts (user_id);
-- 실행 계획 확인
EXPLAIN ANALYZE
SELECT u.name, p.title
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id
WHERE u.active = true;체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 양쪽 모두 존재하는 행만 필요할 때 | INNER JOIN |
| 왼쪽 행을 모두 유지해야 할 때 | LEFT JOIN |
| 오른쪽과 연결 없는 행만 추출할 때 | LEFT JOIN ... WHERE 오른쪽.col IS NULL |
| 오른쪽 테이블을 조건으로 필터링할 때 | 조건을 ON 절 안에 배치 |
| 조인 성능이 느릴 때 | 오른쪽 조인 키 인덱스를 실행 계획과 함께 검토 |
주의할 점
LEFT JOIN 뒤에 오른쪽 테이블 조건을 WHERE 절에 두면 NULL 행이 제거되어 사실상 INNER JOIN처럼 동작합니다.
"없는 행도 유지한다"는 의도라면 오른쪽 테이블 필터를 ON 절 안에 둡니다.
오른쪽 테이블의 조인 컬럼 인덱스는 대규모 반복 탐색에서 후보가 되지만, 필요 여부는 실제 실행 계획으로 확인합니다.
SELECT u.name
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id
WHERE p.title IS NULL;p.title IS NULL은 "글이 없음"과 "글은 있는데 제목이 NULL"을 구분하지 못합니다. 관계 부재를 보려면 보통 오른쪽 기본 키 같은 NOT NULL 기준 열을 검사하는 편이 맞습니다.
SELECT u.id, u.name
FROM users AS u
LEFT JOIN posts AS p
ON p.user_id = u.id
WHERE p.id IS NULL
AND u.active = true;안티조인 패턴은 "오른쪽에 행이 아예 없다"를 보는 용도입니다. 여기에 오른쪽 컬럼 조건을 더 얹고 싶다면, 그 조건도 대개 ON 절에서 먼저 좁혀 놓아야 의도와 실제 결과가 어긋나지 않습니다.
참고 링크
1 sources