585. 서브쿼리 언네스팅 (Subquery Unnesting) - 메인 쿼리 조인 변환

⚠️ 이 문서는 개발자가 가독성을 위해 작성한 WHERE id IN (SELECT id FROM ...) 같은 중첩된 서브쿼리(Nested Subquery)가 수천만 건의 데이터를 만났을 때 끔찍한 병목(반복 수행)을 일으키는 것을 막기 위해, **데이터베이스 엔진(옵티마이저)이 똑똑하게 그 괄호를 찢어버리고 메인 쿼리와 서브쿼리를 '하나의 평평한 조인(JOIN)' 형태로 강제 변환하여 실행 속도를 100배 이상 끌어올리는 마법 같은 내부 최적화 기술인 '서브쿼리 언네스팅'**을 다룹니다.

핵심 인사이트 (3줄 요약)

  1. 본질: 중첩된 괄호(둥지, Nest)를 허물어버리는(Un-nest) 작업이다. 서브쿼리가 먼저 혼자 돌아가거나 메인 쿼리가 돌 때마다 반복해서 도는 멍청한 구조를, 양쪽 테이블을 동등한 자격으로 한 번에 펼쳐놓고 엮는(JOIN) 구조로 뜯어고친다.
  2. 가치: 서브쿼리를 쓰면 메인 쿼리의 데이터가 100만 건일 때 서브쿼리가 100만 번 실행되는 '필터(Filter) 오퍼레이션'의 재앙에 빠지기 쉽다. 언네스팅이 발동하면 해시 조인(Hash Join)이나 소트 머지 조인 같은 강력한 대용량 조인 무기를 쓸 수 있게 되어 성능이 수백 배 향상된다.
  3. 기술 체계: 옵티마이저는 함부로 괄호를 찢지 않는다. 찢었을 때 '데이터가 뻥튀기(M:N 조인 증폭)'되는지 수학적으로 검사하며, 만약 뻥튀기가 예상되면 조인 전/후에 중복을 제거(Sort Unique)하는 방어막을 치고 언네스팅을 수행한다.

Ⅰ. 서브쿼리의 저주: 둥지(Nest) 안에서 벌어지는 비효율

사람 눈에 예쁜 코드가 컴퓨터에게는 끔찍한 막노동일 수 있다.

  1. 중첩 서브쿼리 (Nested Subquery):
    • 개발자는 보통 이렇게 짠다. "서울에 사는 고객들의 주문 내역만 뽑아줘!"
    • SELECT * FROM 주문 WHERE 고객ID IN (SELECT 고객ID FROM 고객 WHERE 주소 = '서울')
    • 인간이 읽기엔 너무 편하다. 괄호 안에서 서울 고객을 찾고, 그 명단으로 주문을 뒤지면 되니까.
  2. 필터 오퍼레이션 (Filter Operation)의 공포:
    • 옵티마이저가 멍청하게 이걸 글자 그대로 실행하면 재앙이 터진다.
    • 주문 테이블(메인)에 데이터가 100만 건 있다. 옵티마이저는 주문 1번 줄을 읽고 "이 고객이 서울 사나?" 확인하려고 서브쿼리로 뛰어갔다 온다. 2번 줄을 읽고 또 뛰어갔다 온다.
    • 이 짓을 100만 번 반복한다. 서브쿼리(둥지)가 족쇄가 되어 전체 쿼리의 목을 졸라버리는 끔찍한 병목 현상이다.

📢 섹션 요약 비유: 서브쿼리는 '도서관에서 책을 찾는 방식'과 같습니다. 메인 쿼리(학생 1,000명)가 책을 찾을 때마다, 한 명씩 사서(서브쿼리)에게 뛰어가 "이 책 서울에 있나요?"라고 1,000번 물어보는 짓입니다(필터 오퍼레이션). 사서가 과로사합니다.


Ⅱ. 언네스팅(Unnesting)의 마법: 조인(JOIN)으로의 강제 변환

