JOIN 차이, INNER JOIN과 LEFT JOIN 한 번에 정리
들어가는 말
INSERT, SELECT와 WHERE를 익히고 나면 데이터를 조회하는 기본기는 갖춰진 셈이다. 하나의 테이블에서 원하는 조건으로 데이터를 걸러내는 것, 그 자체만으로도 꽤 많은 것을 할 수 있다는 느낌이 든다. 그런데 실제 서비스를 개발하다 보면 금방 벽에 부딪히는 순간이 온다. 데이터가 하나의 테이블에만 담겨 있지 않기 때문이다. 참고로 INSERT, SELECT 및 WHERE 구문에 대해 궁금하다면 아래 글을 보고 먼저 보고 오는 것을 추천한다.


실무에서 데이터베이스는 데이터를 역할과 성격에 따라 여러 테이블로 나눠서 관리한다. 사용자 정보는 users 테이블에, 주문 정보는 orders 테이블에, 상품 정보는 products 테이블에 각각 저장된다. 이렇게 분리해서 관리하는 이유는 데이터의 중복을 줄이고, 구조를 명확하게 유지하기 위해서다. 그런데 이 구조에서 “누가 어떤 상품을 주문했는지”를 한 번에 확인하려면 어떻게 해야 할까. 테이블이 나뉘어 있으니 각각 따로 조회해서 손으로 연결해야 할까. 물론 그럴 수 없다.
이 문제를 해결하는 것이 바로 JOIN이다. JOIN은 서로 다른 테이블에 흩어져 있는 데이터를 관계를 기준으로 하나로 묶어서 조회하는 기능이다. 단순히 두 테이블을 붙이는 것이 아니라, 두 테이블 사이의 연결 고리를 정의하고 그 기준으로 데이터를 연결하는 개념이다. 이전 글에서 WHERE 절로 데이터를 필터링하는 방법을 익혔다면, 이번에는 그 필터링을 여러 테이블에 걸쳐서 적용하는 방법으로 확장하는 단계다. 이 글을 조회하고 있다면 JOIN을 사용할 줄은 알지만 INNER JOIN과 LEFT JOIN에 대해 조금 더 알고자 들어온 사람들도 있을 것이다. 그렇다면 잘 찾아왔다. 지금부터 함께 예제들을 통해 JOIN을 이해하고 나면 SQL 활용 범위가 한 단계 이상 올라간다는 것을 직접 느끼게 될 것이다.
JOIN이란 무엇인가, 개념부터 잡고 시작하자
JOIN을 처음 접하는 사람들이 가장 많이 하는 실수는 문법부터 외우려 한다는 것이다. INNER JOIN, LEFT JOIN, RIGHT JOIN이라는 단어를 보고 각각의 문법을 따로따로 암기하려 하면 금방 헷갈린다. JOIN은 문법보다 개념을 먼저 이해하는 것이 훨씬 중요하다.
JOIN의 핵심은 두 테이블 사이의 관계를 정의하는 것이다. 예를 들어 users 테이블에는 사용자 정보가 있고, orders 테이블에는 주문 정보가 있다고 하자. orders 테이블에는 어떤 사용자가 주문했는지를 기록하기 위해 user_id라는 컬럼이 존재한다. 이 user_id가 바로 두 테이블을 연결하는 고리다. JOIN은 이 고리를 기준으로 두 테이블의 데이터를 하나의 결과로 만들어낸다.
SELECT users.name, orders.product
FROM users
INNER JOIN orders ON users.id = orders.user_id;
여기서 ON 절이 하는 역할이 바로 그 연결 고리를 정의하는 것이다. users.id와 orders.user_id가 같은 값을 가진 행끼리 연결하라는 의미다. 이 ON 조건이 없으면 두 테이블의 모든 행이 무작위로 조합되는 카테시안 곱이 발생한다. 데이터가 조금만 많아도 수천만 건의 결과가 만들어지는 상황이 생긴다. ON 조건은 선택이 아니라 필수라는 점을 반드시 기억해야 한다.
JOIN을 이해하는 또 다른 방법은 데이터 구조를 그림으로 그려보는 것이다. 두 테이블을 각각 동그라미로 표현하고, 겹치는 부분이 어디인지를 생각해 보면 각 JOIN 유형의 차이가 훨씬 직관적으로 이해된다. 어떤 데이터를 포함하고 어떤 데이터를 제외할지, 그 기준이 JOIN의 종류를 결정한다.
INNER JOIN과 LEFT JOIN, 이 차이가 핵심이다
JOIN의 종류는 여러 가지가 있지만, 실무에서 가장 많이 사용하는 것은 INNER JOIN과 LEFT JOIN이다. 이 두 가지의 차이를 정확하게 이해하는 것이 JOIN 학습의 핵심이라고 해도 과언이 아니다.
INNER JOIN은 두 테이블 모두에 존재하는 데이터만 결과로 반환한다. 즉, 한쪽 테이블에만 있고 다른 쪽에는 없는 데이터는 결과에서 완전히 제외된다. 앞선 예시에서 주문을 한 번도 하지 않은 사용자가 있다면, 그 사용자는 INNER JOIN 결과에 나타나지 않는다. orders 테이블에 해당 사용자의 데이터가 없기 때문이다.
SELECT users.name, orders.product
FROM users
INNER JOIN orders ON users.id = orders.user_id;
반면 LEFT JOIN은 다르게 동작한다. LEFT JOIN은 왼쪽에 위치한 테이블, 즉 FROM 절에 명시된 테이블의 모든 데이터를 기준으로 결과를 반환한다. 오른쪽 테이블에 연결되는 데이터가 없으면 해당 컬럼은 NULL로 채워진다.
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
이 쿼리를 실행하면 주문이 없는 사용자도 결과에 포함되며, 그 사용자의 orders.product 값은 NULL로 표시된다. 이 차이가 실무에서 매우 중요하게 작용한다. 예를 들어 “가입은 했지만 아직 주문을 하지 않은 사용자 목록을 뽑아라”는 요구사항이 있다면, LEFT JOIN으로 전체 사용자를 가져온 뒤 orders.product IS NULL 조건을 WHERE 절에 추가하면 된다. INNER JOIN으로는 이 결과를 얻을 수 없다.
결국 INNER JOIN은 “두 테이블에 모두 존재하는 데이터만 필요할 때”, LEFT JOIN은 “기준이 되는 테이블의 전체 데이터를 유지하면서 연결 정보를 함께 보고 싶을 때” 사용한다고 정리하면 된다. 이 판단 기준을 머릿속에 잡아두는 것이 JOIN을 제대로 쓰는 출발점이다.
JOIN과 WHERE, 함께 쓸 때 주의할 점
JOIN과 WHERE를 함께 사용하는 것은 매우 자연스러운 패턴이다. 테이블을 연결한 뒤 원하는 조건으로 결과를 필터링하는 구조는 실무에서 가장 빈번하게 등장하는 형태 중 하나다. 그런데 이 둘을 함께 쓸 때 초보자들이 자주 놓치는 함정이 있다. 조건을 ON 절에 거느냐, WHERE 절에 거느냐에 따라 결과가 완전히 달라질 수 있다는 점이다.
INNER JOIN에서는 이 차이가 크게 느껴지지 않는 경우가 많다. 어차피 두 테이블 모두에 존재하는 데이터만 반환하기 때문에, 조건을 ON에 넣든 WHERE에 넣든 대부분 동일한 결과가 나온다. 문제는 LEFT JOIN에서 발생한다.
예를 들어 주문 금액이 10,000원 이상인 데이터만 필터링하고 싶다고 가정하자. 이 조건을 WHERE 절에 넣으면 다음과 같다.
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.price >= 10000;
언뜻 보면 문제없어 보이지만, 이 쿼리는 LEFT JOIN의 특성을 완전히 무너뜨린다. WHERE 조건은 JOIN이 완료된 이후의 결과에 적용된다. orders.price가 NULL인 행, 즉 주문이 없는 사용자는 WHERE 조건을 만족하지 못해 결과에서 제외된다. 결국 INNER JOIN과 동일한 결과가 나오게 된다. LEFT JOIN을 썼지만 실질적으로는 INNER JOIN처럼 동작하는 셈이다.
주문이 없는 사용자도 포함하면서 조건을 적용하고 싶다면 조건을 ON 절로 옮겨야 한다.
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id AND orders.price >= 10000;
이렇게 하면 JOIN 단계에서 조건이 먼저 적용되기 때문에, 주문이 없는 사용자는 NULL을 유지한 채 결과에 포함된다. 조건을 ON에 넣느냐 WHERE에 넣느냐는 단순한 위치 차이가 아니라, 데이터를 어느 시점에 필터링하느냐의 차이다. 이 개념을 이해하지 못하면 쿼리는 오류 없이 실행되는데 결과는 의도와 전혀 다르게 나오는 상황이 반복된다.
정리하면 LEFT JOIN에서 오른쪽 테이블에 대한 조건은 ON 절에 넣고, JOIN 이후 전체 결과에 대한 조건은 WHERE 절에 넣는다는 원칙을 기억하는 것이 좋다. 처음에는 헷갈릴 수 있지만, 이 원칙 하나만 잡아두면 LEFT JOIN 관련 실수의 절반 이상은 사전에 방지할 수 있다.
JOIN을 쓸 때 반드시 고려해야 하는 데이터 구조
JOIN 문법을 익혔다고 해서 바로 실무에 적용할 수 있는 것은 아니다. JOIN을 제대로 사용하려면 테이블 간의 관계 구조를 함께 이해해야 한다. 이 부분을 간과하면 쿼리는 오류 없이 실행되는데 결과가 예상과 전혀 다르게 나오는 상황이 생긴다.
가장 흔하게 발생하는 문제는 데이터 중복이다. 예를 들어 한 명의 사용자가 여러 개의 주문을 가지고 있는 1:N 관계에서 JOIN을 사용하면, 해당 사용자의 정보가 주문 건수만큼 반복해서 나타난다. 이것은 쿼리가 잘못된 것이 아니라 데이터 구조에 따른 정상적인 결과다. 하지만 이 사실을 모르면 “왜 같은 사람이 여러 번 나오지?”라는 혼란이 생긴다.
SELECT users.name, orders.product
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE orders.price >= 10000;
위 쿼리처럼 JOIN 이후에 WHERE 조건을 추가하면 연결된 데이터 중에서 원하는 조건만 필터링할 수 있다. 이 구조가 익숙해지면 데이터 조회를 훨씬 유연하게 구성할 수 있다. SELECT와 WHERE만 쓰던 단계에서 JOIN과 WHERE를 결합하는 단계로 넘어오면, 표현할 수 있는 쿼리의 범위가 비약적으로 넓어진다.
또한 JOIN을 작성하기 전에 항상 “이 두 테이블은 어떤 컬럼으로 연결되는가”를 먼저 파악해야 한다. 연결 고리가 되는 컬럼이 명확하지 않은 상태에서 JOIN을 작성하면, ON 조건이 잘못 설정되어 전혀 엉뚱한 결과가 나올 수 있다. 테이블 설계를 먼저 확인하고, 외래 키 관계가 어떻게 정의되어 있는지를 파악하는 것이 JOIN 작성의 선행 조건이다.
실무에서 JOIN은 이렇게 쓰인다
개념과 문법을 익혔다면 실제로 어떤 맥락에서 JOIN이 사용되는지를 한 번 살펴보는 것이 도움이 된다. 실무에서는 두 테이블만 연결하는 경우보다 세 개 이상의 테이블을 동시에 연결하는 경우가 훨씬 많다. 처음에는 복잡하게 느껴질 수 있지만, 기본 원리는 동일하다. 테이블 간의 관계를 정의하고, 그 기준으로 데이터를 연결하는 것이다.
쇼핑몰 서비스를 예시로 생각해 보자. 사용자 정보를 담은 users 테이블, 주문 정보를 담은 orders 테이블, 상품 정보를 담은 products 테이블이 있다고 가정한다. “누가 어떤 상품을 얼마에 주문했는지”를 한 번에 조회하려면 세 테이블을 모두 연결해야 한다.
SELECT users.name, products.title, orders.price
FROM orders
INNER JOIN users ON orders.user_id = users.id
INNER JOIN products ON orders.product_id = products.id
WHERE orders.price >= 10000;
이 쿼리는 orders 테이블을 기준으로 users와 products를 각각 연결한다. orders 테이블이 users와 products를 이어주는 중심 테이블 역할을 하는 구조다. 실무에서는 이처럼 중심이 되는 테이블을 기준으로 관련 테이블들을 순차적으로 연결하는 패턴이 자주 등장한다.
이 구조에서 중요한 것은 JOIN 순서보다 각 테이블 간의 연결 고리를 정확하게 정의하는 것이다. orders.user_id와 users.id, orders.product_id와 products.id처럼 각 테이블이 어떤 컬럼으로 연결되는지를 먼저 파악하고, 그 관계를 ON 조건으로 명확하게 표현해야 한다. 이 연결 고리가 하나라도 잘못 설정되면 전체 결과가 틀어진다.
실무에서 JOIN을 처음 작성할 때는 한 번에 모든 테이블을 연결하려 하기보다, 두 테이블씩 순서대로 연결하면서 중간 결과를 확인하는 방식으로 접근하는 것이 좋다. 먼저 orders와 users를 연결한 결과를 확인하고, 그 결과가 맞다면 products를 추가로 연결하는 식이다. 이렇게 단계적으로 검증하면서 쿼리를 완성해 나가면 실수를 줄일 수 있고, 문제가 발생했을 때 원인을 찾기도 훨씬 쉽다.
JOIN이 익숙해지면 단순한 데이터 조회를 넘어서 데이터 구조 자체를 읽는 능력이 생긴다. 어떤 테이블이 어떤 테이블과 연결되어 있는지, 그 관계가 어떤 방식으로 설계되어 있는지를 파악하는 것이 곧 서비스의 데이터 구조를 이해하는 것과 같다. JOIN을 잘 다룰 수 있다는 것은 단순히 쿼리를 잘 작성한다는 의미가 아니라, 데이터베이스 전체의 구조를 읽고 활용할 수 있다는 의미이기도 하다.
나오는 말
JOIN은 SQL에서 처음으로 “데이터베이스다운 사고”를 요구하는 개념이다. SELECT와 WHERE는 하나의 테이블 안에서 데이터를 다루는 것이지만, JOIN부터는 테이블 간의 관계를 이해하고 데이터의 흐름을 설계하는 시각이 필요하다. 그렇기 때문에 처음에 어렵게 느껴지는 것은 당연하다. 오히려 처음부터 쉽게 느껴진다면 개념을 제대로 이해하지 못하고 표면만 훑고 있을 가능성이 높다.
중요한 것은 INNER JOIN과 LEFT JOIN의 문법을 외우는 것이 아니라, 어떤 상황에서 어떤 JOIN을 선택해야 하는지를 판단할 수 있는 기준을 갖추는 것이다. 두 테이블 모두에 존재하는 데이터만 필요한가, 아니면 기준 테이블의 전체 데이터를 유지해야 하는가. 이 질문에 답할 수 있다면 JOIN의 핵심은 이미 이해한 것이다.
JOIN을 제대로 이해하고 나면 이후의 학습이 훨씬 수월해진다. GROUP BY로 데이터를 집계할 때도, 서브쿼리를 작성할 때도, 성능 최적화를 고민할 때도 JOIN에 대한 이해가 기반이 된다. 문장을 외우는 것보다 데이터가 어떻게 연결되고 어떻게 흘러가는지를 머릿속으로 그릴 수 있는 수준까지 익히는 것을 목표로 삼길 권한다. 그 수준에 도달하는 순간, SQL은 단순한 조회 도구가 아니라 데이터를 다루는 강력한 언어로 느껴지기 시작할 것이다.
Add your first comment to this post