Quick Reference
SELECT posts.title, users.name
FROM posts
JOIN users ON posts.user_id = users.id;양쪽에 대응 행이 있을 때만 남기면 JOIN(INNER JOIN), 왼쪽 목록을 빠짐없이 유지하려면 LEFT JOIN을 씁니다. JOIN 종류보다 먼저 "관계가 없는 왼쪽 행을 버릴지"를 정하고, 오른쪽 테이블 조건은 LEFT JOIN의 의미를 유지하려면 ON에 둡니다.
문법
INNER JOIN은 양쪽에 대응 행이 있는 경우만 결과에 포함한다
JOIN(= INNER JOIN)은 ON 조건을 만족하는 행만 결합합니다. 한쪽에만 있는 행은 결과에서 사라집니다. 예를 들어 posts.user_id가 실제 users.id에 없는 행은 결과에 나타나지 않습니다. 반대로 글이 없는 사용자도 결과에 나타나지 않습니다. "양쪽 모두에 해당 관계가 있는 것만 보겠다"는 의미입니다. 외래 키 제약이 올바르게 설정된 테이블이라면 고아 행이 없어 INNER JOIN 결과가 예상과 일치합니다.
-- 글이 있는 사용자만 반환 (글 없는 사용자는 제외)
SELECT p.title, u.name AS author
FROM posts AS p
JOIN users AS u ON p.user_id = u.id
WHERE p.published = true;JOIN은 먼저 INNER와 LEFT를 구분해서 고르면 된다
입문 단계에서는 INNER JOIN과 LEFT JOIN만 정확히 구분해도 대부분의 조회를 읽을 수 있습니다. "양쪽에 모두 있는 것만 볼지"면 INNER JOIN, "왼쪽 기준 목록은 유지하고 없으면 NULL로 채울지"면 LEFT JOIN이 기본입니다.
-- 양쪽 매칭만
SELECT p.title, u.name
FROM posts AS p
JOIN users AS u ON u.id = p.user_id;
-- 왼쪽(users)은 유지
SELECT u.name, p.title
FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id;LEFT JOIN은 기준 테이블의 모든 행을 유지한다
LEFT JOIN은 왼쪽(FROM) 테이블의 모든 행을 유지하고, 오른쪽 테이블에 대응하는 행이 없으면 NULL로 채웁니다. "글이 없는 사용자도 목록에 포함하되, 글 수는 0으로 표시"처럼 기준 테이블 기준으로 전체 목록이 필요할 때 씁니다. LEFT JOIN 후 오른쪽 테이블의 열이 NULL인 행만 필터링하면 "관계가 없는 행"을 찾는 anti-join 패턴도 만들 수 있습니다.
-- 글이 없는 사용자도 포함 (post_count = 0)
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;
-- 글이 한 번도 없는 사용자만 찾기 (anti-join)
SELECT u.name FROM users AS u
LEFT JOIN posts AS p ON p.user_id = u.id
WHERE p.id IS NULL;INNER JOIN과 LEFT JOIN은 "없어도 되는 행인가"를 기준으로 고르면 된다
양쪽에 모두 있어야 의미가 완성되면 INNER JOIN, 왼쪽 목록은 유지해야 하고 오른쪽은 없어도 되면 LEFT JOIN이 맞습니다. JOIN 종류를 SQL 문법으로 외우기보다, "관계가 없는 행을 버릴지 남길지"를 먼저 정하는 편이 훨씬 빠릅니다.
-- 주문이 있는 사용자만
SELECT u.id, u.name
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
-- 주문이 없어도 사용자 목록은 유지
SELECT u.id, u.name, o.id AS order_id
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id;ON 조건이 잘못되면 행이 폭발적으로 늘어난다
JOIN의 ON 조건이 1:1 또는 1:N 관계를 정확히 표현하지 않으면 카르테시안 곱처럼 행이 폭발합니다. 예를 들어 posts와 tags 사이에 중간 테이블 없이 직접 JOIN하면 각 글에 모든 태그가 매핑되어 기대보다 훨씬 많은 행이 나올 수 있습니다. 결과 행 수가 이상하게 많다면 ON 조건이 올바른지, 중간 연결 테이블이 빠졌는지 먼저 확인합니다.
-- 올바른 다대다 JOIN (중간 테이블 경유)
SELECT p.title, t.name AS tag
FROM posts AS p
JOIN post_tags AS pt ON pt.post_id = p.id
JOIN tags AS t ON t.id = pt.tag_id;JOIN 인덱스는 접근 패턴과 실행 계획으로 결정한다
PostgreSQL은 통계를 바탕으로 Nested Loop, Hash Join, Merge Join 등의 전략을 선택합니다. 조인 키 인덱스는 Nested Loop의 안쪽 테이블 접근, 부모 삭제·키 변경 검사, 자주 쓰는 자식 조회에 도움이 될 수 있지만 모든 JOIN에 자동으로 필요하지는 않습니다. 대규모 조인에서는 Hash Join이 선택될 수 있고, 메모리·데이터 분포에 따라 디스크 작업이 생길 수도 있습니다. EXPLAIN (ANALYZE, BUFFERS)로 전략과 실제 행 수를 확인하는 것이 성능 진단의 출발점입니다.
-- ON 조건 컬럼에 인덱스 확보
CREATE INDEX idx_posts_user_id ON posts (user_id);
-- EXPLAIN으로 JOIN 전략 확인
EXPLAIN ANALYZE
SELECT p.title, u.name
FROM posts AS p
JOIN users AS u ON u.id = p.user_id
WHERE p.created_at >= NOW() - INTERVAL '7 days';
-- Hash Join 또는 Nested Loop (Index Scan) 확인선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 양쪽 테이블에 모두 대응 행이 있는 경우만 필요할 때 | INNER JOIN (= JOIN) |
| 기준 테이블 전체를 유지하고 없으면 NULL로 채울 때 | LEFT JOIN |
| "관계가 없는" 행만 찾아야 할 때 | LEFT JOIN ... WHERE right.id IS NULL |
| 다대다 관계를 JOIN할 때 | 중간 테이블 경유 JOIN |
주의할 점
JOIN의 ON 조건이 부정확하면 결과 행이 예상보다 훨씬 많아질 수 있습니다. 결과가 이상하게 많다면 ON 조건이 올바른 관계 열을 연결하는지, 다대다 관계에서 중간 테이블을 빠뜨리지 않았는지 먼저 확인합니다. 또한 JOIN ON 조건 컬럼에 인덱스가 없으면 대형 테이블 간 JOIN에서 Hash 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;이렇게 LEFT JOIN 뒤에 오른쪽 테이블 조건을 WHERE에 두면, NULL 행이 제거되어 사실상 INNER JOIN처럼 동작합니다. 왼쪽 목록을 유지하려면 조건을 ON 쪽으로 옮기는 편이 맞습니다.
SELECT p.title, t.name
FROM posts AS p
JOIN tags AS t ON t.id = p.id;이처럼 실제 관계 열이 아닌 우연히 같은 값을 가진 열끼리 JOIN하면, 결과 행이 적당히 나오는 것처럼 보여도 의미가 완전히 틀릴 수 있습니다. 결과 수가 맞아 보여도 먼저 관계 키가 맞는지 확인하는 습관이 필요합니다.
참고 링크
1 sources