UNION을 사용한 조건 분기는 SQL 초보자가 좋아하는 기술 중 하나이다. 일반적으로 이러한 조건 분기는, WHERE 구만 조금씩 다른 여러 개의 SELECT 구문을 합쳐서, 복수의 조건에 일치하는 하나의 결과 집합을 얻고 싶을때 사용한다.
이러한 방법은 큰 문제를 작은 문제를 나눌 수 있다는 점에서 생각하기 쉽다는 장점이 있다. 따라서 조건 분기와 관련된 문제를 접할 때 가장 처음 생각할 수 있는 기본적인 방법이다.
하지만 이런 방법은 성능적인 측면에서 굉장히 큰 단점을 가지고 있다. 외부적으로는 하나의 SQL 구문을 실행하는 것처럼 보이지만, 내부적으로는 여러 개의 SELECT 구문을 실행하는 실행 계획으로 해석되기 때문이다. 따라서 테이블에 접근하는 횟수가 많아져서 I/O 비용이 크게 들어난다.
따라서 SQL에서 조건분기를 할때 UNION을 사용해도 좋을지 여부는 신중히 검토해야한다. 아무 생각없이 무조건 UNION을 사용해서는 안된다.
- UNION을 사용한 조건 분기와 관련된 간단한 예제
- 상품 Table
item_id | year | iteam_name | price_tax_ex | price_tax_in |
---|---|---|---|---|
100 | 2000 | 머그컵 | 500 | 525 |
100 | 2001 | 머그컵 | 520 | 546 |
100 | 2002 | 머그컵 | 600 | 630 |
100 | 2003 | 머그컵 | 600 | 630 |
101 | 2000 | 티스푼 | 500 | 525 |
101 | 2001 | 티스푼 | 500 | 525 |
101 | 2002 | 티스푼 | 500 | 525 |
101 | 2003 | 티스푼 | 500 | 525 |
102 | 2000 | 나이프 | 600 | 630 |
102 | 2001 | 나이프 | 550 | 577 |
102 | 2002 | 나이프 | 500 | 577 |
102 | 2003 | 나이프 | 500 | 420 |
- 구하고자 하는 결과
item_name | year | price |
---|---|---|
나이프 | 2000 | 500 |
나이프 | 2001 | 520 |
나이프 | 2002 | 630 |
나이프 | 2003 | 630 |
티스푼 | 2000 | 500 |
티스푼 | 2001 | 500 |
티스푼 | 2002 | 525 |
티스푼 | 2003 | 525 |
나이프 | 2000 | 600 |
나이프 | 2001 | 550 |
나이프 | 2002 | 577 |
나이프 | 2003 | 420 |
- UNION을 사용한 조건 분기
SELECT item_name
,year
,price_tax_ex AS price
FROM Items
WHERE year <= 2001
UNION ALL
SELECT item_name
,year
,price_tax_in AS price
FROM Items
WHERE year > 2001;
위 쿼리는 조건이 배타적이므로 중복된 레코드가 발생하지 않는다. 쓸데없이 정렬 등의 처리를 하지 않아도 되므로 UNION ALL을 사용했다. 하지만 이 코드는 굉장히 큰 문제를 안고있는데, 첫번째 문제는 쓸데없이 길다는 것이다. 거의 같은 두개의 쿼리를 두번이나 실행하고 있다. 이는 쿼리를 쓸데없이 길고, 읽기 힘들게 만들 뿐이다. 두번째 문제는 성능이다.
- UNION을 사용했을 때의 실행 계획 문제
UNION 쿼리는 iteam 테이블에 2회 접근한다는 것을 알수 있습니다. 그리고 그때마다 TABLE ACCESS FULL이 발생하므로, 읽어들이는 비용도 테이블의 크기에 따라 선형으로 증가하게 된다. 물론 데이터 개시에 테이블의 데이터가 있으면 어느 정도 그런 증상이 완화되겠지만, 테이블의 크기가 커지면 캐시 히트율이 낮아지므로 그러한 것도 기대하기 힘들어진다.
- 정확한 판단 없이는 UNION 사용 회피
간단하게 레코드 집합을 합칠 수 있다는 점에서 UNION은 굉장히 편리한 도구이다. 따라서 UNION을 조건 분기를 위해 사용하고 싶은 유혹에 사로잡히는 것도 무리는 아니다. 하지만 이는 굉장히 위험한 생각이다. 정확한 판단 없이 SELECT 구문 전체를 여러 번 사용해서 코드를 길게 만드는 것은 쓸데없는 테이블 접근을 발생 시키며 SQL의 성능을 나쁘게 만든다. 또한 물리 자원(저장소 I/O 비용)도 쓸데없이 소비하게 된다.
- WHERE 구에서 조건 분기를 하는 사람은 초보자
"조건 분기를 WHERE 구로 하는 사람은 초보자다. 잘하는 사람은 SELECT 구만으로 조건분기를 한다."
SELECT item_name
,year
,CASE WHEN year <= 2001 THEN price_tax_ex
WHEN year >= 2002 THEN price_tax_in
END AS price
FROM Items
위 쿼리는 SELECT 구만으로 조건 분기를 한 쿼리이다. 같은 결과를 출력하지만 성능적으로는 UNION을 한 쿼리보다 훨씬 좋다. (테이블의 크기가 커질수록 더욱 명확하게 드러난다.)
- SELECT 구를 사용한 조건 분기의 실행 계획
CASE 식을 사용한 쿼리의 실행은 Item 테이블에 대한 접근이 1회로 줄어든다. 이전의 UNION을 사용한 구문보다 성능이 2배 좋아졌다고 할수 있다.
이처럼 쿼리 성능이 좋은지 나쁜지는 반드시 실행 계획 레벨에서 판단해야한다. SQL 구문에는 어떻게 데이터를 검색할지를 나타내는 접근 경로가 쓰여있지 않기 때문이다. 이를 알려면 실행계획을 보는 수 밖에 없다.
사실 이는 좋은 것이 아니다. "사용자가 데이터에 접근 경로라는 물리 레벨의 문제를 의식하지 않고 싶다"라는 것이 RDB와 SQL이 가진 컨셉이기 때문이다. 하지만 아직 이런 뜻을 이루기에는 현재의 RDB와 SQL(그리고 하드웨어)는 역부족이다. 따라서 은폐하고 있는 접근 경로를 엔지니어가 체크해줘야한다.
어쨌거나 UNION과 CASE의 쿼리를 구문적인 관점에서 비교하면, UNION은 SELECT '구문'을 기본 단위로 분기하고 있다. 구문을 기본단위로 사용하고 있다는 점에서, 아직 절차 지향적 발상을 벗어나지 못한 방법이라고 말할 수 있다. 반면 CASE 식을 사용한 분기는 문자 그대로 '식'을 바탕으로 한 사고이다. 이렇게 '구문'에서 '식'으로 사고를 변경하는 것이 SQL을 마스터 하는 열쇠 중 하나이다.
하나의 요령은 어떤 문제가 있을 때 스스로 "문제를 절차 지향적 언어로 해결한다면 어떤 IF 조건문을 사용해야 할까?"라고 사고할때마다 "이것을 SQL의 CASE로는 어떻게 해결할 수 있지?"라는 것을 꾸준히 의식하는 것