이미지 로딩 중...

LEFT JOIN과 외부 조인 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 23. · 3 Views

LEFT JOIN과 외부 조인 완벽 가이드

SQL의 LEFT JOIN과 외부 조인을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 실무에서 자주 사용하는 패턴과 함께 INNER JOIN과의 차이, NULL 처리 방법까지 다룹니다.


목차

  1. LEFT JOIN 개념
  2. NULL 값으로 매칭되지 않은 데이터 처리
  3. RIGHT JOIN 사용법
  4. FULL OUTER JOIN PostgreSQL
  5. LEFT JOIN vs INNER JOIN 차이
  6. 실무에서 자주 쓰는 패턴

1. LEFT JOIN 개념

시작하며

여러분이 회원 목록을 조회하는데, 아직 주문을 하지 않은 회원도 함께 보고 싶을 때 이런 상황을 겪어본 적 있나요? INNER JOIN을 사용하면 주문 기록이 있는 회원만 나타나서, 주문하지 않은 회원은 목록에서 사라져버립니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 고객 관리 시스템이나 대시보드를 만들 때, "아직 활동하지 않은 사용자"나 "구매 이력이 없는 회원"도 함께 파악해야 하는 경우가 많습니다.

INNER JOIN만 사용하면 이런 중요한 정보를 놓치게 됩니다. 바로 이럴 때 필요한 것이 LEFT JOIN입니다.

LEFT JOIN은 왼쪽 테이블의 모든 데이터를 유지하면서 오른쪽 테이블의 매칭되는 데이터를 가져옵니다. 매칭되지 않아도 왼쪽 데이터는 모두 보여줍니다.

개요

간단히 말해서, LEFT JOIN은 왼쪽 테이블의 모든 행을 결과에 포함시키고, 오른쪽 테이블에서 매칭되는 데이터가 있으면 함께 보여주고, 없으면 NULL로 표시하는 조인 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터 분석이나 리포팅 작업에서 "전체 데이터"를 유지하면서 관련 정보를 결합해야 할 때가 많습니다.

예를 들어, 전체 상품 목록을 보면서 각 상품의 판매 실적을 확인하고 싶을 때, 아직 한 번도 팔리지 않은 상품도 함께 봐야 재고 관리가 가능합니다. 기존에는 INNER JOIN으로 매칭되는 데이터만 가져왔다면, 이제는 LEFT JOIN으로 기준 테이블의 모든 데이터를 유지하면서 추가 정보를 결합할 수 있습니다.

LEFT JOIN의 핵심 특징은 첫째, 왼쪽 테이블의 모든 행이 항상 결과에 포함됩니다. 둘째, 오른쪽 테이블에 매칭되는 데이터가 없으면 NULL 값이 들어갑니다.

셋째, 데이터 손실 없이 안전하게 테이블을 결합할 수 있습니다. 이러한 특징들이 데이터 무결성을 유지하면서도 풍부한 정보를 제공하기 때문에 중요합니다.

코드 예제

-- 회원 테이블 (왼쪽)
-- users: id, name, email

-- 주문 테이블 (오른쪽)
-- orders: id, user_id, product_name, order_date

-- 모든 회원을 보면서 주문 정보도 함께 조회
SELECT
    u.id,
    u.name,
    u.email,
    o.product_name,
    o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.id;

-- 결과: 주문하지 않은 회원도 모두 표시되며,
-- 주문 정보가 없는 경우 product_name과 order_date는 NULL

설명

이것이 하는 일: LEFT JOIN은 기준이 되는 왼쪽 테이블의 모든 행을 결과에 포함시키고, 오른쪽 테이블에서 조인 조건에 맞는 데이터를 찾아서 결합합니다. 마치 왼쪽 테이블을 "주인공"으로 두고, 오른쪽 테이블은 "추가 정보"로 활용하는 것입니다.

첫 번째로, FROM users u 부분은 기준이 되는 왼쪽 테이블을 지정합니다. 이 테이블의 모든 행은 반드시 결과에 포함됩니다.

u는 테이블의 별칭으로, 쿼리를 간결하게 만들어줍니다. 왜 이렇게 하는지 이해하려면, LEFT JOIN에서 "LEFT"가 바로 이 FROM 절에 지정된 테이블을 의미한다는 것을 알아야 합니다.

그 다음으로, LEFT JOIN orders o ON u.id = o.user_id 부분이 실행되면서 각 회원의 id와 주문 테이블의 user_id를 비교합니다. 내부에서는 users 테이블의 각 행마다 orders 테이블을 스캔하면서 매칭되는 행을 찾습니다.

매칭되는 주문이 여러 개면 회원 정보가 반복되어 여러 행으로 나타나고, 매칭되는 주문이 없으면 주문 정보 컬럼들은 NULL로 채워집니다. 마지막으로, SELECT 절에서 지정한 컬럼들이 조합되어 최종적으로 완성된 결과 집합을 만들어냅니다.

ORDER BY u.id는 결과를 회원 ID 순서로 정렬하여 보기 쉽게 만들어줍니다. 여러분이 이 코드를 사용하면 전체 회원 목록을 유지하면서도 각 회원의 주문 정보를 함께 확인할 수 있습니다.

