ON절의 alias 순서가 JOIN에 미치는 영향
SELECT
a.customer_id,
a.customer_name,
b.order_id,
b.order_date
FROM
customers AS a
JOIN
orders AS b
ON
b.customer_id = a.customer_id;
ON절은 왜 다들 이렇게 작성할까
'ON b.customer_id = a.customer_id'
조인을 위한 ON절에서 흔히 조인 대상이 되는 테이블의 alias를 먼저 적은 쿼리를 흔히 볼 수 있다.
SQL JOIN을 처음 가르쳐주시던 강사님도, 회사 선배도 다 저렇게 쓰길래 관례거니 하고 써왔는데 막상 왜 b가 a 앞에 오는지는 제대로 고민해 본 적이 없었다. 왜 다들 JOIN 키워드 뒤에 오는 테이블의 alias를 늘 ON절에서는 앞에서 쓸까. 사실 '친절한 SQL 튜닝'이란 책을 읽다가 'NL 조인 메커니즘을 정확히 이해한 개발자라면 조인문을 아래와 같이(inner 테이블 alias를 왼쪽에)에 기술하는 습관이 자연스레 생긴다'라는 문장을 보기 전까지 이에 대해 딱히 의문을 품어본 적이 없었다. NL 조인의 메커니즘을 이해하기도 전에 감히 생긴 습관에 대해 알아본 바를 정리하고자 한다.
NL조인
Nested Loops 조인 과정은 이중 for문을 통해 데이터를 탐색하는 과정과 유사하다. 데이터베이스에서 탐색에 사용되는 캐싱이나 메모리 최적화, 인덱스 등의 개념은 뒤로 하고 직관적으로 비유하자면 외부 for문이 내부 for문을 감싼 이중 for문의 탐색과정과 같다. 데이터를 순차적으로 탐색하다 조건에 일치하는 것을 찾으면, 즉 ON절에 일치하는 데이터를 찾으면 반환하다.
이를 파이썬 코드로 예시를 들면 아래와 같다.
result_list = []
# outer_list의 각 요소에 대해 inner_list를 반복 탐색
for outer in outer_list:
for inner in inner_list:
# 조건문 (ON절의 조건)
if outer == inner:
# 조건을 만족하는 경우 결과 리스트에 추가
result_list.append((outer, inner))
print(f"{outer} - {inner}\n")
print(result_list)
하지만 이렇게 이중 for문이기만 하면 조인 순서가 성능에 영향을 미치지 않는다. 조인 순서가 성능에 어떤 영향을 미치는지 설명하는데 적합하지 않다. 외부 for문과 내부 for문의 순서가 어떠하든 반복문은 len(outer_list) * len(inner_list)만큼 실행되기 때문이다.
NL조인일 때 조인 순서가 어떤 역할을 하는지 알기 위해서는 대해 NL조인이 '조건에 따라 early return이 가능한' 이중 for문이라고 표현해야 한다.
Early return이 가능한 이중for문을 작성하는 경우 성능을 고려하는 프로그래머라면 아래와 같이 탐색 범위가 좁은 리스트를 외부 for문에, 탐색 범위가 넓은 리스트를 내부 for문으로 작성할 것이다.
result_list = []
# outer_list의 각 요소에 대해 inner_list를 반복 탐색
for outer in outer_list:
match_found = False # 플래그 초기화
for inner in inner_list:
# 조건문: ON절에 해당
if outer == inner:
result_list.append((outer, inner))
match_found = True # 조건 충족
break # 내부 반복문을 조기 종료
if match_found:
print(f"{outer} - {inner}\n")
print(result_list)
프로그래밍과 SQL의 차이는 프로그래밍에서는 외부 for문과 내부 for문이 프로그래머가 작성한 대로 실행된다면 SQL의 경우 데이터베이스 엔진, 옵티마이저가 인덱스와 데이터의 분포 등을 종합적으로 고려하여 판단하기 때문에 실제 실행계획은 달라질 수 있다는 점이다.
JOIN을 하더라도 NL조인일지 해시 조인일지 소트 조인일지 실행계획을 보기 전에는 모른다. 어떤 경우 NL일지 추측은 가능하나 많은 테이블이 조인되고 조건이 복잡해질수록 실제 실행계획은 예측하기는 더 어려워진다.
조인의 기준이 되는 테이블과 탐색 대상이 되는 테이블
조인 조건의 기준이 되는 테이블을 드라이빙 테이블(이끌고 가는 테이블), 탐색 대상이 되는 테이블을 드라이븐 테이블(끌려가는 테이블)이라 부른다. JOIN 키워드 앞에 있는지 뒤에 있는지에 따라 드라이빙 테이블과 드라이븐 테이블이 결정되진 않는다. 옵티마이저가 데이터베이스 통계정보를 통해 결정한다.
어떨 때 NL조인인가
NL조인의 경우 기준이 되는 드라이빙 테이블의 레코드 수가 드라이븐 테이블에 비해 상대적으로 작고 드라이븐 테이블에 ON절에 사용된 컬럼과 관련한 인덱스가 있을 경우나 WHERE 조건절에 의해 필터 되는 레코드 수가 적으리라 예상되는 경우에 주로 실행된다.
드라이빙 테이블의 수가 작다고 해서 반드시 NL조인이 실행되지는 않는다.
옵티마이저의 선택
쉽게 설명하기 위해 위의 파이썬 코드를 작성했는데 조건에 의한 early return이 가능한 경우라면 프로그래머는 두 리스트 중에 상대적으로 길이가 짧은 리스트를 외부 for문에 작성할 것이다.
이처럼 옵티마이저도 NL조인을 한다면 범위가 작은 테이블을 드라이븐 테이블로 삼을 것이다.
ON절의 역할
ON절을 통해 옵티마이저는 JOIN 조건에 대해 해석하고 실행계획을 수립한다. 그러나 ON절에서 어느 테이블이 먼저 나왔느냐가 실행계획에 반영되지는 않는다.
SQL에서 드라이븐 테이블
SQL에서 어떤 표준을 정하고 있지는 않지만 SQL을 작성할 때 일반적으로 데이터의 양이 상대적으로 적고 필터링의 기준이 되는 드라이븐 테이블을 JOIN 뒤에 작성한다고 한다.
그래서 왜 'b.customer_id = a.customer_id'로 작성하는가
JOIN 키워드 뒤에 오는 테이블이 드라이빙 테이블일 것이라는 관례적 이해 하에 의한 것이다. ON절의 작성 순서 자체가 성능에 영향을 미치지는 않는다.
드라이빙 테이블이 JOIN 뒤에 작성되는 관례가 있지만 ON절에서는 무엇을 기준으로 조인이 되는지 명확하게 하기 위해 ON절에서는 드라이빙 테이블의 alias를 앞에 명시하니 'b.customer_id = a.customer_id'의 형태로 작성되는 것이다. 이는 쿼리의 일관성과 가독성을 위한 작성으로 성능에 영향을 미치지는 않는다. 책의 문구도 NL조인의 메커니즘을 이해한다면 어떤 테이블이 기준이 되어야 하는지 알기 때문에 저렇게 작성할 수 있다는 의미였지 성능에 영향을 준다는 뜻은 아닌 듯하다.
조인 성능과 최적화에 영향을 주는 거라 생각해 찾아봤지만 결국 성능에는 영향을 미치지 않는다는 결론에 이르게 되었다. 하지만 이를 계기로 Nested Loop 조인에 대해 자세히 알아보는 계기가 되었다.
'SQL' 카테고리의 다른 글
[Oracle]SQLPlus에서 'set autotrace on' 설정 시 에러 조치 (0) | 2024.06.09 |
---|---|
[SQL][#1][강의]기초 데이터 분석을 위한 핵심 SQL (0) | 2024.03.24 |
댓글