TECH/sql

[PostgreSQL] 단순 JOIN으로는 부족할 때? LATERAL JOIN 완벽 가이드

런코리치 2026. 2. 4. 10:40

안녕하세요! 오늘은 SQL의 성능과 가독성을 한 단계 업그레이드해 줄 수 있는 강력한 기능, LATERAL JOIN에 대해 알아보려고 합니다.

데이터베이스 쿼리를 짜다 보면 "A 테이블의 각 행을 읽으면서, 그 행의 데이터를 변수처럼 사용해서 B 테이블을 조회하고 싶다"는 생각을 할 때가 있습니다. 일반적인 JOIN으로는 해결하기 까다로운 이 문제를 LATERAL 키워드가 어떻게 해결해 주는지 상세히 살펴보겠습니다.


1. LATERAL JOIN이란 무엇인가?

한 줄로 요약하자면 "JOIN 안에서 앞의 테이블 데이터를 참조할 수 있게 해주는 기능"입니다.

보통 SQL에서 JOIN은 양쪽 테이블이 서로 독립적인 상태에서 공통된 키값으로 엮입니다. 하지만 LATERAL을 사용하면 왼쪽(앞쪽)에 있는 테이블의 컬럼 값을 마치 함수의 매개변수(Parameter)처럼 오른쪽 서브쿼리에 전달할 수 있습니다.

자바나 파이썬 같은 프로그래밍 언어의 for each 문이나 Map 함수와 매우 유사한 동작 방식이라고 이해하시면 쉽습니다.


2. 왜 사용하는가? (주요 이점)

① 서브쿼리 내에서 외부 테이블 참조 가능

일반 서브쿼리나 JOIN 내에서는 외부 테이블의 컬럼을 마음대로 가져다 쓸 수 없습니다. LATERAL을 붙이는 순간 서브쿼리 내에서 "아, 지금 보고 있는 행의 ID가 이거구나!"라고 인식하게 됩니다.

② TOP-N 분석의 최적화 (가장 흔한 사례)

"사용자별로 가장 최근 주문 3건씩만 가져오고 싶다"는 요청이 있을 때, 일반 JOIN으로는 모든 주문을 다 가져온 뒤 번호를 매겨서 잘라내야 합니다. 하지만 LATERAL을 쓰면 각 사용자당 3건씩만 딱딱 집어서 가져오기 때문에 데이터가 많을수록 훨씬 효율적입니다.

③ 복잡한 계산식의 재사용

한 쿼리 안에서 특정 계산 값을 여러 번 써야 할 때, LATERAL 서브쿼리에서 계산을 한 번 수행하고 그 결과를 컬럼처럼 여러 곳에서 참조할 수 있어 가독성이 좋아집니다.


3. 실전 예시: 사용자별 최신 로그 조회하기

배치 프로그램이나 통계 화면을 만들 때 가장 자주 마주치는 "각 사용자별로 가장 최근 활동 로그 1개씩만 가져오기"를 예로 들어보겠습니다.

일반적인 방식 (Window Function 사용)

전체 로그를 다 읽어서 순번을 매긴 뒤 1번만 남기는 방식이라 데이터가 많으면 느려집니다.

SQL
SELECT user_id, action, created_at
FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn
    FROM user_logs
) t
WHERE rn = 1;

LATERAL JOIN 방식 (강력 추천)

사용자 한 명당 로그 테이블에서 '딱 하나'만 찾아옵니다. 인덱스가 있다면 성능이 비약적으로 향상됩니다.

SQL
SELECT 
    u.user_id, 
    u.name, 
    log.action, 
    log.created_at
FROM users u
LEFT JOIN LATERAL (
    -- u.user_id를 서브쿼리 내부에서 참조할 수 있습니다!
    SELECT action, created_at 
    FROM user_logs
    WHERE user_id = u.user_id  -- 이 부분이 핵심
    ORDER BY created_at DESC
    LIMIT 1
) log ON true;

4. 고급 활용: 복잡한 로직의 캡슐화

만약 특정 데이터의 상태를 계산하는 로직이 매우 복잡하다면, LATERAL을 이용해 가독성을 높일 수 있습니다.

SQL
SELECT 
    p.product_name,
    calc.discounted_price,
    calc.tax,
    calc.total_price
FROM products p
LEFT JOIN LATERAL (
    -- 복잡한 세금 및 할인율 계산 로직을 별도로 분리
    SELECT 
        p.price * 0.9 AS discounted_price,
        (p.price * 0.9) * 0.1 AS tax,
        (p.price * 0.9) * 1.1 AS total_price
) calc ON true;

이렇게 하면 메인 SELECT 절이 지저분해지지 않고, 계산 결과를 다른 연산에 바로 활용할 수 있습니다.


5. 주의사항 및 성능 팁

  1. 인덱스 활용: LATERAL 서브쿼리 안에서 WHERE 절에 사용되는 컬럼(위 예제의 user_id)에는 반드시 인덱스가 걸려 있어야 합니다. 인덱스가 없다면 루프를 돌 때마다 전체 테이블을 스캔하므로 성능이 최악으로 떨어질 수 있습니다.
  2. PostgreSQL 버전: LATERAL은 PostgreSQL 9.3 버전부터 지원됩니다. (질문하신 12.5나 15 버전에서는 아주 원활하게 작동합니다.)
  3. ON true: LATERAL 조인에서는 조인 조건이 서브쿼리 내부에 이미 포함되어 있는 경우가 많습니다. 이때는 관례적으로 ON true를 사용하여 문법을 완성합니다.

마무리카드

LATERAL JOIN은 처음 접할 때는 생소할 수 있지만, 한 번 익혀두면 복잡한 비즈니스 로직을 SQL 한 줄로 깔끔하게 처리할 수 있는 "마법의 지팡이"가 됩니다.

특히 대용량 배치 작업에서 사용자별/그룹별 데이터를 가공해야 할 때 ROW_NUMBER() 보다 훨씬 직관적이고 빠른 결과를 보여주기도 하죠. 오늘 정리해 드린 키워드인 ON CONFLICT, DO UPDATE, IS DISTINCT FROM 등과 함께 활용하신다면, 더 이상 중복 데이터나 동시성 문제로 고민하지 않는 완벽한 쿼리를 작성하실 수 있을 것입니다!