실무에서의 이점은 첫째, 아직 주문하지 않은 신규 회원을 식별할 수 있고, 둘째, 마케팅 타겟을 선정할 때 "비활성 사용자"를 쉽게 찾을 수 있으며, 셋째, 전체 회원 대비 구매 전환율을 계산할 수 있습니다.

실전 팁

💡 LEFT JOIN 결과에서 오른쪽 테이블의 값이 NULL인 행을 찾으면 "매칭되지 않은 데이터"를 쉽게 찾을 수 있습니다. WHERE o.id IS NULL을 추가하면 주문하지 않은 회원만 조회됩니다.

💡 LEFT JOIN을 여러 번 연결할 때는 순서가 중요합니다. 가장 먼저 나오는 테이블이 기준이 되므로, 어떤 데이터를 모두 유지할지 먼저 결정하세요.

💡 성능을 위해 JOIN 조건에 사용되는 컬럼에는 반드시 인덱스를 생성하세요. 특히 user_id 같은 외래 키 컬럼은 인덱스가 필수입니다.

💡 LEFT JOIN 후 COUNT(*)를 사용하면 중복 카운트될 수 있으니, COUNT(DISTINCT u.id) 처럼 DISTINCT를 사용하거나 서브쿼리를 활용하세요.

💡 NULL 값 처리는 COALESCE 함수를 활용하면 편리합니다. COALESCE(o.product_name, '주문 없음')처럼 기본값을 지정할 수 있습니다.


2. NULL 값으로 매칭되지 않은 데이터 처리

시작하며

여러분이 LEFT JOIN을 사용했는데, 결과에 NULL 값이 너무 많아서 보기 불편했던 적 있나요? 또는 "아직 주문하지 않은 회원만" 따로 추출하고 싶은데 어떻게 해야 할지 막막했던 경험이 있을 겁니다.

이런 문제는 LEFT JOIN을 처음 배우는 개발자들이 가장 자주 겪는 어려움입니다. NULL 값은 단순히 "데이터가 없다"는 의미가 아니라, "매칭되지 않았다"는 중요한 정보를 담고 있습니다.

이를 제대로 활용하지 못하면 LEFT JOIN의 진정한 가치를 절반도 사용하지 못하는 것입니다. 바로 이럴 때 필요한 것이 NULL 값 처리 기법입니다.

WHERE 절과 IS NULL 조건을 활용하면 매칭되지 않은 데이터만 필터링하거나, COALESCE 함수로 NULL을 원하는 값으로 대체할 수 있습니다.

개요

간단히 말해서, LEFT JOIN에서 발생하는 NULL 값은 오른쪽 테이블에 매칭되는 데이터가 없다는 신호이며, 이를 활용하면 "차집합" 개념으로 데이터를 분석할 수 있습니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 비즈니스 분석에서 "없는 것"을 찾는 것이 매우 중요하기 때문입니다.

예를 들어, 상품을 등록했지만 아직 판매되지 않은 상품, 회원 가입했지만 로그인하지 않은 사용자, 이메일을 발송했지만 열어보지 않은 수신자 같은 경우에 매우 유용합니다. 기존에는 서브쿼리나 NOT EXISTS를 복잡하게 사용했다면, 이제는 LEFT JOIN과 IS NULL 조합으로 간단하고 직관적으로 해결할 수 있습니다.

NULL 처리의 핵심 특징은 첫째, IS NULL 조건으로 매칭되지 않은 행만 필터링할 수 있습니다. 둘째, COALESCE나 IFNULL 함수로 NULL을 기본값으로 대체할 수 있습니다.

셋째, NULL은 = 연산자로 비교할 수 없고 반드시 IS NULL을 사용해야 합니다. 이러한 특징들이 데이터 분석의 정확성과 가독성을 높이기 때문에 중요합니다.

코드 예제

-- 주문하지 않은 회원만 찾기
SELECT
    u.id,
    u.name,
    u.email
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- o.id IS NULL: 주문 테이블에 매칭되는 행이 없음을 의미

-- NULL 값을 기본값으로 대체하기
SELECT
    u.name,
    COALESCE(o.product_name, '주문 없음') AS product,
    COALESCE(o.order_date, '날짜 없음') AS ordered_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

설명

이것이 하는 일: NULL 값 처리는 LEFT JOIN의 결과에서 오른쪽 테이블에 매칭되지 않은 행을 식별하거나, 빈 값을 사용자 친화적인 텍스트로 변환하는 작업입니다. 마치 "빈칸 찾기" 게임처럼, NULL을 활용하면 데이터의 공백을 정확히 파악할 수 있습니다.

첫 번째로, WHERE o.id IS NULL 조건은 LEFT JOIN 후에 적용됩니다. LEFT JOIN이 먼저 실행되어 모든 회원과 주문 정보를 결합한 뒤, WHERE 절이 그 결과를 필터링합니다.

왜 o.id를 선택했는지 이해하려면, orders 테이블의 PRIMARY KEY인 id는 절대 NULL일 수 없다는 점을 알아야 합니다. 즉, o.id IS NULL이라는 것은 orders 테이블의 행 자체가 존재하지 않는다는 뜻입니다.

