백엔드 개발을 하다 보면 이런 쿼리를 자주 작성하게 된다.
SELECT *
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
AND o.created_at >= '2024-01-01';
겉보기에는 단순한 LEFT JOIN 쿼리지만,
이 쿼리를 왜 이렇게 써야 하는지 정확히 설명할 수 있는 개발자는 생각보다 많지 않다.
이번 글에서는
LEFT JOIN의 핵심 목적은 하나다.
기준 테이블의 row는 반드시 유지하고,
매칭되는 데이터가 있으면 붙인다.
위 쿼리에서 기준 테이블은 users다.
즉, 이 쿼리가 의도하는 바는 다음과 같다.
2024-01-01 이후에 생성된 주문이 있으면 함께 조회한다이 의도를 SQL로 정확히 표현한 것이 바로 위의 쿼리다.
많은 개발자가 JOIN을 이렇게 오해한다.
JOIN = 컬럼을 옆에 붙이는 작업
하지만 실제로 JOIN은 row를 기준으로 새로운 결과 집합을 만드는 연산이다.
LEFT JOIN의 처리 순서를 단순화하면 다음과 같다.
users 테이블의 row를 하나씩 읽는다orders 테이블에서 JOIN 조건에 맞는 row를 찾는다orders 컬럼을 NULL로 채운 row 생성여기서 중요한 점은
JOIN 조건은 ON 절에서만 판단된다는 것이다.
다시 쿼리를 보자.
LEFT JOIN orders o
ON o.user_id = u.id
AND o.created_at >= '2024-01-01';
이 조건은 다음을 의미한다.
orders.user_id = users.id 인 주문 중에서created_at >= '2024-01-01' 인 row만즉, JOIN 자체에 참여할 orders row를 제한하는 조건이다.
이 조건에 맞는 주문이 없으면?
이것이 LEFT JOIN의 본래 의미다.
많이 보게 되는 잘못된 쿼리는 다음과 같다.
SELECT *
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.created_at >= '2024-01-01';
겉으로 보면 거의 동일해 보이지만,
결과는 완전히 달라진다.
SQL의 논리적 실행 순서는 단순화하면 다음과 같다.
즉,
LEFT JOIN 이후, 주문이 없는 유저의 row는 이렇게 생긴다.
users 컬럼: 값 있음
orders 컬럼: 전부 NULL
이 상태에서 WHERE 절을 적용하면?
WHERE o.created_at >= '2024-01-01'
o.created_at은 NULL결과적으로:
이 차이를 명확하게 구분해야 한다.
한 문장으로 요약하면:
LEFT JOIN에서 JOIN 대상에 대한 조건은 반드시 ON 절에 작성해야 한다.
이 문제는 단순히 SQL 문법의 문제가 아니다.
실무에서는:
같은 요구사항이 매우 많다.
이때 ON / WHERE를 잘못 사용하면:
그리고 이런 버그는 DB를 모르면 찾기 어렵다.
SELECT *
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
AND o.created_at >= '2024-01-01';
이 쿼리는 단순한 SQL이 아니라,
LEFT JOIN의 본질을 정확히 이해하고 작성한 쿼리다.
usersorders는 조건에 맞는 경우에만 JOIN이 차이를 이해하는 순간,
JOIN 결과가 왜 깨졌는지
왜 row가 사라졌는지
왜 DISTINCT를 써도 해결이 안 됐는지
설명할 수 있게 된다.