586. 뷰 조인 조건 밀어 넣기 (Join Predicate Pushdown)
⚠️ 이 문서는 개발자가 "자주 쓰는 조인 쿼리니까 그냥 뷰(View)로 만들어놓고 편하게 써야지~"라고 만든 거대한 뷰를 바깥에서
SELECT할 때, 멍청한 DB가 뷰 안의 1,000만 건 데이터를 다 읽고 조인한 뒤에야 바깥의 조건을 적용해 10분을 낭비하는 끔찍한 사태를 막기 위해, 옵티마이저가 바깥 쿼리에 있는 검색 조건(WHERE)을 뷰 안쪽(괄호 속)으로 깊숙이 강제로 밀어 넣어서(Pushdown), 애초에 뷰가 데이터를 읽을 때부터 딱 필요한 1건만 읽고 조인하도록 수술해 버리는 기적의 최적화 기술을 다룹니다.
핵심 인사이트 (3줄 요약)
- 본질: 뷰(View)나 인라인 뷰(From 절의 서브쿼리) 바깥에 적혀있는 필터링 조건(
WHERE id=1)을, 옵티마이저가 뷰 내부의 쿼리로 끄집어 내려가서 먼저 실행되게 뜯어고치는 쿼리 변환(Query Transformation) 기술이다.- 가치: 1,000만 건 테이블 2개를 통째로 조인(수십 초 소요)한 뒤 1건을 찾는 미친 짓을 막고, 애초에 1건만 찾아서(인덱스 스캔) 조인을 시도(0.01초 소요)하게 만들어 쿼리 속도를 우주 끝까지 끌어올린다.
- 기술 체계: 옵티마이저가 이 마법을 부리려면 뷰 안에 데이터를 뭉개는
GROUP BY나ROWNUM같은 방해물이 없어야(또는 풀 수 있어야) 하며, 힌트(push_pred)를 주어 강제할 수도 있다.
Ⅰ. 뷰(View)의 편의성이 부른 재앙: 쓸데없이 다 읽어버리기
미리 만들어둔 레고 블록(뷰)을 쓰면 편하지만, 컴퓨터는 무식하게 일한다.
- 상황극 (거대한 뷰의 생성):
- 개발자가
고객테이블(100만 건)과주문테이블(1,000만 건)을 엮은[고객_주문_통합_뷰]를 예쁘게 만들어 놨다. - 오늘은 딱 한 명, '홍길동(ID=100)'의 데이터만 보고 싶다.
SELECT * FROM 고객_주문_통합_뷰 WHERE 고객ID = 100;
- 개발자가
- 멍청한 실행 계획 (No Pushdown):
- 옵티마이저가 피곤해서 최적화를 안 했다고 치자.
- DB는 먼저 뷰 안으로 들어간다. 100만 고객과 1,000만 주문을 **전부 다 100% 조인(해시 조인 등)**해서 거대한 1,000만 줄짜리 가상 테이블을 메모리에 낑낑대며 띄워놓는다. (여기서 10분이 걸림)
- 조인이 다 끝나고 나서야 바깥에 있던
WHERE 고객ID = 100조건을 보고, 1,000만 줄 중에서 홍길동의 데이터 3줄을 찾아내어 화면에 뿌려준다. 완벽한 자원 낭비의 극치다.
📢 섹션 요약 비유: 도서관 사서(DB)에게 "1층 과학책과 2층 소설책 명단을 싹 다 합친 장부(View)에서, '홍길동'이 빌려 간 책만 찾아주세요"라고 부탁했습니다. 멍청한 사서는 일단 과학책 1만 권과 소설책 1만 권을 모조리 수작업으로 1권씩 대조해서 '전체 대출 통합 장부 2만 줄'을 1시간 동안 만듭니다(뷰 통째로 실행). 장부를 다 완성하고 나서야 그 장부에서 홍길동 이름 1개를 찾습니다.
Ⅱ. 푸시 다운 (Pushdown)의 마법: 조건을 먼저 안으로 밀어 넣어라
합치고 나서 찾지 마라. 처음부터 1명만 찾아서 합쳐라.
- 옵티마이저의 수술 (Query Transformation):
- 똑똑한 옵티마이저(CBO)는 이 멍청한 쿼리를 보자마자 바깥에 있는 조건(
고객ID=100)을 뜯어낸다. - 그리고 뷰의 껍데기(괄호)를 뚫고 들어가, 뷰 내부의 원본 테이블(
고객테이블) 검색 조건으로 강제로 이식(Pushdown)해 버린다.
- 똑똑한 옵티마이저(CBO)는 이 멍청한 쿼리를 보자마자 바깥에 있는 조건(
- 기적의 실행 계획 (With Pushdown):
- 이제 DB는 뷰를 실행하기 전에
고객테이블의 인덱스(PK)를 먼저 탄다. 100만 명 중 '홍길동(ID=100)' 딱 1명의 데이터만 0.001초 만에 쏙 뽑아낸다. - 그리고 이 홍길동 1명의 정보만을 들고
주문테이블로 넘어가서 조인(Nested Loop Join)을 건다. 주문 테이블도 홍길동의 주문 3건만 찾고 끝난다. - 1,000만 건의 거대 해시 조인이 0.01초짜리 가벼운 인덱스 조인으로 환골탈태했다.
- 이제 DB는 뷰를 실행하기 전에
- 조건 밀어 넣기의 2가지 갈래:
- 조인 조건 밀어 넣기 (Join Predicate Pushdown): 바깥 쿼리가 뷰를 다른 테이블과 조인할 때, 그 조인 키(Key) 값을 뷰 안쪽으로 밀어 넣어주는 고급 기술이다.
- 필터 조건 밀어 넣기 (Filter Pushdown): 단순히 위처럼
WHERE ID=1같은 상숫값을 밀어 넣는 기술이다.
📢 섹션 요약 비유: 똑똑한 사서(옵티마이저)는 무식하게 장부 2만 줄을 다 합치지 않습니다. 고객이 "홍길동 꺼 찾아줘(바깥 조건)"라고 말한 순간, 이 메모지를 머릿속 뷰(장부 만드는 로직) 안으로 쑥 밀어 넣습니다(Pushdown). 사서는 즉시 1층과 2층으로 달려가, 수만 권의 책은 쳐다보지도 않고 오직 대출자에 '홍길동'이라고 적힌 책 딱 3권만 쏙 뽑아와서 그 3권만 가지고 미니 장부를 만들어 1초 만에 건네줍니다. 검색 조건을 작업의 최우선 순위로 끌어올린 극강의 효율입니다.
Ⅲ. 방해물과 한계: 뷰 안에 믹서기가 있으면 못 밀어 넣는다
함부로 뚫고 들어갔다간 통계의 진실이 왜곡될 수 있다.
- 밀어 넣기가 불가능한 조건 (View Merging Failure):
- 옵티마이저가 바깥 조건을 뷰 안으로 밀어 넣으려는데 뷰 안에 '방해물'이 있으면 밀어 넣기를 포기한다.
- 방해물 1:
ROWNUM(오라클 순번): 뷰 안에서 1등부터 10등까지 번호를 매겨놨는데, 바깥 조건을 밀어 넣어서 1명만 찾게 만들면 번호 체계가 다 무너져버린다. - 방해물 2: 복잡한 분석 함수 (Window Function): 뷰 안에서 '전체 부서 평균 대비 내 월급'을 구하고 있는데, 홍길동 1명만 밀어 넣으면 전체 부서 평균을 구할 수 없게 되어 수식이 터진다.
- GROUP BY (믹서기)의 돌파:
- 옛날에는 뷰 안에
GROUP BY 부서번호가 있으면 조건 밀어 넣기를 무조건 포기했다. (홍길동 1명만 밀어 넣으면 그룹이 안 묶이니까.) - 하지만 최신 오라클(11g 이상)은 이마저도 돌파했다. 바깥에서
WHERE 부서번호 = 10이라고 치면, "어? 어차피 부서번호로 그룹핑할 거니까 10번 부서 데이터만 밀어 넣어서 그룹핑하면 되잖아!"라며 **GROUP BY안쪽으로까지 조건을 욱여넣는 미친 최적화(Group By Pushdown)**를 해낸다.
- 옛날에는 뷰 안에
- 힌트(Hint)를 통한 멱살잡이:
- 가끔 옵티마이저가 쫄아서 조건을 안 밀어 넣을 때가 있다.
- 개발자는 쿼리에
/*+ push_pred(view_name) */(오라클 기준)이라는 힌트를 달아서, "야! 안전하니까 쫄지 말고 저 뷰 안으로 조건 무조건 밀어 넣어!"라고 강제로 지시하여 튜닝을 완수한다.
📢 섹션 요약 비유: 과일 주스 레시피(뷰) 안에 "모든 과일을 믹서기(GROUP BY)에 갈아라"라는 지시가 있습니다. 사장님이 밖에서 "딸기 주스만 줘(바깥 조건)"라고 시킵니다. 멍청한 기계는 모든 과일을 다 갈아버린 뒤(뷰 전체 실행)에 딸기 주스만 컵에 따릅니다. 똑똑한 기계(Pushdown)는 이 '딸기'라는 조건을 믹서기 안으로 밀어 넣어서 처음부터 딸기만 믹서기에 넣고 갑니다. 하지만 레시피에 "전체 과일 개수 순위를 매겨라(ROWNUM)"라는 룰이 있다면? 딸기만 넣으면 전체 순위를 매길 수 없어 에러가 나므로, 이때는 어쩔 수 없이 조건을 못 밀어 넣고 무식하게 다 실행해야만 하는 뼈아픈 예외 조항이 존재합니다.