그 다음으로, COALESCE(o.product_name, '주문 없음') 함수가 실행되면서 각 행의 product_name 값을 검사합니다. 내부에서는 첫 번째 인자인 o.product_name이 NULL인지 확인하고, NULL이면 두 번째 인자인 '주문 없음'을 반환합니다.

NULL이 아니면 원래 값을 그대로 사용합니다. COALESCE는 여러 인자를 받을 수 있어서 COALESCE(col1, col2, col3, '기본값') 처럼 우선순위대로 NULL이 아닌 첫 값을 선택할 수도 있습니다.

마지막으로, 이러한 NULL 처리를 통해 최종적으로 깔끔하고 의미 있는 결과를 만들어냅니다. 사용자는 NULL이라는 추상적인 값 대신 "주문 없음"이라는 명확한 메시지를 보게 됩니다.

여러분이 이 코드를 사용하면 비활성 사용자를 쉽게 찾아 마케팅 캠페인 타겟으로 활용하거나, 리포트에서 NULL 대신 읽기 쉬운 텍스트를 표시할 수 있습니다. 실무에서의 이점은 첫째, 데이터 품질 검사에 유용하고, 둘째, 사용자 인터페이스에 표시할 때 가독성이 높아지며, 셋째, 비즈니스 인사이트를 빠르게 얻을 수 있습니다.

실전 팁

💡 NULL 체크는 항상 IS NULL 또는 IS NOT NULL을 사용하세요. = NULL이나 != NULL은 작동하지 않습니다. SQL에서 NULL은 "알 수 없는 값"이기 때문에 등호로 비교할 수 없습니다.

💡 여러 컬럼을 체크할 때는 AND/OR를 조합하세요. WHERE o.id IS NULL AND p.id IS NULL 처럼 여러 조건을 결합하면 더 정확한 필터링이 가능합니다.

💡 COALESCE 대신 IFNULL이나 NVL을 사용할 수도 있지만, COALESCE가 SQL 표준이라 대부분의 데이터베이스에서 작동합니다. 이식성을 고려하면 COALESCE를 추천합니다.

💡 숫자 컬럼의 NULL 처리 시 COALESCE(amount, 0)처럼 0으로 대체하면 SUM이나 AVG 계산이 편리합니다. 단, 비즈니스 로직상 0과 NULL의 의미가 다른 경우 주의하세요.

💡 성능이 중요한 쿼리에서는 LEFT JOIN ... WHERE IS NULL 대신 NOT EXISTS를 사용하는 것이 더 빠를 수 있습니다. 데이터 크기에 따라 실행 계획을 확인하세요.


3. RIGHT JOIN 사용법

시작하며

여러분이 LEFT JOIN을 배우고 나서, "그럼 RIGHT JOIN은 뭐지?"라고 궁금해하신 적 있나요? 또는 팀 동료가 작성한 쿼리에서 RIGHT JOIN을 보고 당황했던 경험이 있을 겁니다.

이런 문제는 실제로 RIGHT JOIN이 LEFT JOIN보다 훨씬 덜 사용되기 때문에 발생합니다. 대부분의 개발자는 RIGHT JOIN을 거의 사용하지 않고, 테이블 순서를 바꿔서 LEFT JOIN으로 해결합니다.

하지만 기존 쿼리를 읽거나 레거시 코드를 유지보수할 때 RIGHT JOIN을 이해하지 못하면 곤란한 상황이 생깁니다. 바로 이럴 때 필요한 것이 RIGHT JOIN의 개념입니다.

RIGHT JOIN은 LEFT JOIN의 정반대로, 오른쪽 테이블의 모든 데이터를 유지하면서 왼쪽 테이블의 매칭 데이터를 가져옵니다.

개요

간단히 말해서, RIGHT JOIN은 오른쪽 테이블(JOIN 키워드 뒤에 오는 테이블)의 모든 행을 결과에 포함시키고, 왼쪽 테이블에서 매칭되는 데이터를 결합하는 방식입니다. LEFT JOIN을 좌우 반대로 뒤집은 것과 같습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 사실 대부분의 경우 RIGHT JOIN은 필요하지 않습니다. 테이블 순서를 바꿔서 LEFT JOIN으로 작성하는 것이 더 직관적이기 때문입니다.

예를 들어, A RIGHT JOIN B는 B LEFT JOIN A와 완전히 동일한 결과를 만듭니다. 하지만 복잡한 서브쿼리나 뷰가 포함된 쿼리에서는 테이블 순서를 바꾸기 어려운 경우가 있어, 이럴 때 RIGHT JOIN이 유용할 수 있습니다.

기존에는 LEFT JOIN만 사용했다면, 이제는 RIGHT JOIN도 이해하여 다른 사람의 쿼리를 읽을 수 있고, 필요한 경우 적절히 활용할 수 있습니다. RIGHT JOIN의 핵심 특징은 첫째, 오른쪽 테이블의 모든 행이 결과에 포함됩니다.

둘째, LEFT JOIN을 테이블 순서만 바꿔서 표현할 수 있습니다. 셋째, 가독성 측면에서 LEFT JOIN보다 덜 직관적이라 실무에서 잘 사용되지 않습니다.

