DB

SQL 레벨업 3장 SQL의 조건 분기

Bryan Lee 2022. 4. 11. 20:29

 

8강 UNION을 사용한 쓸데없이 긴 표현

 

- UNION을 사용한 조건 분기는 SQL 초보자가 좋아하는 기술 중 하나입니다.

  하지만 이런 방법은 성능적인 측면에서 굉장히 큰 단점을 가지고 있습니다. 

  외부적으로는 하나의 SQL 구문을 실행하는 것처럼 보이지만,  

  내부적으로는 여러 개의 SELECT 구문을 실행하는 실행 계획으로 해석되기 때문입니다.  

  따라서 테이블에 접근하는 횟수가 많아져서 I/O 비용이 크게 늘어납니다. 

-> 따라서 SQL에서 조건 분기를 할 때 UNION을 사용해도 좋을지 여부는 신중히 검토해야 합니다. 

    아무 생각 없이 무조건 UNION을 사용해서는 안 됩니다. 

 

1. 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 >= 2002;

 이 코드는 굉장히 큰 문제점을 안고 있습니다 

-> 첫 번째 문제는 쓸데없이 길다는 것입니다

    거의 같은 두 개의 쿼리를 두 번이나 실행하고 있습니다.  

    이는 SQL을 쓸데없이 길고, 읽기 힘들게 만들 뿐입니다.

-> 두 번째 문제는 성능입니다.  

 

- UNION을 사용한 쿼리의 성능 문제를 명확히 하기 위해 실행 계획을 살펴보겠습니다.  

-> 어떤 것을 사용하더라도 UNION 쿼리는 Items 테이블에 2회 접근한다는 것을 알 수 있습니다. 

-> 그리고 그때마다 TABLE ACCESS FULL이 발생하므로,  

    읽어들이는 비용도 테이블의 크기에 따라 선형으로 증가하게 됩니다.  

 

 

2. WHERE 구에서 조건 분기를 하는 사람은 초보자

 

- SQL에는 이러한 격언들이 있습니다.  

  "조건 분기를 WHERE 구로 하는 사람은 초보자다.  

   잘하는 사람은 SELECT 구만으로 조건 분기를 한다."  

-> 지금 살펴보고 있는 문제도 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;

- 이 쿼리도 UNION을 사용한 쿼리와 같은 결과를 출력합니다. 

-> 하지만 성능적으로 이번 쿼리가 훨씬 좋습니다. 

 

 

3. SELECT 구를 사용한 조건 분기의 실행 계획 

 

- CASE 식을 사용하면 Items 테이블에 대한 접근이 1회로 줄어듭니다. 

-> 이전의 UNION을 사용한 구문보다 성능이 2배 좋아졌다고 할 수 있습니다. 

    또한 SQL 구문 자체의 가독성도 굉장히 좋아졌습니다.  

 

- SQL 구문의 성능이 좋은지 나쁜지는 반드시 실행 계획 레벨에서 판단해야 합니다.  

-> 이유는 SQL 구문에는 어떻게 데이터를 검색할지를 나타내는 접근 경로가 쓰여 있지 않기 때문입니다. 

 

- UNION과 CASE의 쿼리를 구문적인 관점에서 비교할 수 있습니다.  

->UNION을 사용한 분기는 SELECT '구문'을 기본 단위로 분기하고 있습니다.  

   구문을 기본 단위로 사용하고 있다는 점에서,

   아직 절차 지향형의 발상을 벗어나지 못한 방법이라고 말할 수 있습니다.

 

- 반면 CASE 식을 사용한 분기는 문자 그대로 '식'을 바탕으로 하는 사고입니다. 

  이렇게 '구문'에서 '식'으로 사고를 변경하는 것이 SQL을 마스터하는 열쇠 중 하나입니다.  

 

 

9강 집계와 조건 분기

 

1. 집계 대상으로 조건 분기

 

- 이 문제를 풀 때 절차 지향적인 사고 방식을 가진다면,  

  일단 남성의 인구를 지역별로 구하고, 

  여성의 인구를 지역별로 구한 뒤 머지(merge)하는 방법을 생각할 것입니다.  

