- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 데이터리안
- IF()
- 네이버웹툰
- SQL캠프
- 특정차원
- SQL캠프입문반
- NULL값 있는 컬럼 정렬하기
- 고객세분화분석
- 채용공고
- 재귀쿼리
- 데이터리안 세미나
- 퀵테이블계산
- 아니시에이팅
- 태블로
- 리텐션
- GROUP BY와 NULL
- pmset
- mysql
- Tableau
- SQL
- 데이터분석
- 데이터분석가
- python연동
- 데이터 분석
- TWBX
- 4LS
- 살짝보기
- 대시보드
- 대시보드 체크리스트
- TWBX 7기
ThinkCatLog
[SQL] MySQL 재귀 쿼리 (Recursive Common Table Expressions) 본문
목차
재귀 쿼리
어디에 사용할까?
아래 글은 MySQL 버전 8.0 이상을 기준으로 작성하였습니다. 5.x 버전에서 재귀쿼리(Recursive CTE)를 사용하는 방법은 스택오버플로우 문서를 참고하시길 바랍니다.
재귀 쿼리
재귀 쿼리는 자기 자신을 참조하여 쿼리를 실행하는 것이다. 바로 예시를 확인해보자.
WITH RECURSIVE cte AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 5 -- FROM절에 자신이 참조됨.
)
SELECT * FROM cte;
위의 예시에서 with문의 테이블 명으로 사용한 CTE는 Common Table Expression의 약자이다. CTE는 WITH문을 사용하여 만든 임시 테이블을 말한다. 재귀 쿼리는 영어로 Recursive CTE이다. 즉 WITH문을 사용할 때 FROM절에서 자기 자신을 참조하여 것이다. 재귀 쿼리를 사용할 때에는 WITH 뒤에 RECURSIVE를 사용한다.
재귀 쿼리는 두개의 SELECT절로 이루어지며 각 SELECT절은 UNION으로 구분된다. 첫번째 SELECT절은 Non-Recursive한 부분이고 두번째 SELECT절은 Recursive한 부분이다. 두번째 SELECT절이 반복적으로 자기자신을 참조하여 쿼리를 실행한다.
위의 예시 쿼리를 실행했을 때의 결과이다.
+------+
| n |
+------+
| 1 | -> 첫번째 SELECT 절이 반환한 행
| 2 | -> 첫 행을 기반으로 두번째 SELECT 절이 반환한 행 (1 + 1 = 2)
| 3 |
| 4 |
| 5 | -> n < 5 일 때까지 반복. n = 5에 반복 종료.
+------+
첫번째 SELECT 절은 반복을 시작할 첫번째 행을 만드는 역할이고, 두번째 SELECT 절은 첫번째 SELECT절이 만든 행을 기반으로 반복적으로 행을 만들어낸다. 그리고 WHERE 절에 명시된 조건에 의해 n=5가 되었을 때 반복은 종료된다.
기본적인 구조이다.
-- WITH 절 뒤에 RECURSIVE
WITH RECURSIVE cte AS
(
-- Non-Recursive : 반복을 진행하기 위한 행 세팅
SELECT 1
UNION ALL
-- Recursive : 위의 SELECT절에서 세팅된 행을 기반으로 반복을 진행.
SELECT n + 1
FROM cte -- 자기자신 참조 명시
WHERE n < 5 -- 반복 종료 조건
)
SELECT * FROM cte;
어디에 사용할 수 있을까?
연속적인 값을 생성할 때, 계층 구조를 탐색하기 위한 용도로 사용된다.
연속적인 값 생성
아래와 같은 A라는 이름의 테이블이 있고, 해당 데이터를 가지고 시간대별로 ID갯수를 카운트해야 한다.
CREATE TABLE A (
time int,
id char(10)
);
INSERT INTO A (time, id)
VALUES
(1, "id0"),
(2, "id1"),
(2, "id2"),
(2, "id3"),
(7, "id4"),
(8, "id5"),
(8, "id6"),
(11, "id7"),
(11, "id8"),
(12, "id9")
;
아래 테이블 같이 결과를 요약한 테이블을 만들어 내고 싶다고 할때 사용할 수 있다. 아래 테이블은 0부터 23까지의 모든 데이터를 포함하고 있다.
time | count_id |
0 | 0 |
1 | 1 |
2 | 3 |
... | ... |
22 | 0 |
23 | 0 |
A라는 테이블을 이용하여 단순히 시간대별 ID 갯수를 카운트하면 다음과 같이 쿼리를 작성할 수 있다.
SELECT
time,
COUNT(ID) AS count_id
FROM A
GROUP BY time
결과는 다음과 같이 나올 것이다. 하지만, 원하는 결과와는 차이가 있다. 테이블 시간 컬럼에 0~23시를 모두 포함해야하기 때문이다. 위의 쿼리로는 갯수가 0인 시간을 출력할 수 없다.
time | count_id |
1 | 1 |
2 | 3 |
7 | 1 |
8 | 2 |
11 | 2 |
12 | 1 |
이때 재귀 쿼리를 사용하여 문제를 해결할 수 있다.
WITH RECURSIVE time_table AS (
SELECT 0 AS 'time'
UNION ALL
SELECT time + 1 AS 'time'
FROM time_table
WHERE time < 23
), count_A AS (
SELECT
time,
COUNT(id) AS count_id
FROM A
GROUP BY time
)
SELECT
time_table.time,
IFNULL(count_A.count_id, 0) AS count_id -- A 테이블에 없는 시간대는 NULL로 표시되기 때문에 0으로 치환
FROM time_table
LEFT JOIN count_A
ON time_table.time = count_A.time
계층 구조 탐색
다음과 같은 계층적 구조가 있다고 한다.
계층구조를 나타낸 B라는 이름의 테이블은 다음과 같다.
CREATE TABLE B (
id int,
parent_id int
);
INSERT INTO B (id, parent_id)
VALUES
(1, null),
(2, null),
(3, 2),
(4, 2),
(5, 2),
(6, 4),
(7, 4),
(8, 6)
;
이 테이블 이용해 위 그림처럼 세대를 구분하는 쿼리를 작성해야 한다. 출력 예시는 다음과 같다. gen이라는 컬럼을 생성해야 한다.
id | parent_id | gen |
1 | null | 1 |
2 | null | 1 |
3 | 2 | 2 |
4 | 2 | 2 |
5 | 2 | 2 |
6 | 4 | 3 |
7 | 4 | 3 |
8 | 6 | 4 |
이를 위해 재귀 쿼리를 사용한다.
WITH RECURSIVE B_GEN AS (
SELECT
id,
parent_id,
1 AS GEN
FROM B
WHERE parent_id IS NULL
UNION ALL
SELECT
B.id,
B.parent_id,
GEN + 1 AS GEN
FROM B_GEN
INNER JOIN B
ON B_GEN.id = B.parent_id
)
SELECT *
FROM B_GEN
위 쿼리 실행과정을 나타내면 다음과 같다.
1. 처음에 첫번째 SELECT 절이 실행된다.
첫번째 iteration 실행 결과
id | parent_id | GEN |
1 | null | 1 |
2 | null | 1 |
2. 위의 결과를 기반으로 두번째 재귀 쿼리를 실행한다.
두번째 iteration 실행 결과
id | parent_id | GEN |
3 | 2 | 2 |
4 | 2 | 2 |
5 | 2 | 2 |
3. 두번째 재귀 쿼리 실행 결과가 나오지 않을때까지 반복.
세번째 Iteration 실행결과
id | parent_id | GEN |
6 | 4 | 3 |
7 | 4 | 3 |
네번째 iteration 실행 결과
id | parent_id | GEN |
8 | 6 | 4 |
다섯번째 iteration 실행 결과
없음
4. 반복이 종료 된 후, 실행 결과를 모두 UNION 한다.
UNION 결과
id | parent_id | gen |
1 | null | 1 |
2 | null | 1 |
3 | 2 | 2 |
4 | 2 | 2 |
5 | 2 | 2 |
6 | 4 | 3 |
7 | 4 | 3 |
8 | 6 | 4 |
참고
MySQL 공식 문서
프로그래머스 문제 : 멸종 위기의 대장균 찾기
'데이터 분석 > SQL' 카테고리의 다른 글
[SQL] MySQL - AI로 쿼리 틀린 부분 찾기 (0) | 2024.05.03 |
---|---|
[SQL] MySQL 워크벤치 Incompatible server version 해결 (0) | 2024.03.07 |
[SQL] Pivot Table : 피벗 테이블 (1) | 2024.02.06 |
[SQL] CASE와 IF() (1) | 2024.01.24 |
[SQL] GROUP BY는 NULL을 어떻게 처리할까? (0) | 2024.01.21 |