이러한 특징들이 코드 리딩 능력과 SQL 이해도를 높이는 데 중요합니다.

코드 예제

-- RIGHT JOIN 예제
SELECT
    u.name,
    o.product_name,
    o.order_date
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- 모든 주문을 표시하되,
-- 회원 정보가 없는 주문(탈퇴한 회원 등)도 포함

-- 위와 동일한 결과를 LEFT JOIN으로 표현
SELECT
    u.name,
    o.product_name,
    o.order_date
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;
-- 테이블 순서만 바꾸면 LEFT JOIN으로 동일한 결과

설명

이것이 하는 일: RIGHT JOIN은 JOIN 키워드 오른쪽에 위치한 테이블(위 예제에서는 orders)의 모든 행을 결과에 포함시키고, 왼쪽 테이블(users)에서 조인 조건에 맞는 데이터를 찾아서 결합합니다. LEFT JOIN과 정확히 반대 방향으로 작동합니다.

첫 번째로, FROM users u 부분은 테이블을 지정하지만, RIGHT JOIN이기 때문에 이 테이블은 "보조" 역할을 합니다. 왜 이렇게 하는지 이해하려면, RIGHT JOIN에서는 FROM 절의 테이블이 아니라 JOIN 절의 테이블이 기준이 된다는 점을 알아야 합니다.

즉, "RIGHT"는 JOIN 키워드의 오른쪽을 의미합니다. 그 다음으로, RIGHT JOIN orders o ON u.id = o.user_id 부분이 실행되면서 모든 주문(orders)을 기준으로 삼고, 각 주문의 user_id와 매칭되는 회원 정보를 users 테이블에서 찾습니다.

내부에서는 orders 테이블의 모든 행이 결과에 포함되며, 매칭되는 회원이 없으면(예: 탈퇴한 회원의 주문) 회원 정보 컬럼들은 NULL로 채워집니다. 마지막으로, 이 쿼리를 LEFT JOIN으로 바꾸면 FROM orders o LEFT JOIN users u가 되어 더 읽기 쉬운 형태가 됩니다.

대부분의 개발자는 LEFT JOIN에 익숙하기 때문에, 가능하면 테이블 순서를 조정하여 LEFT JOIN을 사용하는 것이 팀 협업에 유리합니다. 여러분이 이 코드를 사용하면 레거시 쿼리나 다른 사람이 작성한 코드를 이해할 수 있고, 복잡한 뷰나 서브쿼리 상황에서 유연하게 대처할 수 있습니다.

실무에서의 이점은 첫째, SQL 독해력이 향상되고, 둘째, 다양한 쿼리 패턴을 이해하게 되며, 셋째, 필요한 경우 RIGHT JOIN을 적절히 활용할 수 있습니다.

실전 팁

💡 가능하면 RIGHT JOIN 대신 LEFT JOIN을 사용하세요. 대부분의 팀에서 LEFT JOIN을 표준으로 사용하므로 코드 일관성이 높아집니다.

💡 기존 쿼리에서 RIGHT JOIN을 발견하면, 테이블 순서를 바꿔 LEFT JOIN으로 리팩토링하는 것을 고려하세요. 가독성이 크게 향상됩니다.

💡 복잡한 서브쿼리나 WITH 절과 함께 사용할 때는 RIGHT JOIN이 불가피한 경우도 있습니다. 이럴 때는 주석을 달아 의도를 명확히 하세요.

💡 RIGHT JOIN과 LEFT JOIN은 성능상 차이가 없습니다. 옵티마이저가 동일하게 처리하므로 성능보다는 가독성을 우선하세요.


4. FULL OUTER JOIN PostgreSQL

시작하며

여러분이 두 테이블의 모든 데이터를 빠짐없이 확인하고 싶은데, LEFT JOIN으로는 한쪽만, RIGHT JOIN으로는 반대쪽만 볼 수 있어서 답답했던 적 있나요? 양쪽 테이블의 매칭되지 않은 데이터를 모두 보고 싶은 상황이 있을 겁니다.

이런 문제는 데이터 무결성 검사나 동기화 작업에서 자주 발생합니다. 예를 들어, 두 시스템 간 데이터를 비교할 때 한쪽에만 있는 데이터와 다른 쪽에만 있는 데이터를 동시에 찾아야 하는 경우가 많습니다.

LEFT JOIN과 UNION을 복잡하게 조합하는 것은 비효율적입니다. 바로 이럴 때 필요한 것이 FULL OUTER JOIN입니다.

FULL OUTER JOIN은 양쪽 테이블의 모든 데이터를 결과에 포함시키며, 매칭되지 않은 부분은 NULL로 표시합니다.

개요

간단히 말해서, FULL OUTER JOIN은 LEFT JOIN과 RIGHT JOIN을 합친 것으로, 왼쪽 테이블의 모든 행과 오른쪽 테이블의 모든 행을 결과에 포함시키는 조인 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터 마이그레이션이나 시스템 통합 작업에서 매우 유용합니다.

