본문 바로가기

IT Story/SQL Story

공통 테이블 식(Common Table Expression, CTE)

주로 데이터베이스에 없는 테이블이 필요할 때 사용하며, 바로 다음에 실행할 SELECT 문에만 사용해야 한다.

 

일반 CTE

CTE의 기본 형식
WITH  [CTE_테이블 이름]  (열 이름1, 열 이름 2, ...)
AS
(
<SELECT 문>
)
SELECT  [열 이름]  FROM  [CTE_테이블 이름]

예제)

WITH cte_stock_price (date, symbol, price)

AS

(

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-01' AND date <= '2021-01-10'

)

SELECT * FROM cte_stock_price WHERE symbol = 'MSFT'

→해석: AS(...)에 포함된 <SELECT>문을 통해 반환된 date, symbol, [close] 열의 값을 WITH 문의 cte_stock_price 테이블의 date, symbol, price 열에 대입한 다음, 마지막 SELECT 문의 cte_stock_price 테이블에서 symbol이 'MSFT'인 조건을 만족하는 모든 행을 출력하겠다.

 

결과

date                                       symbol price
2021-01-08 00:00:00.000 MSFT 219.619995117188
2021-01-07 00:00:00.000 MSFT 218.289993286133
2021-01-06 00:00:00.000 MSFT 212.25
2021-01-05 00:00:00.000 MSFT 217.899993896484
2021-01-04 00:00:00.000 MSFT 217.690002441406

 

CTE에서 정의한 열 개수와 CTE의 <SELECT문>에서 얻은 열 목록이 다르면 에러가 발생

 

UNION 문과 UNION ALL 문으로 CTE 결합하기

이 둘의 차이점은 중복을 제거한 행 포함 여부이다. 

UNION ALL 문으로 CTE 결합

예제)

WITH cte_stock_price (date, symbol, price)

AS

(

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-01' AND date <= '2021-01-10'

UNION ALL

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-02-01' AND date <= '2021-02-07'

)

SELECT * FROM cte_stock_price WHERE symbol = 'MSFT'

→해석: AS(...)에 2개의 SELCET 문이 있는데, UNION ALL을 통해 이들 두 SELECT문의 결과를 모두  cte_stock_price 테이블에 반환하고, 마지막 SELECT 문의 cte_stock_price 테이블에서 symbol이 'MSFT'인 조건을 만족하는 모든 행을 출력하겠다.

결과

date                                      symbol price
2021-01-04 00:00:00.000 MSFT 217.690002441406
2021-01-05 00:00:00.000 MSFT 217.899993896484
2021-01-06 00:00:00.000 MSFT 212.25
2021-01-07 00:00:00.000 MSFT 218.289993286133
2021-01-08 00:00:00.000 MSFT 219.619995117188
2021-02-01 00:00:00.000 MSFT 239.649993896484
2021-02-02 00:00:00.000 MSFT 239.509994506836
2021-02-03 00:00:00.000 MSFT 243
2021-02-04 00:00:00.000 MSFT 242.009994506836
2021-02-05 00:00:00.000 MSFT 242.199996948242

 

중복을 제거한 결과를 보고 싶다면 UNION 문을 사용한다.

※ 주의: UNION 문과 UNION ALL 문은 다른 쿼리로 데이터를 어느 정도 거른다음 사용하는 경우가 많으므로 UNION ALL 문의 사용빈도가 더 높다. 즉, 되도록이면 다른 쿼리에서 중복을 제거한 다음 UNION ALL 문을 사용하라.

 

INTERSECT 문으로 CTE 결합하기

내부 조인과 비슷하나, 내부 조인은 테이블 사이의 조인 조건에 맞는 데이터를 반환하고, INTERSECT 문은 각 쿼리에서 반환한 결과에서 중복 결과를 걸러 내 반환하다는 차이점이 있다. INTERSECT 문은 테이블 A 와 테이블 B의 교집합에 해당한다. 

예제)

WITH cte_stock_price (date, symbol, price)

AS

(

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-01' AND date <= '2021-01-10'

INTERSECT

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-07' AND date <= '2021-01-20'

)

SELECT * FROM cte_stock_price WHERE symbol = 'MSFT'

→해석: AS(...) 내에 2개의 SELECT 문(SELECT1, SELECT2)이 있는데, INTERSECT 문을 통해 SELECT1과 SELECT2의 공통 부분(중복)만을 반환해서, 마지막 SELECT 문의 cte_stock_price 테이블에서 symbol이 'MSFT'인 조건을 만족하는 행을 출력하겠다.  

결과

date                                       symbol price
2021-01-07 00:00:00.000 MSFT 218.289993286133
2021-01-08 00:00:00.000 MSFT 219.619995117188

 

EXCEPT 문으로 CTE 결합하기

