LEFT OUTER JOIN은 왼쪽 테이블의 모든 행을 결과에 포함시킨다. 오른쪽 테이블에 매칭되는 행이 없으면 오른쪽 컬럼은 NULL로 채운다. 데이터가 없어도 왼쪽은 절대 제외되지 않는다.
동작 원리
유저 목록을 조회하는데, 피드가 없는 유저도 포함시켜야 하는 상황이다.
SELECT u.*, f.*
FROM user u
LEFT OUTER JOIN feed f ON u.id = f.author_id
결과를 보면 다음과 같다.
| user.id | user.name | feed.id | feed.content |
|---|---|---|---|
| 1 | Alice | 10 | "오늘 날씨..." |
| 1 | Alice | 11 | "주말 코디..." |
| 2 | Bob | NULL | NULL |
Bob은 피드가 없지만 결과에 포함된다. 피드 관련 컬럼은 NULL이다. Alice는 피드가 2개라 2개 행으로 뻥튀기된다.
여기에 LEFT JOIN의 집합 관계(왼쪽 전체 + 교집합)를 보여주는 다이어그램 필요.
INNER JOIN과의 차이
같은 쿼리를 INNER JOIN으로 바꾸면 Bob이 결과에서 사라진다.
SELECT u.*, f.*
FROM user u
INNER JOIN feed f ON u.id = f.author_id
피드가 없는 유저는 매칭되는 행이 없으니 제외된다. 어떤 JOIN을 쓰느냐에 따라 결과가 달라지는 전형적인 사례다.
언제 쓰는가
연관 데이터가 없는 행도 결과에 포함해야 할 때 사용한다.
- 피드를 작성하지 않은 유저도 목록에 포함
- 댓글이 없는 게시글도 조회
- 선택적 연관관계 (
@ManyToOne이 NULL 허용인 경우)
연관 데이터가 반드시 존재한다고 보장된다면 INNER JOIN이 의미가 더 명확하다.
JPA JPQL에서의 LEFT JOIN
JPQL에서는 LEFT JOIN 또는 LEFT OUTER JOIN으로 쓴다.
@Query("SELECT u FROM User u LEFT JOIN u.feeds f WHERE f.id IS NULL")
List<User> findUsersWithNoFeed();
피드가 없는 유저를 찾는 쿼리다. LEFT JOIN 후 오른쪽이 NULL인 행만 필터링한다.
LAZY 로딩을 해결하면서 LEFT JOIN 동작이 필요하면 LEFT JOIN FETCH를 쓴다.
@Query("SELECT f FROM Feed f LEFT JOIN FETCH f.ootds")
List<Feed> findAllWithOotds();
ootds가 없는 Feed도 결과에 포함시키면서, ootds 데이터를 한 번에 로딩한다.
자주 하는 실수
왼쪽 테이블의 한 행에 오른쪽 행이 여러 개 매칭되면 왼쪽 행이 중복된다. Alice처럼 피드가 2개면 유저 Alice가 2개 행으로 나온다. 컬렉션 관계에서 LEFT JOIN을 쓸 때는 DISTINCT로 중복을 제거해야 한다.
```java
@Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.feeds")
List
```
[!BUG] LEFT JOIN인데 WHERE 절에서 오른쪽 컬럼 필터링
LEFT JOIN을 써서 NULL 행을 포함시켜 놓고, WHERE f.id = :id처럼 오른쪽 컬럼으로 필터링하면 NULL 행이 다시 제외된다. 사실상 INNER JOIN과 같은 결과가 나온다. NULL 체크가 필요하다면 WHERE f.id IS NULL OR f.id = :id처럼 써야 한다.