예를 들어, 구 시스템과 신 시스템의 회원 데이터를 비교할 때, 양쪽에 모두 있는 회원, 구 시스템에만 있는 회원(미이관), 신 시스템에만 있는 회원(신규)을 한 번에 파악할 수 있습니다. 기존에는 LEFT JOIN과 RIGHT JOIN을 UNION으로 합치거나 복잡한 서브쿼리를 사용했다면, 이제는 FULL OUTER JOIN 하나로 간단하게 해결할 수 있습니다.

FULL OUTER JOIN의 핵심 특징은 첫째, 양쪽 테이블의 모든 행이 결과에 포함됩니다. 둘째, 매칭되지 않은 부분은 양쪽 모두 NULL로 표시됩니다.

셋째, PostgreSQL, Oracle, SQL Server에서는 지원하지만 MySQL에서는 지원하지 않습니다(MySQL은 UNION으로 구현해야 함). 이러한 특징들이 데이터 통합과 검증 작업을 효율적으로 만들기 때문에 중요합니다.

코드 예제

-- PostgreSQL에서 FULL OUTER JOIN 사용
SELECT
    COALESCE(old.user_id, new.user_id) AS user_id,
    old.username AS old_username,
    new.username AS new_username,
    CASE
        WHEN old.user_id IS NULL THEN '신규 가입'
        WHEN new.user_id IS NULL THEN '미이관'
        ELSE '정상'
    END AS status
FROM old_users old
FULL OUTER JOIN new_users new
    ON old.user_id = new.user_id;

-- 양쪽에 모두 없는 조합은 존재하지 않으므로,
-- 적어도 한쪽은 항상 데이터가 있음

설명

이것이 하는 일: FULL OUTER JOIN은 두 테이블의 합집합을 만드는 것과 비슷합니다. 왼쪽에만 있는 데이터, 오른쪽에만 있는 데이터, 양쪽에 모두 있는 데이터를 모두 결과에 포함시킵니다.

마치 벤 다이어그램의 두 원을 완전히 합친 것과 같습니다. 첫 번째로, FULL OUTER JOIN old_users old ...

new_users new 부분은 두 테이블의 모든 행을 대상으로 합니다. 왜 이렇게 하는지 이해하려면, FULL OUTER는 "완전한 외부 조인"이라는 의미로, 어느 한쪽도 누락시키지 않겠다는 뜻임을 알아야 합니다.

LEFT나 RIGHT와 달리 양쪽을 동등하게 취급합니다. 그 다음으로, ON old.user_id = new.user_id 조건으로 매칭을 시도합니다.

내부에서는 세 가지 경우가 발생합니다: (1) 양쪽에 모두 존재하는 user_id는 한 행으로 결합됩니다. (2) old_users에만 있는 user_id는 new 쪽 컬럼들이 NULL로 표시됩니다.

(3) new_users에만 있는 user_id는 old 쪽 컬럼들이 NULL로 표시됩니다. 마지막으로, COALESCE(old.user_id, new.user_id)를 사용하여 어느 쪽이든 NULL이 아닌 user_id 값을 선택합니다.

CASE 문으로 각 행의 상태를 분류하여 최종적으로 데이터 동기화 상태를 한눈에 파악할 수 있는 결과를 만들어냅니다. 여러분이 이 코드를 사용하면 데이터 마이그레이션 검증, 시스템 간 동기화 체크, 데이터 품질 감사 등의 작업을 효율적으로 수행할 수 있습니다.

실무에서의 이점은 첫째, 누락된 데이터를 빠르게 발견하고, 둘째, 데이터 불일치를 한 번에 확인하며, 셋째, 복잡한 UNION 쿼리를 단순화할 수 있습니다.

실전 팁

💡 MySQL에서는 FULL OUTER JOIN을 지원하지 않으므로, LEFT JOIN UNION RIGHT JOIN 패턴을 사용해야 합니다. 이식성을 고려하여 데이터베이스를 선택하세요.

💡 FULL OUTER JOIN 결과에서 양쪽이 모두 NULL인 경우는 절대 발생하지 않습니다. 적어도 한쪽은 데이터가 있기 때문입니다.

💡 대용량 테이블에서 FULL OUTER JOIN은 성능 부담이 클 수 있습니다. 가능하면 WHERE 조건으로 데이터를 먼저 필터링하거나, 필요한 경우에만 사용하세요.

💡 COALESCE를 활용하여 양쪽 컬럼을 하나로 합치면 결과가 더 깔끔해집니다. 특히 KEY 컬럼은 반드시 COALESCE로 처리하세요.

💡 데이터 검증 용도로 사용할 때는 WHERE old.id IS NULL OR new.id IS NULL 조건을 추가하여 차이가 있는 행만 확인하면 효율적입니다.


5. LEFT JOIN vs INNER JOIN 차이

시작하며

여러분이 쿼리를 작성할 때, LEFT JOIN을 써야 할지 INNER JOIN을 써야 할지 고민하신 적 있나요? 또는 무심코 INNER JOIN을 사용했다가 필요한 데이터가 결과에서 사라져서 당황했던 경험이 있을 겁니다.