NOT IN과 비슷하나, EXCEPT  문은 결괏값에서 중복을 제거한 유일한 행을 반환하고 NOT IN은 중복을 제거하지 않고 반환한다. 또한, CTE에서 먼저 작성한 쿼리 기준으로, 그 다음 작성한 SELECT 문 쿼리와 중복되지 않는 데이터를 반환한다. 

예제)

WITH cte_stock_price (date, symbol, price)

AS

(

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-01' AND date <= '2021-01-10'

EXEPCT

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-07' AND date <= '2021-01-20'

)

SELECT * FROM cte_stock_price WHERE symbol = 'MSFT'

→해석: AS(...) 내에 2개의 SELECT 문(SELECT1, SELECT2)이 있는데, EXCEPT 문을 통해 SELECT1 쿼리를 (2021-01-1~10) 기준으로  SELECT2와의 공통 부분(중복)(2021-01-07~10)을 제거해 (2021-01-1~7) 반환해서, 마지막 SELECT 문의 cte_stock_price 테이블에서 symbol이 'MSFT'인 조건을 만족하는 행을 출력하겠다. 

date                                       symbol price
2021-01-04 00:00:00.000 MSFT 217.690002441406
2021-01-05 00:00:00.000 MSFT 217.899993896484
2021-01-06 00:00:00.000 MSFT 212.25

 

예제) CTE 내부의 SELECT 순서 변경

WITH cte_stock_price (date, symbol, price)

AS

(

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-07' AND date <= '2021-01-20'

EXEPCT

SELECT date, symbol, [close] FROM stock

WHERE date >= '2021-01-01' AND date <= '2021-01-10'

)

SELECT * FROM cte_stock_price WHERE symbol = 'MSFT'

결과

date                                       symbol price
2021-01-11 00:00:00.000 MSFT 217.490005493164
2021-01-12 00:00:00.000 MSFT 214.929992675781
2021-01-13 00:00:00.000 MSFT 216.339996337891
2021-01-14 00:00:00.000 MSFT 213.020004272461
2021-01-15 00:00:00.000 MSFT 212.649993896484
2021-01-19 00:00:00.000 MSFT 216.440002441406
2021-01-20 00:00:00.000 MSFT 224.339996337891

 

재귀 CTE

CTE 결과를 CTE 내부의 쿼리에 재사용함으로써, 반복 실행하는 쿼리 구조를 갖는다. 재귀 CTE는 주로 계층 데이터를 검색할 때 많이 사용한다.

재귀 CTE의 기본 형식
WITH  [CTE_테이블 이름]  (열 이름1, 열 이름 2, ...)
AS
(
<SELECT * FROM 테이블 A>  ←-쿼리1(앵커 멤버)
UNION ALL
<SELECT * FROM 테이블 B JOIN CTE_테이블 이름>  ←쿼리2(재귀 멤버)
)
SELECT * FROM  [CTE_테이블 이름]

기본 형식에서 보듯이 재귀 CTE는 적어도 2개의 CTE 쿼리가 필요하다. 이 때 각 쿼리는 앵커 멤버와 재귀 멤버를 포함해야 한다. 앵커 멤버는 자기 자신 CTE를 참조하지 않는다. 주의할 점은 앵커 멤버는 첫 번째 재귀 멤버 앞에 있어야 하고, 재귀 멤버의 열 자료형은 반드시 앵커 멤버의 열 자료형과 일치해야 한다. 또한 앵커 멤버와 재귀 멤버는 여러 개 정의할 수 있다. 

 

재귀 CTE의 실행 순서
① 최초에 쿼리 1을 실행한다. 이때 쿼리 2의 기본값은 0으로 초기화한다.
② 쿼리 2를 실행한다. 이때 쿼리 2의 기본 값은 1씩 증가한다. 쿼리 1의 결과 행 수만큼 쿼리 2에서 CTE_테이블 이름을 재귀 호출하고, 쿼리 2의 기본값이 1씩 증가하면서 쿼리 1의 결과 행 수에 도달해 결과가 더 없다면 재귀 호출을 중단한다.
③ 외부 SELECT 문에서 과정 ①, ②를 통해 만든 CTE 누적 결과를 검색한다.

예제) 

테이블 생성

IF OBJECT_ID('doit_cte_recursive', 'U') IS NOT NULL

DROP TABLE dbo.doit_cte_recursive

GO

CREATE TABLE dbo.doit_cte_recursive

(

EmployeeID int NOT NULL PRIMARY KEY,

FirstName varchar(50) NOT NULL,

LastName varchar(50) NOT NULL,

ManagerID int NULL

)

GO

INSERT INTO doit_cte_recursive VALUES (101, 'Ken', 'Sanchez', NULL)

INSERT INTO doit_cte_recursive VALUES (102, 'Terri', 'Duffy', 101)