SELECT prefecture, SUM(pop_men) AS pop_men, SUM(pop_wom) AS pop_wom
  FROM (SELECT prefecture, pop AS pop_men, null AS pop_wom  
        FROM Population 
        WHERE sex = '1'
        UNION
        SELECT prefecture, NULL AS pop_men, pop AS pop_wom
        FROM Population
        WHERE sex = '2')TMP
 GROUP BY prefecture;

 

- 하지만 이러한 쿼리의 가장 큰 문제는 WHERE 구에서 sex 필드로 분기를 하고,  

  결과를 UNION으로 머지한다는 절차지향적인 구성에 있습니다. 

 

- 집계의 조건 분기도 CASE 식을 사용할 수 있습니다. 

SELECT prefecture
       SUM(CASE WHEN sex='1' THEN pop ELSE 0 END) AS pop_men,
       SUM(CASE WHEN sex='2' THEN pop ELSE 0 END) AS pop_wom
       FROM Population
GROUP BY prefecture;

- 이렇게 CASE 식으로 조건 분기를 잘 사용하면 UNION을 사용하지 않을 수 있습니다.

  그렇게 되면 테이블에 대한 접근을 줄일 수 있습니다.  

  CASE 식은 SQL에서 굉장히 중요한 도구입니다.  

  다양한 표현을 할 수 있는 것은 물론 성능적으로도 큰 힘을 발휘하기 때문입니다.  

 

 

2. 집약 결과로 조건 분기

 

- 집약에 조건 분기를 적용하는 또 하나의 패턴으로, 

  집약 결과에 조건 분기를 수행하는 경우가 있습니다.  

 

1) UNION으로 조건 분기한 코드 

SELECT emp_name,
       MAX(team) AS team
FROM Employees
GROUP BY emp_name
HAVING COUNT(*) = 1
UNION
SELECT emp_name
       '2개를 겸무' AS team
FROM Employees
GROUP BY emp_name
HAVING COUNT(*) = 2
UNION 
SELECT emp_name 
      '3개 이상을 겸무' AS team
FROM Employees
GROUP BY emp_name
HAVING COUNT(*) >= 3;

- UNION의 실행 계획을 살펴보면,  

  3개의 쿼리를 머지하는 쿼리이므로 예상대로 Employees 테이블에 대한 접근도 3번 발생합니다.  

 

2) CASE 식을 사용한 조건 분기

SELECT emp_name, 
       CASE WHEN COUNT(*) = 1 THEN MAX(team)
       WHEN COUNT(*) = 2 THEN '2개를 겸무'
       WHEN COUNT(*) >= 3 THEN '3개 이상을 겸무'
       END AS team
FROM Employees
GROUP BY emp_name;

- CASE 식을 사용하면 테이블에 접근 비용을 3분의 1로 줄일 수 있습니다.

  추가적으로 GROUP BY의 HASH 연산도 3회에서 1회로 줄어들었습니다. 

 

 

10강. 그래도 UNION이 필요한 경우

 

1) UNION을 사용할 수 밖에 없는 경우

- 머지 대상이 되는 SELECT 구문들에서 사용하는 테이블이 다른 경우가 대표적입니다.

SELECT col_1
   FROM Table_A
   WHERE col_2 = 'A'
UNION ALL
SELECT col_3
   FROM Table_B
   WHERE col_4 = 'B';

- CASE 식을 사용할 수 없다는 것은 아니지만, 

  그렇게 하면 필요 없는 결합이 발생해서 성능적으로 악영향이 발생합니다.  

 

2) UNION을 사용하는 것이 성능적으로 더 좋은 경우

 

- UNION을 사용했을 때 좋은 인덱스(압축을 잘 하는 인덱스)를 사용하지만,  

  이외의 경우에는 테이블 풀 스캔이 발생한다면,  

  UNION을 사용한 방법이 성능적으로 더 좋을 수 있습니다. 

 

- 예를 들어, 3개의 날짜 필드 data_1~date_3과, 

  그것과 짝을 이루는 플래그 필드 flg_1~flg_3을 가진 테이블 ThreeElements를 생각해봅시다.  

 