이런 문제는 초급 개발자부터 중급 개발자까지 매우 흔하게 겪는 실수입니다. 조인 타입을 잘못 선택하면 데이터가 누락되어 버그가 발생하거나, 반대로 불필요한 NULL 값이 너무 많아져 성능이 저하될 수 있습니다.

특히 프로덕션 환경에서 이런 실수는 치명적인 비즈니스 영향을 미칠 수 있습니다. 바로 이럴 때 필요한 것이 LEFT JOIN과 INNER JOIN의 명확한 차이를 이해하는 것입니다.

두 조인의 동작 원리와 결과 차이를 정확히 알면, 상황에 맞는 올바른 조인을 선택할 수 있습니다.

개요

간단히 말해서, INNER JOIN은 양쪽 테이블에 모두 매칭되는 데이터만 결과에 포함시키는 반면, LEFT JOIN은 왼쪽 테이블의 모든 데이터를 유지하면서 오른쪽 테이블의 매칭 데이터를 결합합니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 비즈니스 요구사항에 따라 "교집합"이 필요한지 "전체 데이터"가 필요한지가 다르기 때문입니다.

예를 들어, "주문한 고객 목록"을 보려면 INNER JOIN이 적합하지만, "모든 고객과 그들의 주문 여부"를 보려면 LEFT JOIN이 필요합니다. 잘못 선택하면 비즈니스 로직이 틀어집니다.

기존에는 차이를 모르고 습관적으로 한 가지만 사용했다면, 이제는 상황에 맞게 적절한 조인 타입을 선택하여 정확하고 효율적인 쿼리를 작성할 수 있습니다. LEFT JOIN과 INNER JOIN의 핵심 차이점은 첫째, 결과 행 개수가 다릅니다(INNER JOIN이 항상 같거나 적음).

둘째, NULL 처리 방식이 다릅니다(INNER JOIN은 NULL 행이 없음). 셋째, 사용 목적이 다릅니다(INNER JOIN은 교집합, LEFT JOIN은 기준 테이블 유지).

이러한 차이점들이 쿼리 정확성과 성능에 직접적인 영향을 미치기 때문에 중요합니다.

코드 예제

-- INNER JOIN: 주문한 고객만 조회
SELECT
    u.name,
    o.product_name,
    o.order_date
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 결과: 주문이 없는 고객은 제외됨

-- LEFT JOIN: 모든 고객과 주문 정보 조회
SELECT
    u.name,
    o.product_name,
    o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- 결과: 주문이 없는 고객도 포함됨 (주문 정보는 NULL)

-- 실무 예시: 고객별 주문 건수 집계
-- INNER JOIN 사용 시 (주문한 고객만)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
INNER JOIN orders o ON u.id = o.user_id
GROUP BY u.name;

-- LEFT JOIN 사용 시 (모든 고객)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.name;
-- COUNT(o.id)는 NULL을 세지 않으므로 주문 없는 고객은 0

설명

이것이 하는 일: LEFT JOIN과 INNER JOIN은 데이터를 결합하는 방식은 같지만, 결과에 포함시키는 행의 범위가 다릅니다. INNER JOIN은 벤 다이어그램의 교집합, LEFT JOIN은 왼쪽 원 전체를 나타냅니다.

첫 번째로, INNER JOIN은 ON 조건이 TRUE인 행만 결과에 포함시킵니다. 왜 이렇게 하는지 이해하려면, INNER JOIN의 목적이 "양쪽에 모두 존재하는 관계"를 찾는 것임을 알아야 합니다.

주문이 없는 회원은 "주문 관계"가 성립하지 않으므로 제외됩니다. 이는 데이터를 엄격하게 필터링하는 효과가 있습니다.

그 다음으로, LEFT JOIN은 왼쪽 테이블의 모든 행을 먼저 결과에 포함시킨 뒤, 오른쪽 테이블에서 매칭을 시도합니다. 내부에서는 매칭이 실패해도 왼쪽 행을 그대로 유지하고 오른쪽 컬럼들을 NULL로 채웁니다.

이는 "기준 데이터를 절대 잃지 않는" 안전한 조인 방식입니다. 마지막으로, COUNT 집계 함수를 사용할 때 차이가 명확히 드러납니다.

INNER JOIN에서는 주문이 없는 고객이 아예 결과에 없지만, LEFT JOIN에서는 COUNT(o.id)가 0이 되어 "주문 0건"으로 표시됩니다. COUNT(*)를 사용하면 LEFT JOIN에서도 1 이상이 나오므로 주의해야 합니다.

항상 COUNT(특정_컬럼)을 사용하세요. 여러분이 이 차이를 이해하면 리포팅 쿼리에서 누락된 데이터 문제를 방지하고, 비즈니스 요구사항에 정확히 맞는 쿼리를 작성할 수 있습니다.

실무에서의 이점은 첫째, 버그 발생률이 크게 줄어들고, 둘째, 데이터 분석 결과의 신뢰도가 높아지며, 셋째, 성능 최적화 시 적절한 조인 타입을 선택할 수 있습니다.

실전 팁

💡 "모든 A와 관련된 B"가 필요하면 LEFT JOIN, "B가 있는 A만"이 필요하면 INNER JOIN을 사용하세요. 이 기준만 기억해도 90% 이상의 상황을 해결할 수 있습니다.