괄호를 찢고 두 테이블을 동등한 위치로 끌어내라.

  1. 옵티마이저의 수술 (Query Transformation):
    • 똑똑한 옵티마이저는 위 쿼리를 보자마자 괄호(Nest)를 파괴(Un-nesting)해 버린다.
    • 그리고 내부적으로 코드를 아예 이렇게 뜯어고친다.
    • SELECT 주문.* FROM 주문, 고객 WHERE 주문.고객ID = 고객.고객ID AND 고객.주소 = '서울'
  2. 조인(JOIN)으로 얻는 자유와 권력:
    • 괄호를 찢어 조인 형태로 평평하게(Flat) 만들면 엄청난 특권이 생긴다.
    • 100만 번 핑퐁을 칠 필요 없이, 주문 테이블과 고객 테이블을 한 번씩만 쫙 읽어서 메모리에 올려둔 뒤 '해시 조인(Hash Join)' 같은 대용량 특화 무기로 1초 만에 싹 엮어버릴 수 있다.
    • 또한, 옵티마이저가 상황을 보고 "고객 테이블을 먼저 읽을까? 주문을 먼저 읽을까?"라는 실행 순서(Driving Table)를 자기 마음대로 유리하게 바꿀 수 있는 통제권을 쥐게 된다.

📢 섹션 요약 비유: 언네스팅(Unnesting)은 1,000명의 학생이 일일이 사서에게 뛰어가는 짓을 멈추게 합니다. 대신, '주문 책 목록(메인)'과 '서울 고객 목록(서브)'을 거대한 책상 위에 나란히 쫙 펼쳐놓고(조인), 도장 찍기 기계(해시 조인)를 돌려서 두 목록에 이름이 겹치는 것만 1초 만에 쾅쾅쾅 찍어내는 초고속 공장화 작업입니다. 괄호라는 벽을 부수었기에 가능한 일입니다.


Ⅲ. 언네스팅의 딜레마: 데이터 뻥튀기(증폭) 방어전

괄호를 함부로 찢었다간, 100건짜리 결과가 1만 건으로 폭발할 수 있다.

  1. 조인(JOIN)의 부작용 (M:N 증폭):
    • 서브쿼리는 본질적으로 "조건에 맞냐 안 맞냐(EXISTS, IN)"만 확인하는 '체크(Check)' 기능이다. 조건이 맞으면 메인 테이블의 데이터를 1줄만 뱉는다.
    • 그런데 이걸 조인(JOIN)으로 억지로 찢어발기면, 서브 테이블에 똑같은 이름이 3명 있을 때 메인 테이블의 데이터가 3줄로 **다중 복제(데이터 뻥튀기)**되는 치명적 수학 오류가 발생한다.
  2. 옵티마이저의 방어막 (Sort Unique / Semi Join):
    • 그래서 옵티마이저는 괄호를 찢기 전에 수학 검사를 한다. "고객 테이블의 고객ID 컬럼에 PK(기본 키)나 UNIQUE 인덱스가 걸려있나?"
    • PK가 있다면: 어차피 고객 이름이 중복될 리 없으니 뻥튀기 안 됨. 안심하고 100% 조인으로 언네스팅해버린다.
    • PK가 없다면(중복 위험): 옵티마이저는 괄호를 찢긴 찢되, 서브 테이블 데이터를 먼저 중복 제거(Sort Unique)하고 나서 조인을 하거나, 한 번 짝을 찾으면 더 안 찾고 멈추는 **'세미 조인(Semi Join)'**이라는 특수 조인 방식을 써서 뻥튀기를 완벽하게 틀어막는다.

📢 섹션 요약 비유: 괄호를 찢는 언네스팅 수술은 위험합니다. 자칫하면 '홍길동의 주문 내역'이라는 영수증 1장이, 동명이인 홍길동 3명 때문에 영수증 3장으로 복제(데이터 증폭)되어 고객 청구서가 엉망이 되기 때문입니다. 그래서 똑똑한 DB 의사(옵티마이저)는 수술 전에 동명이인(중복 데이터)이 있는지 주민번호(PK)를 먼저 검사하고, 만약 동명이인이 있다면 그들을 한 명으로 묶어버리거나(Sort Unique), 첫 번째 홍길동만 확인하고 쿨하게 뒤돌아서는(Semi Join) 방어 기술을 섞어서 완벽한 수술을 해냅니다.