1) UNION을 사용한 방법

SELECT key, name
       date_1, flg_1,
       date_2, flg_2,
       date_3, flg_3
  FROM ThreeElements 
  WHERE date_1 = '2013-11-01'
  AND flg_1 = 'T'
UNION
  SELECT key, name
       date_1, flg_1,
       date_2, flg_2,
       date_3, flg_3
  FROM ThreeElements
  WHERE date_2 = '2013-11-01'
  AND flg_2 = 'T'
UNION
  SELECT key, name,
         date_1, flg_1,
         date_2, flg_2,
         date_3, flg_3
  FROM ThreeElements
  WHERE date_3 = '2013-11-01'
  AND flg_3 = 'T';

- 이 때, 포인트가 인덱스입니다. 

  이 쿼리를 최적의 성능으로 수행하려면 다음과 같은 필드 조합에 인덱스가 필요합니다.  

CREATE INDEX IDX_1 ON ThreeElements (date_1, flg_1);
CREATE INDEX IDX_2 ON ThreeElements (date_2, flg_2);
CREATE INDEX IDX_3 ON ThreeElements (date_3, flg_3);

 

2) OR을 사용한 방법

- OR를 사용해 조건을 연결할 수 있습니다. 

SELECT key, name
       date_1, flg_1,
       date_2, flg_2,
       date_3, flg_3
FROM ThreeElements
  WHERE (date_1 = '2013-11-01' AND flg_1 = 'T')
  OR (date_2 = '2013-11-01' AND flg_2 = 'T')
  OR (date_3 = '2013-11-01' AND flg_3 = 'T');

- SELECT 구문이 하나로 줄어들었기 때문에

  ThreeElements 테이블에 대한 접근이 1회로 줄어들었습니다.  

  하지만 이 때 인덱스가 사용되지 않고, 그냥 테이블 풀스캔이 수행됩니다.  

  

- 따라서 이러한 경우에 UNION과 OR의 성능 비교는 결국

  3회의 인덱스 스캔 VS 1회의 테이블 풀 스캔 

  중에서 어떤 것이 더 빠른지에 대한 문제가 됩니다.  

-> 이는 테이블 크기와 검색 조건에 따른 선택 비율(레코드 히트율)에 따라 

    답이 달라집니다.  

 

3) IN을 사용한 방법

- OR 쿼리를 IN 쿼리로 변환해서 다음과 같은 코드를 만들 수도 있습니다.

SELECT key, name
       date_1, flg_1,
       date_2, flg_2,
       date_3, flg_3
FROM ThreeElements
WHERE ('2013-11-01', 'T')
      INT ((date_1, flg_1),
          (date_2, flg_2),
          (date_3, flg_3));

- OR을 사용했을 때보다 간단하고 이해하기 쉬울 수 있지만,  

  실행 계획은 OR을 사용할 때와 같습니다. 

  따라서 성능적인 문제도 같습니다.  

 

4) CASE 식을 사용한 방법

SELECT key, name
       date_1, flg_1,
       date_2, flg_2,
       date_3, flg_3
FROM ThreeElements
WHERE CASE WHEN date_1 = '2013-11-01' THEN flg_1
           WHEN date_2 = '2013-11-01' THEN flg_2
           WHEN date_3 = '2013-11-01' THEN flg_3
           ELSE NULL END='T';

- 이 쿼리도 원하는 결과를 만들어냅니다.

  하지만 실행 계획은 OR, IN을 사용할 때와 같습니다.  

  따라서 성능적으로 같은 문제를 안게 됩니다.  

 

 

'DB' 카테고리의 다른 글

SQL 레벨업 6장 결합  (0) 2022.04.25
SQL 레벨업 5장 반복문 (2)  (0) 2022.04.25
SQL 레벨업 5장 반복문 (1)  (0) 2022.04.18
SQL 레벨업 4장 집약과 자르기  (1) 2022.04.18
SQL 레벨업 1장 DBMS 아키텍쳐  (2) 2022.04.04