💡 LEFT JOIN 후 WHERE 절에서 오른쪽 테이블 컬럼 조건을 걸면 INNER JOIN과 같아질 수 있습니다. WHERE o.status = 'completed'처럼 NULL이 아닌 값을 요구하면 매칭되지 않은 행이 제거됩니다.

💡 성능상 INNER JOIN이 일반적으로 더 빠릅니다. 결과 행이 적고, 옵티마이저가 최적화하기 쉽기 때문입니다. 꼭 필요한 경우에만 LEFT JOIN을 사용하세요.

💡 여러 테이블을 조인할 때 조인 타입이 섞이면 순서가 중요합니다. A LEFT JOIN B INNER JOIN C는 예상과 다른 결과를 만들 수 있으니 괄호로 명시하세요.

💡 COUNT, SUM, AVG 같은 집계 함수를 사용할 때는 LEFT JOIN에서 NULL 처리를 항상 확인하세요. COUNT(*)와 COUNT(컬럼)의 결과가 다를 수 있습니다.


6. 실무에서 자주 쓰는 패턴

시작하며

여러분이 LEFT JOIN의 기본 개념은 이해했지만, 실제 프로젝트에서 어떻게 활용해야 할지 막막하신 적 있나요? 또는 복잡한 비즈니스 로직을 SQL로 표현할 때 어떤 패턴을 사용해야 효율적인지 고민하신 경험이 있을 겁니다.

이런 문제는 이론과 실무 사이의 간극 때문에 발생합니다. 책이나 튜토리얼에서는 간단한 예제만 다루지만, 실제 업무에서는 여러 테이블을 복잡하게 조인하고, NULL 처리와 집계를 동시에 해야 하는 경우가 많습니다.

검증된 패턴 없이 접근하면 비효율적이고 버그가 많은 쿼리를 작성하게 됩니다. 바로 이럴 때 필요한 것이 실무에서 검증된 LEFT JOIN 패턴들입니다.

"매칭되지 않은 데이터 찾기", "NULL 안전한 집계", "다중 LEFT JOIN" 같은 패턴을 익히면 대부분의 실무 상황을 효과적으로 해결할 수 있습니다.

개요

간단히 말해서, 실무 LEFT JOIN 패턴은 자주 발생하는 비즈니스 요구사항을 효율적으로 해결하는 검증된 쿼리 구조입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 같은 문제를 매번 새로 고민하는 것보다 검증된 패턴을 활용하는 것이 생산성과 품질 면에서 훨씬 우수하기 때문입니다.

예를 들어, "비활성 사용자 찾기"는 거의 모든 서비스에서 필요한 기능인데, LEFT JOIN + IS NULL 패턴을 알면 5분 안에 해결할 수 있습니다. 기존에는 매번 인터넷을 검색하거나 시행착오를 겪었다면, 이제는 상황별로 적합한 패턴을 바로 적용하여 빠르고 정확한 쿼리를 작성할 수 있습니다.

실무 패턴의 핵심 특징은 첫째, 자주 발생하는 비즈니스 요구사항을 커버합니다. 둘째, 성능과 가독성이 검증되었습니다.

셋째, 약간의 수정만으로 다양한 상황에 적용 가능합니다. 이러한 특징들이 개발 속도와 코드 품질을 동시에 높이기 때문에 중요합니다.

코드 예제

-- 패턴 1: 매칭되지 않은 데이터 찾기 (Anti Join)
SELECT u.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- 활용: 주문하지 않은 고객, 로그인하지 않은 회원 등