INSERT INTO doit_cte_recursive VALUES (103, 'Roberto', 'Tamburello', 101)

INSERT INTO doit_cte_recursive VALUES (104, 'Rob', 'Walters', 102)

INSERT INTO doit_cte_recursive VALUES (105, 'Gail', 'Erickson', 102)

INSERT INTO doit_cte_recursive VALUES (106, 'Jossef', 'Goldberg', 103)

INSERT INTO doit_cte_recursive VALUES (107, 'Dylan', 'Miller', 103)

INSERT INTO doit_cte_recursive VALUES (108, 'Diane', 'Marghein', 105)

INSERT INTO doit_cte_recursive VALUES (109, 'Gigi', 'Matthew', 105)

INSERT INTO doit_cte_recursive VALUES (110, 'Michael', 'Raheem', 106)

 

SELECT * from doit_cte_recursive

 

결과

  EmployeeID FirstName LastName ManagerID
1 101 Ken Sanchez NULL
2 102 Terri Duffy 101
3 103 Roberto Tamburello 101
4 104 Rob Walters 102
5 105 Gail Erickson 102
6 106 Jossef Goldberg 103
7 107 Dylan Miller 103
8 108 Diane Marghein 105
9 109 Gigi Matthew 105
10 110 Michael Raheem 106

 

WITH cte_recursive (EmpID, FirstName, LastName, MgrID, EmpLevel)

AS

(

SELECT EmployeeID, FirstName, LastName, MgrID,  1

FROM doit_cte_recursive  WHERE  ManagerID IS NULL

UNION ALL

SELECT e.EmployeeID, e.FirstName, e.LastName, e.ManagerID,  r.EmpLevel + 1

FROM doit_cte_recursive As e

             INNER JOIN cte_recursive As r ON e.ManagerID = r.EmpID

)

SELECT 

        FirstName + '  ' + LastName AS FullName, EmpLevel, 

       (SELECT  FirstName + '  ' + LastName FROM doit_cte_recursive

       WHERE EmployeeID = cte_recursive.MgrID) AS Manager

FROM cte_recursive 

ORDER BY EmpLevel, MgrID

  FullName EmpLevel Manager
1 Ken  Sanchez 1 NULL
2 Terri  Duffy 2 Ken  Sanchez
3 Roberto  Tamburello 2 Ken  Sanchez
4 Rob  Walters 3 Terri  Duffy
5 Gail  Erickson 3 Terri  Duffy
6 Jossef  Goldberg 3 Roberto  Tamburello
7 Dylan  Miller 3 Roberto  Tamburello
8 Diane  Marghein 4 Gail  Erickson
9 Gigi  Matthew 4 Gail  Erickson
10 Michael  Raheem 4 Jossef  Goldberg

→해석: 1번째 SELECT 문의 SELECT EmployeeID, FirstName, LastName, MgrID,  1을 보면, 최상위 직원을 검색해 EmpLevel을 1로 지정한다. 2번째 SELECT 문은 1번째 SELECT 문이 반환한 값을 doit_cte_recursive 테이블과 조인하면서 바로 앞 행의 EmpLevel에 1을 더하며 재귀 호출한다. 이때 ManageID와 EmpID가 같은 값을 조인 조건으로 사용해 매니저가 같으면 같은 EmpLevel을 지정한다.  AS(...) 의 결과가 WITH 문의 cte_recursive 테이블에 입력되고, 다시AS(...) 내의 재귀 멤버에서 cte_recursive을 호출하여, 결과를  cte_recursive 테이블에 반환한다. 이 재귀 호출은 테이블 행 끝까지 진행한다. 마지막 SELECT 문은 cte_recursive 테이블을 검색한다. 스칼라 서브 쿼리를 사용해 FullName과 EmpLevel을 검색하고 각 행의 Manage 이름을 검색한다. 

 

퀴즈 4. industry_group 테이블에서 industry 열의 데이터가 Oil에 해당하는 symbol을 industry_group_symbol 테이블에서 검색한 다음, nasdaq_company 테이블에서 해당 symbol의 company_name을 검색하세요(CTE 형식의 코드를 사용해 작성).

 

WITH cte_1
AS
(
SELECT b.symbol
FROM industry_group as a
INNER JOIN industry_group_symbol as b ON a.num = b.num
WHERE a.industry ='Oil'
)
SELECT c.symbol, c.company_name 
FROM nasdaq_company as c 
INNER JOIN cte_1 as d ON c.symbol = d.symbol

 

 

 

 

 

 

 

'IT Story > SQL Story' 카테고리의 다른 글

SQL 함수: 날짜 함수  (0) 2022.12.02
SQL 함수: 문자열 함수  (0) 2022.12.02
서브 쿼리  (0) 2022.11.30
조인(Join): 외부 조인  (0) 2022.11.29
조인(Join): 내부 조인  (0) 2022.11.28