MySQL 8.0 connect by
MySQL 에서 Oracle connect by 구현하기
Oracle에서 Hierarchy 구조를 구현하는 connect by 구문은 없지만, with recursive 문으로 동일하게 구현할 수 있습니다.
재귀 공통 테이블 표현식은 자체 이름을 참조하는 하위 쿼리가 있는 식입니다. 예를 들어:
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 5
)
SELECT * FROM cte;
명령문을 실행하면 간단한 선형 시퀀스를 포함하는 단일 열인 다음 결과가 생성됩니다.
+------+
| n |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+------+
재귀 CTE(Common Table Expressions)의 구조는 다음과 같습니다.
- WITH 절의 CTE가 자신을 참조하는 경우 WITH 절은 WITH RECURSIVE로 시작해야 합니다. (CTE가 자신을 참조하지 않으면 RECURSIVE가 허용되지만 필수는 아닙니다.) 재귀 CTE에 대한 RECURSIVE를 잊은 경우 다음 오류가 발생할 수 있습니다.
ERROR 1146 (42S02): Table 'cte_name' doesn't exist
- 재귀 CTE 하위 쿼리에는 UNION ALL 또는 UNION [DISTINCT]로 구분되는 두 부분이 있습니다.
SELECT ... -- return initial row set
UNION ALL
SELECT ... -- return additional row sets
- 첫 번째 SELECT는 CTE에 대한 초기 행을 생성하며 CTE 이름을 참조하지 않습니다. 두 번째 SELECT는 추가 행을 생성하고 FROM 절에서 CTE 이름을 참조하여 재귀합니다. 이 부분이 새 행을 생성하지 않으면 재귀가 종료됩니다. 따라서 재귀적 CTE는 비재귀적 SELECT 부분과 재귀적 SELECT 부분으로 구성됩니다.각 SELECT 부분은 그 자체로 여러 SELECT 문의 합집합이 될 수 있습니다.
CTE 결과 열의 유형은 비재귀적 SELECT 부분의 열 유형에서만 유추되며 열은 모두 null을 허용합니다. 유형 결정의 경우 재귀 SELECT 부분은 무시됩니다.
비재귀 부분과 재귀 부분이 UNION DISTINCT로 구분되면 중복 행이 제거됩니다. 이는 무한 루프를 피하기 위해 전이적 폐쇄(transitive closures)를 수행하는 쿼리에 유용합니다.
재귀 부분의 각 반복은 이전 반복에서 생성된 행에서만 작동합니다. 재귀 부분에 여러 쿼리 블록이 있는 경우 각 쿼리 블록의 반복은 지정되지 않은 순서로 예약되며 각 쿼리 블록은 이전 반복이 종료된 이후 또는 다른 쿼리 블록에서 생성된 행에서 작동합니다.
이전에 표시된 재귀적 CTE 하위 쿼리에는 단일 행을 검색하여 초기 행 집합을 생성하는 비재귀적 부분이 있습니다.
SELECT 1
CTE 하위 쿼리에는 다음 재귀 부분도 있습니다.
SELECT n + 1 FROM cte WHERE n < 5
각 반복에서 해당 SELECT는 이전 행 집합의 n 값보다 하나 더 큰 새 값을 가진 행을 생성합니다. 첫 번째 반복은 초기 행 세트(1)에서 작동하고 1+1=2를 생성합니다. 두 번째 반복은 첫 번째 반복의 행 집합(2)에서 작동하고 2+1=3을 생성합니다. 이것은 n이 더 이상 5보다 작지 않을 때 발생하는 재귀가 끝날 때까지 계속됩니다.
CTE의 재귀 부분이 비재귀 부분보다 열에 대해 더 넓은 값을 생성하는 경우 데이터 잘림을 방지하기 위해 비재귀 부분의 열을 확장해야 할 수 있습니다.
WITH RECURSIVE cte AS
(
SELECT 1 AS n, 'abc' AS str
UNION ALL
SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
)
SELECT * FROM cte;
비엄격 SQL 모드에서 명령문은 다음 출력을 생성합니다.
+------+------+
| n | str |
+------+------+
| 1 | abc |
| 2 | abc |
| 3 | abc |
+------+------+
비재귀적 SELECT가 열 너비를 결정하기 때문에 str 열 값은 모두 'abc'입니다. 결과적으로 재귀 SELECT에 의해 생성된 더 넓은 str 값이 잘립니다.
엄격한 SQL 모드에서 명령문은 오류를 생성합니다.
ERROR 1406 (22001): Data too long for column 'str' at row 1
이 문제를 해결하려면 명령문이 잘림이나 오류를 생성하지 않도록 비재귀 SELECT에서 CAST()를 사용하여 str 열을 더 넓게 만듭니다.
WITH RECURSIVE cte AS
(
SELECT 1 AS n, CAST('abc' AS CHAR(20)) AS str
UNION ALL
SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
)
SELECT * FROM cte;
Now the statement produces this result, without truncation:
+------+--------------+
| n | str |
+------+--------------+
| 1 | abc |
| 2 | abcabc |
| 3 | abcabcabcabc |
+------+--------------+
열은 위치가 아닌 이름으로 액세스됩니다. 즉, 이 CTE에서와 같이 재귀 부분의 열은 위치가 다른 비재귀 부분의 열에 액세스할 수 있습니다.
WITH RECURSIVE cte AS
(
SELECT 1 AS n, 1 AS p, -1 AS q
UNION ALL
SELECT n + 1, q * 2, p * 2 FROM cte WHERE n < 5
)
SELECT * FROM cte;
한 행의 p는 이전 행의 q에서 파생되고 그 반대의 경우도 마찬가지이므로 양수 값과 음수 값은 출력의 각 연속 행에서 위치를 바꿉니다.
+------+------+------+
| n | p | q |
+------+------+------+
| 1 | 1 | -1 |
| 2 | -2 | 2 |
| 3 | 4 | -4 |
| 4 | -8 | 8 |
| 5 | 16 | -16 |
+------+------+------+
일부 구문 제약 조건은 재귀 CTE 하위 쿼리 내에서 적용됩니다.
- 재귀 SELECT 부분은 다음 구문을 포함해서는 안 됩니다. MySQL 8.0.19 이전에는 재귀 CTE의 재귀 SELECT 부분도 LIMIT 절을 사용할 수 없었습니다. 이 제한은 MySQL 8.0.19에서 해제되었으며 이제 LIMIT가 선택적 OFFSET 절과 함께 이러한 경우에 지원됩니다. 결과 집합에 미치는 영향은 가장 바깥쪽 SELECT에서 LIMIT를 사용할 때와 동일하지만 재귀 SELECT와 함께 사용하면 요청된 행 수가 생성되는 즉시 행 생성을 중지하므로 더 효율적입니다.
- 이러한 제약 조건은 재귀 CTE의 비재귀 SELECT 부분에는 적용되지 않습니다. DISTINCT에 대한 금지는 UNION 회원에게만 적용됩니다. UNION DISTINCT는 허용됩니다.
- Aggregate functions such as SUM()
- Window functions
- GROUP BY
- ORDER BY
- DISTINCT
- 재귀 SELECT 부분은 하위 쿼리가 아닌 FROM 절에서만 한 번만 CTE를 참조해야 합니다. CTE 이외의 테이블을 참조하고 CTE와 조인할 수 있습니다. 이와 같은 조인에서 사용되는 경우 CTE는 LEFT JOIN의 오른쪽에 있어서는 안 됩니다.
이러한 제약 조건은 ORDER BY, LIMIT(MySQL 8.0.18 및 이전 버전) 및 DISTINCT의 MySQL 관련 제외를 제외한 SQL 표준에서 비롯됩니다.
재귀 CTE의 경우 재귀 SELECT 부분에 대한 EXPLAIN 출력 행은 Extra 열에 Recursive를 표시합니다.
EXPLAIN이 표시하는 예상 비용은 반복당 비용을 나타내며 총 비용과 상당히 다를 수 있습니다. 옵티마이저는 WHERE 절이 false가 되는 시점을 예측할 수 없기 때문에 반복 횟수를 예측할 수 없습니다.
CTE 실제 비용은 결과 집합 크기의 영향을 받을 수도 있습니다. 많은 행을 생성하는 CTE는 메모리 내 형식에서 디스크 형식으로 변환하기에 충분히 큰 내부 임시 테이블이 필요할 수 있으며 성능이 저하될 수 있습니다. 그럴경우 허용되는 메모리 내 임시 테이블 크기를 늘리면 성능이 향상될 수 있습니다.
공통 테이블 표현식 재귀 제한
재귀 CTE의 경우 재귀 SELECT 부분에 재귀를 종료하는 조건이 포함되어 있다는 것이 중요합니다. 런어웨이 재귀 CTE를 방지하기 위한 개발 기법으로 실행 시간을 제한하여 강제 종료할 수 있습니다.
- cte_max_recursion_depth 시스템 변수는 CTE의 재귀 수준 수에 대한 제한을 적용합니다. 서버는 이 변수 값보다 더 많은 수준을 반복하는 모든 CTE의 실행을 종료합니다.
- max_execution_time 시스템 변수는 현재 세션 내에서 실행되는 SELECT 문에 대한 실행 제한 시간을 적용합니다.
- MAX_EXECUTION_TIME 옵티마이저 힌트는 나타나는 SELECT 문에 대해 쿼리당 실행 제한 시간을 적용합니다.
재귀 CTE가 재귀 실행 종료 조건 없이 실수로 작성되었다고 가정합니다.
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte
)
SELECT * FROM cte;
기본적으로 cte_max_recursion_depth 값은 1000이므로 CTE가 1000 수준을 초과하여 재귀하면 종료됩니다. 애플리케이션은 요구 사항에 맞게 세션 값을 변경할 수 있습니다.
SET SESSION cte_max_recursion_depth = 10; -- permit only shallow recursion
SET SESSION cte_max_recursion_depth = 1000000; -- permit deeper recursion
이후에 시작되는 모든 세션에 영향을 미치도록 전역 cte_max_recursion_depth 값을 설정할 수도 있습니다.
쿼리가 느리게 실행되어 반복되는 경우 또는 cte_max_recursion_depth 값을 매우 높게 설정해야 하는 상황에서 깊은 반복을 방지하는 또 다른 방법은 세션당 시간 제한을 설정하는 것입니다. 이렇게 하려면 CTE 문을 실행하기 전에 다음과 같은 문을 실행하십시오.
SET max_execution_time = 1000; -- impose one second timeout
또는 CTE 문 자체에 옵티마이저 힌트를 포함합니다.
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte
)
SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */ * FROM cte;
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte
)
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;
MySQL 8.0.19부터 재귀 쿼리 내에서 LIMIT를 사용하여 가장 바깥쪽 SELECT에 반환할 최대 행 수를 지정할 수도 있습니다. 예를 들면 다음과 같습니다.
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte LIMIT 10000
)
SELECT * FROM cte;
시간 제한을 설정하는 대신 또는 추가로 이 작업을 수행할 수 있습니다. 따라서 다음 CTE는 10,000개 행을 반환하거나 1초(1000밀리초) 동안 실행한 후(둘 중 먼저 발생하는 경우) 종료됩니다.
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte LIMIT 10000
)
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;
실행 시간 제한이 없는 재귀 쿼리가 무한 루프에 빠지면 KILL QUERY를 사용하여 다른 세션에서 종료할 수 있습니다. 세션 자체 내에서 쿼리를 실행하는 데 사용되는 클라이언트 프로그램은 쿼리를 종료하는 방법을 제공할 수 있습니다. 예를 들어, mysql에서 Ctrl+C를 입력하면 현재 명령문이 중단됩니다.
재귀 공통 테이블 표현식 예제
앞서 언급한 바와 같이 재귀 공통 테이블 표현식(CTE)은 계열 생성 및 계층적 또는 트리 구조 데이터 순회에 자주 사용됩니다. 이 섹션에서는 이러한 기술의 몇 가지 간단한 예를 보여줍니다.
피보나치 수열은 두 개의 숫자 0과 1(또는 1과 1)로 시작하며 그 이후의 각 숫자는 이전 두 숫자의 합입니다. 재귀 공통 테이블 표현식은 재귀 SELECT에 의해 생성된 각 행이 계열의 이전 두 숫자에 액세스할 수 있는 경우 피보나치 수열을 생성할 수 있습니다. 다음 CTE는 0과 1을 처음 두 숫자로 사용하여 10개의 숫자 계열을 생성합니다.
WITH RECURSIVE fibonacci (n, fib_n, next_fib_n) AS
(
SELECT 1, 0, 1
UNION ALL
SELECT n + 1, next_fib_n, fib_n + next_fib_n
FROM fibonacci WHERE n < 10
)
SELECT * FROM fibonacci;
CTE는 다음 결과를 생성합니다.
+------+-------+------------+
| n | fib_n | next_fib_n |
+------+-------+------------+
| 1 | 0 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 2 |
| 4 | 2 | 3 |
| 5 | 3 | 5 |
| 6 | 5 | 8 |
| 7 | 8 | 13 |
| 8 | 13 | 21 |
| 9 | 21 | 34 |
| 10 | 34 | 55 |
+------+-------+------------+
CTE 작동 방식:
- n은 행에 n번째 피보나치 수가 포함되어 있음을 나타내는 표시 열입니다. 예를 들어 8번째 피보나치 수는 13입니다.
fib_n 열은 피보나치 수 n을 표시합니다. - next_fib_n 열은 숫자 n 다음의 다음 피보나치 수를 표시합니다. 이 열은 다음 행에 다음 계열 값을 제공하므로 해당 행은 fib_n 열에서 이전 두 계열 값의 합계를 생성할 수 있습니다.
- 재귀는 n이 10에 도달하면 종료됩니다. 이것은 작은 행 세트로 출력을 제한하기 위한 임의의 선택입니다.
앞의 출력은 전체 CTE 결과를 보여줍니다. 일부만 선택하려면 최상위 SELECT에 적절한 WHERE 절을 추가합니다. 예를 들어 8번째 피보나치 수를 선택하려면 다음과 같이 하십시오.
mysql> WITH RECURSIVE fibonacci ...
...
SELECT fib_n FROM fibonacci WHERE n = 8;
+-------+
| fib_n |
+-------+
| 13 |
+-------+
공통 테이블 식은 일련의 연속 날짜를 생성할 수 있으며, 이는 요약 데이터에 표시되지 않은 날짜를 포함하여 계열의 모든 날짜에 대한 행을 포함하는 요약을 생성하는 데 유용합니다.
판매량 테이블에 다음 행이 포함되어 있다고 가정합니다.
mysql> SELECT * FROM sales ORDER BY date, price;
+------------+--------+
| date | price |
+------------+--------+
| 2017-01-03 | 100.00 |
| 2017-01-03 | 200.00 |
| 2017-01-06 | 50.00 |
| 2017-01-08 | 10.00 |
| 2017-01-08 | 20.00 |
| 2017-01-08 | 150.00 |
| 2017-01-10 | 5.00 |
+------------+--------+
이 쿼리는 일일 판매를 요약합니다.
mysql> SELECT date, SUM(price) AS sum_price
FROM sales
GROUP BY date
ORDER BY date;
+------------+-----------+
| date | sum_price |
+------------+-----------+
| 2017-01-03 | 300.00 |
| 2017-01-06 | 50.00 |
| 2017-01-08 | 180.00 |
| 2017-01-10 | 5.00 |
+------------+-----------+
그러나 이 결과에는 테이블에 포함된 날짜 범위에 표시되지 않은 날짜에 대한 "구멍"이 포함되어 있습니다. 범위의 모든 날짜를 나타내는 결과는 판매 데이터에 대한 LEFT JOIN과 조인된 해당 날짜 집합을 생성하는 재귀 CTE를 사용하여 생성할 수 있습니다.
날짜 범위 시리즈를 생성하는 CTE는 다음과 같습니다.
WITH RECURSIVE dates (date) AS
(
SELECT MIN(date) FROM sales
UNION ALL
SELECT date + INTERVAL 1 DAY FROM dates
WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
)
SELECT * FROM dates;
CTE는 다음 결과를 생성합니다.
+------------+
| date |
+------------+
| 2017-01-03 |
| 2017-01-04 |
| 2017-01-05 |
| 2017-01-06 |
| 2017-01-07 |
| 2017-01-08 |
| 2017-01-09 |
| 2017-01-10 |
+------------+
CTE 작동 방식:
- 비재귀적 SELECT는 판매 테이블에 포함된 날짜 범위에서 가장 낮은 날짜를 생성합니다.
- 재귀 SELECT에 의해 생성된 각 행은 이전 행에 의해 생성된 날짜에 하루를 더합니다.
- 날짜가 판매 테이블에 포함된 날짜 범위에서 가장 높은 날짜에 도달하면 재귀가 종료됩니다.
판매 테이블에 대해 LEFT JOIN을 사용하여 CTE를 조인하면 범위의 각 날짜에 대한 행이 있는 판매 요약이 생성됩니다.
WITH RECURSIVE dates (date) AS
(
SELECT MIN(date) FROM sales
UNION ALL
SELECT date + INTERVAL 1 DAY FROM dates
WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
)
SELECT dates.date, COALESCE(SUM(price), 0) AS sum_price
FROM dates LEFT JOIN sales ON dates.date = sales.date
GROUP BY dates.date
ORDER BY dates.date;
출력은 다음과 같습니다.
+------------+-----------+
| date | sum_price |
+------------+-----------+
| 2017-01-03 | 300.00 |
| 2017-01-04 | 0.00 |
| 2017-01-05 | 0.00 |
| 2017-01-06 | 50.00 |
| 2017-01-07 | 0.00 |
| 2017-01-08 | 180.00 |
| 2017-01-09 | 0.00 |
| 2017-01-10 | 5.00 |
+------------+-----------+
몇 가지 참고 사항:
- 쿼리, 특히 재귀 SELECT의 각 행에 대해 MAX() 하위 쿼리가 실행되는 쿼리가 비효율적입니까? EXPLAIN은 MAX()를 포함하는 하위 쿼리가 한 번만 평가되고 결과가 캐시됨을 보여줍니다.
- COALESCE()를 사용하면 판매 테이블에서 판매 데이터가 발생하지 않는 날에 sum_price 열에 NULL이 표시되는 것을 방지할 수 있습니다.
재귀 공통 테이블 표현식은 계층 구조를 형성하는 데이터를 순회하는 데 유용합니다. 회사의 각 직원에 대해 직원 이름과 ID 번호, 직원 관리자의 ID를 표시하는 작은 데이터 세트를 생성하는 다음 명령문을 고려하십시오. 최상위 직원(CEO)의 관리자 ID는 NULL(관리자 없음)입니다.
CREATE TABLE employees (
id INT PRIMARY KEY NOT NULL,
name VARCHAR(100) NOT NULL,
manager_id INT NULL,
INDEX (manager_id),
FOREIGN KEY (manager_id) REFERENCES employees (id)
);
INSERT INTO employees VALUES
(333, "Yasmina", NULL), # Yasmina is the CEO (manager_id is NULL)
(198, "John", 333), # John has ID 198 and reports to 333 (Yasmina)
(692, "Tarek", 333),
(29, "Pedro", 198),
(4610, "Sarah", 29),
(72, "Pierre", 29),
(123, "Adil", 692);
결과 데이터 세트는 다음과 같습니다.
mysql> SELECT * FROM employees ORDER BY id;
+------+---------+------------+
| id | name | manager_id |
+------+---------+------------+
| 29 | Pedro | 198 |
| 72 | Pierre | 29 |
| 123 | Adil | 692 |
| 198 | John | 333 |
| 333 | Yasmina | NULL |
| 692 | Tarek | 333 |
| 4610 | Sarah | 29 |
+------+---------+------------+
각 직원의 관리 체인(즉, CEO에서 직원으로의 경로)이 포함된 조직도를 생성하려면 재귀 CTE를 사용합니다.
WITH RECURSIVE employee_paths (id, name, path) AS
(
SELECT id, name, CAST(id AS CHAR(200))
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, CONCAT(ep.path, ',', e.id)
FROM employee_paths AS ep JOIN employees AS e
ON ep.id = e.manager_id
)
SELECT * FROM employee_paths ORDER BY path;
CTE는 다음 출력을 생성합니다.
+------+---------+-----------------+
| id | name | path |
+------+---------+-----------------+
| 333 | Yasmina | 333 |
| 198 | John | 333,198 |
| 29 | Pedro | 333,198,29 |
| 4610 | Sarah | 333,198,29,4610 |
| 72 | Pierre | 333,198,29,72 |
| 692 | Tarek | 333,692 |
| 123 | Adil | 333,692,123 |
+------+---------+-----------------+
CTE 작동 방식:
- 비재귀적 SELECT는 CEO에 대한 행(NULL 관리자 ID가 있는 행)을 생성합니다.
- 재귀 SELECT에 의해 생성된 더 긴 경로 값을 위한 공간이 있는지 확인하기 위해 경로 열이 CHAR(200)로 확장됩니다.
- 재귀 SELECT에 의해 생성된 각 행은 이전 행에서 생성된 직원에게 직접 보고하는 모든 직원을 찾습니다. 이러한 각 직원에 대해 행에는 직원 ID와 이름, 직원 관리 체인이 포함됩니다. 체인은 관리자의 체인이며 끝에 직원 ID가 추가됩니다.
- 재귀는 직원에게 보고하는 다른 직원이 없을 때 종료됩니다.
특정 직원의 경로를 찾으려면 최상위 SELECT에 WHERE 절을 추가합니다. 예를 들어 Tarek과 Sarah에 대한 결과를 표시하려면 해당 SELECT를 다음과 같이 수정합니다.
mysql> WITH RECURSIVE ...
...
SELECT * FROM employees_extended
WHERE id IN (692, 4610)
ORDER BY path;
+------+-------+-----------------+
| id | name | path |
+------+-------+-----------------+
| 4610 | Sarah | 333,198,29,4610 |
| 692 | Tarek | 333,692 |
+------+-------+-----------------+
유사한 구조와 비교한 공통 테이블 표현식
공통 테이블 표현식(CTE)은 몇 가지 면에서 파생 테이블과 유사합니다.
- 두 구문 모두 이름이 지정됩니다.
- 두 구문 모두 단일 문의 범위에 대해 존재합니다.
이러한 유사성으로 인해 CTE와 파생 테이블은 종종 서로 바꿔서 사용할 수 있습니다. 간단한 예로, 다음 문장은 동일합니다.
WITH cte AS (SELECT 1) SELECT * FROM cte;
SELECT * FROM (SELECT 1) AS dt;
그러나 CTE는 파생 테이블에 비해 몇 가지 장점이 있습니다.
- 파생 테이블은 쿼리 내에서 한 번만 참조할 수 있습니다. CTE는 여러 번 참조될 수 있습니다. 파생 테이블 결과의 여러 인스턴스를 사용하려면 결과를 여러 번 파생시켜야 합니다.
- CTE는 자체 참조(재귀적)일 수 있습니다.
- 하나의 CTE는 다른 CTE를 참조할 수 있습니다.
- CTE는 해당 정의가 문장 안에 포함되지 않고 문장의 시작 부분에 나타날 때 읽기가 더 쉬울 수 있습니다.
CTE는 CREATE [TEMPORARY] TABLE로 생성된 테이블과 유사하지만 명시적으로 정의하거나 삭제할 필요는 없습니다. CTE의 경우 테이블을 생성하는 데 권한이 필요하지 않습니다.