-- 패턴 2: NULL 안전한 집계
SELECT
    u.name,
    COUNT(o.id) AS order_count,
    COALESCE(SUM(o.amount), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- COUNT(o.id)는 NULL을 세지 않음 (0 반환)
-- SUM(o.amount)는 NULL이면 NULL이므로 COALESCE로 0 처리

-- 패턴 3: 다중 LEFT JOIN (순서 중요)
SELECT
    u.name,
    o.product_name,
    p.payment_status
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN payments p ON o.id = p.order_id;
-- users 기준으로 모든 데이터 유지
-- orders가 없으면 payments도 자동으로 NULL

-- 패턴 4: 조건부 LEFT JOIN (특정 조건만 조인)
SELECT
    u.name,
    recent_order.product_name
FROM users u
LEFT JOIN (
    SELECT user_id, product_name
    FROM orders
    WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
) recent_order ON u.id = recent_order.user_id;
-- 최근 30일 주문만 조인, 나머지는 NULL

설명

이것이 하는 일: 실무 LEFT JOIN 패턴은 반복적으로 발생하는 데이터 조회 요구사항을 효율적으로 해결하는 템플릿입니다. 마치 요리 레시피처럼, 상황에 맞는 패턴을 선택하여 약간만 수정하면 원하는 결과를 빠르게 얻을 수 있습니다.

첫 번째로, Anti Join 패턴(LEFT JOIN + WHERE IS NULL)은 "A에는 있지만 B에는 없는" 데이터를 찾을 때 사용합니다. 왜 이렇게 하는지 이해하려면, NOT IN이나 NOT EXISTS보다 LEFT JOIN이 때로는 더 직관적이고 성능도 우수할 수 있다는 점을 알아야 합니다.

특히 대용량 데이터에서 인덱스가 잘 설정되어 있으면 매우 효율적입니다. 그 다음으로, NULL 안전 집계 패턴은 GROUP BY와 함께 사용할 때 필수입니다.

내부에서 COUNT(o.id)는 NULL 행을 세지 않아 자동으로 0을 반환하지만, SUM이나 AVG는 NULL을 반환하므로 COALESCE로 감싸야 합니다. 이 차이를 모르면 "총 매출액이 NULL로 나온다"는 버그가 발생합니다.

COUNT(*)는 NULL 행도 세므로 LEFT JOIN에서는 COUNT(특정_컬럼)을 사용하세요. 세 번째로, 다중 LEFT JOIN 패턴은 여러 테이블을 연결할 때 순서가 중요합니다.

users → orders → payments 순서로 조인하면, users에 매칭되는 orders가 없으면 payments는 자동으로 NULL이 됩니다. 이는 논리적으로 당연한데, orders가 없으면 payments가 있을 수 없기 때문입니다.

순서를 바꾸면 결과가 달라질 수 있으니 주의하세요. 마지막으로, 조건부 LEFT JOIN 패턴은 서브쿼리나 WITH 절로 미리 필터링한 데이터만 조인합니다.

이는 성능 최적화에도 유리하고, 비즈니스 로직을 명확히 표현할 수 있습니다. "최근 30일 주문이 있는 고객"을 찾는 것이 아니라 "모든 고객과 그들의 최근 30일 주문"을 보는 것입니다.

여러분이 이 패턴들을 익히면 대부분의 실무 쿼리를 빠르고 정확하게 작성할 수 있습니다. 실무에서의 이점은 첫째, 개발 시간이 크게 단축되고, 둘째, 코드 리뷰에서 검증된 패턴이라 승인이 빠르며, 셋째, 유지보수 시 다른 개발자도 쉽게 이해할 수 있습니다.

실전 팁

💡 Anti Join 패턴은 데이터 검증과 클린징 작업에 매우 유용합니다. "고아 레코드(orphan records)" 찾기, 미완성 프로세스 찾기 등에 활용하세요.

💡 다중 LEFT JOIN에서 WHERE 조건을 추가할 때는 위치를 신중히 선택하세요. WHERE o.status = 'completed'는 결과를 INNER JOIN처럼 만들 수 있습니다. ON 절에 조건을 넣는 것도 고려하세요.

💡 서브쿼리를 사용한 조건부 JOIN은 복잡한 비즈니스 로직을 단계적으로 표현할 수 있습니다. WITH 절(CTE)을 활용하면 가독성이 더욱 향상됩니다.

💡 LEFT JOIN 결과를 다시 LEFT JOIN할 때는 NULL 전파를 고려하세요. 중간 테이블이 NULL이면 다음 테이블도 자동으로 NULL이 됩니다.

💡 성능이 중요한 경우 LEFT JOIN보다 EXISTS나 IN이 더 빠를 수 있습니다. EXPLAIN 으로 실행 계획을 확인하여 최적의 방법을 선택하세요.


#SQL#LEFT JOIN#OUTER JOIN#NULL 처리#조인 패턴#SQL,Database,데이터베이스

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

SQL 실전 종합 프로젝트 완벽 가이드

전자상거래 시스템을 직접 구축하면서 배우는 SQL 실전 프로젝트입니다. DB 설계부터 성능 최적화까지, 실무에서 필요한 모든 SQL 기술을 단계별로 마스터할 수 있습니다. 초급 개발자도 따라하기 쉬운 친절한 가이드로 구성되어 있습니다.

실무 데이터 분석 SQL 완벽 가이드

실제 업무에서 자주 사용하는 SQL 데이터 분석 기법을 단계별로 학습합니다. 매출 집계부터 고객 세분화까지, 실전 대시보드 쿼리 작성 방법을 배워보세요.

데이터 모델링과 정규화 완벽 가이드

데이터베이스 설계의 핵심인 데이터 모델링과 정규화를 초급 개발자 눈높이에서 쉽게 설명합니다. ERD 작성부터 제1~3정규형, 정규화의 장단점, 비정규화 전략, 실무 설계 패턴까지 실전에서 바로 활용할 수 있는 노하우를 담았습니다.

트랜잭션과 ACID 원칙 완벽 가이드

데이터베이스의 핵심 개념인 트랜잭션과 ACID 원칙을 초급 개발자도 쉽게 이해할 수 있도록 실무 예제와 함께 설명합니다. 안전한 데이터 처리를 위한 필수 지식을 친근하게 배워보세요.

인덱스와 쿼리 성능 최적화 완벽 가이드

데이터베이스 성능의 핵심인 인덱스를 처음부터 끝까지 배워봅니다. B-Tree 구조부터 실행 계획 분석까지, 실무에서 바로 사용할 수 있는 인덱스 최적화 전략을 초급자도 이해할 수 있게 설명합니다.