숏컷 코드
SELECT posts.title, users.name
FROM posts
JOIN users ON posts.user_id = users.id;문법
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 성능은 ON 조건 컬럼의 인덱스에 달려있다
PostgreSQL은 JOIN 전략을 Nested Loop, Hash Join, Merge Join 중에서 통계를 기반으로 선택합니다. ON 조건의 컬럼에 인덱스가 있으면 Nested Loop에서 Index Scan을 활용해 빠르게 처리할 수 있습니다. 대규모 테이블 간 JOIN에서는 Hash Join이 선택되는 경우가 많으며, work_mem 설정이 충분하지 않으면 Hash Join이 디스크를 사용해 느려집니다. EXPLAIN ANALYZE로 JOIN 전략과 실제 실행 시간을 확인하는 것이 성능 진단의 출발점입니다.
-- 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