์นดํ
๊ณ ๋ฆฌ ์์
MySQL Recursive CTE ์๋ฒฝ ๊ฐ์ด๋
Data-SSung
2025. 6. 13. 22:28
๋ฐ์ํ
๐ฏ ์ฌ๊ท CTE๋?
ecursive CTE (Common Table Expression)๋ ์๊ธฐ ์์ ์ ์ฐธ์กฐํ๋ ์์ ํ ์ด๋ธ๋ก, ๊ณ์ธต ๊ตฌ์กฐ๋ ๋ฐ๋ณต์ ์ธ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ์ฌ์ฉ๋ฉ๋๋ค.
๊ธฐ๋ณธ ๊ตฌ์กฐ
sql
WITH RECURSIVE cte_name AS (
-- 1. ๊ธฐ๋ณธ ์ผ์ด์ค (Base Case/Anchor)
SELECT ์ด๊ธฐ๊ฐ
FROM ํ
์ด๋ธ
WHERE ์์์กฐ๊ฑด
UNION ALL
-- 2. ์ฌ๊ท ์ผ์ด์ค (Recursive Case)
SELECT ๋ค์๊ฐ
FROM ํ
์ด๋ธ t
INNER JOIN cte_name c ON ์กฐ์ธ์กฐ๊ฑด -- ์๊ธฐ ์ฐธ์กฐ!
)
SELECT * FROM cte_name;
๐ ์ฌ๊ท ์คํ ๊ณผ์
Step-by-Step ์คํ ํ๋ฆ
1๋จ๊ณ: Base Case ์คํ → ์ด๊ธฐ ๊ฒฐ๊ณผ์
์์ฑ
2๋จ๊ณ: ์ด๊ธฐ ๊ฒฐ๊ณผ์ ์๋ณธ ํ
์ด๋ธ ์กฐ์ธ → ์๋ก์ด ํ ์์ฑ
3๋จ๊ณ: ์๋ก์ด ํ๊ณผ ์๋ณธ ํ
์ด๋ธ ์กฐ์ธ → ๋ ๋ค๋ฅธ ํ ์์ฑ
...
N๋จ๊ณ: ๋ ์ด์ ์๋ก์ด ํ์ด ์์ฑ๋์ง ์์ผ๋ฉด ์ข
๋ฃ
์๊ฐ์ ์์
Base: [1]
1ํ์ฐจ: [1] → [1,2]
2ํ์ฐจ: [1,2] → [1,2,3]
3ํ์ฐจ: [1,2,3] → [1,2,3,4]
4ํ์ฐจ: [1,2,3,4] → ์๋ก์ด ํ ์์ → ์ข
๋ฃ
๐ ์ค์ ์์ ๋ก ์ดํดํ๊ธฐ
์์ 1: ์ซ์ ์ํ์ค ์์ฑ
sql
-- 1๋ถํฐ 10๊น์ง ์ซ์ ์์ฑ
WITH RECURSIVE numbers AS (
-- Base Case: ์์์
SELECT 1 AS n
UNION ALL
-- Recursive Case: n+1 ์์ฑ
SELECT n + 1
FROM numbers
WHERE n < 10 -- ์ข
๋ฃ ์กฐ๊ฑด
)
SELECT * FROM numbers;
-- ๊ฒฐ๊ณผ: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
์์ 2: ์กฐ์ง๋/๊ณ์ธต ๊ตฌ์กฐ
sql
-- ์ํ ๋ฐ์ดํฐ
CREATE TABLE employees (
id INT,
name VARCHAR(50),
manager_id INT
);
INSERT INTO employees VALUES
(1, 'CEO', NULL),
(2, '๋ถ์ฌ์ฅA', 1),
(3, '๋ถ์ฌ์ฅB', 1),
(4, 'ํ์ฅA', 2),
(5, 'ํ์ฅB', 2),
(6, '์ฌ์A', 4),
(7, '์ฌ์B', 5);
-- ์ฌ๊ท ์ฟผ๋ฆฌ: ์กฐ์ง๋ ์ ์ฒด ๊ตฌ์กฐ
WITH RECURSIVE org_chart AS (
-- Base: ์ต๊ณ ๊ฒฝ์์ง (CEO)
SELECT id, name, manager_id, 1 AS level, name AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive: ๊ฐ ๋ ๋ฒจ์ ํ์ ์ง์๋ค
SELECT e.id, e.name, e.manager_id,
o.level + 1,
CONCAT(o.path, ' → ', e.name) AS path
FROM employees e
INNER JOIN org_chart o ON e.manager_id = o.id
)
SELECT level, name, path
FROM org_chart
ORDER BY level, id;
๊ฒฐ๊ณผ:
level | name | path
------|---------|------------------
1 | CEO | CEO
2 | ๋ถ์ฌ์ฅA | CEO → ๋ถ์ฌ์ฅA
2 | ๋ถ์ฌ์ฅB | CEO → ๋ถ์ฌ์ฅB
3 | ํ์ฅA | CEO → ๋ถ์ฌ์ฅA → ํ์ฅA
3 | ํ์ฅB | CEO → ๋ถ์ฌ์ฅA → ํ์ฅB
4 | ์ฌ์A | CEO → ๋ถ์ฌ์ฅA → ํ์ฅA → ์ฌ์A
4 | ์ฌ์B | CEO → ๋ถ์ฌ์ฅA → ํ์ฅB → ์ฌ์B
๐งฌ Ecoli ์์ ๋ถ์
๋ฐ์ดํฐ ๊ตฌ์กฐ ์ดํด
ecoli_data ํ
์ด๋ธ:
id | parent_id | ...
1 | NULL (1์ธ๋ - ๋ฃจํธ)
2 | 1 (2์ธ๋ - 1์ ์์)
3 | 1 (2์ธ๋ - 1์ ์์)
4 | 2 (3์ธ๋ - 2์ ์์)
5 | 3 (3์ธ๋ - 3์ ์์)
6 | 4 (4์ธ๋ - 4์ ์์)
์ฌ๊ท ์คํ ๊ณผ์
sql
WITH RECURSIVE g AS (
-- Step 1: Base Case - 1์ธ๋ ์ฐพ๊ธฐ
SELECT id, 1 g_level -- ๊ฒฐ๊ณผ: {1}
FROM ecoli_data
WHERE parent_id IS NULL
UNION ALL
-- Step 2~N: Recursive Case - ์์๋ค ์ฐพ๊ธฐ
SELECT a.id, b.g_level+1
FROM ecoli_data a -- ์ ์ฒด ๋ฐ์ดํฐ
INNER JOIN g b -- ์ด์ ๋จ๊ณ ๊ฒฐ๊ณผ
ON a.parent_id = b.id -- ๋ถ๋ชจ-์์ ๊ด๊ณ
)
์คํ ๋จ๊ณ:
1๋จ๊ณ: {(1,1)} - 1์ธ๋
2๋จ๊ณ: {(1,1), (2,2), (3,2)} - 2์ธ๋ ์ถ๊ฐ
3๋จ๊ณ: {(1,1), (2,2), (3,2), (4,3), (5,3)} - 3์ธ๋ ์ถ๊ฐ
4๋จ๊ณ: {(1,1), (2,2), (3,2), (4,3), (5,3), (6,4)} - 4์ธ๋ ์ถ๊ฐ
5๋จ๊ณ: ๋ ์ด์ ์์ ์์ → ์ข
๋ฃ
โ ๏ธ ์ฃผ์์ฌํญ & ํ
1. ๋ฌดํ ๋ฃจํ ๋ฐฉ์ง
sql
-- โ ์ํ: ๋ฌดํ ๋ฃจํ ๊ฐ๋ฅ์ฑ
WITH RECURSIVE danger AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM danger -- ์ข
๋ฃ ์กฐ๊ฑด ์์!
)
-- โ
์์ : ์ข
๋ฃ ์กฐ๊ฑด ๋ช
์
WITH RECURSIVE safe AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM safe
WHERE n < 1000 -- ์ต๋ 1000๊น์ง๋ง
)
2. ์ฑ๋ฅ ์ต์ ํ
sql
-- โ
์ธ๋ฑ์ค ํ์ฉ์ ์ํ ์กฐ๊ฑด ์์
WITH RECURSIVE hierarchy AS (
SELECT id, parent_id, 1 AS level
FROM tree_table
WHERE parent_id IS NULL
UNION ALL
SELECT t.id, t.parent_id, h.level + 1
FROM tree_table t
INNER JOIN hierarchy h ON t.parent_id = h.id
WHERE h.level < 10 -- ๊น์ด ์ ํ์ผ๋ก ์ฑ๋ฅ ๋ณด์ฅ
)
3. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
sql
-- ๋์ฉ๋ ๋ฐ์ดํฐ์ ๊ฒฝ์ฐ ์ค๊ฐ ๊ฒฐ๊ณผ ์ ํ
WITH RECURSIVE large_tree AS (
-- Base case
UNION ALL
-- Recursive case
WHERE ์กฐ๊ฑด AND level <= 5 -- ๋ ๋ฒจ ์ ํ
)
๐ฏ ์ฝ๋ฉํ ์คํธ์์์ ํ์ฉ
์์ฃผ ์ถ์ ๋๋ ํจํด
1. ๊ณ์ธต ๊ตฌ์กฐ ๋ถ์
sql
-- "๊ฐ ์ธ๋๋ณ ๊ฐ์ฒด ์ ๊ตฌํ๊ธฐ"
-- "๋ฆฌํ ๋
ธ๋(์์ ์๋ ๋
ธ๋) ์ฐพ๊ธฐ"
-- "ํน์ ์ธ๋์ ์กฐ์ ์ฐพ๊ธฐ"
2. ์ฐ์๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
sql
-- "์ฐ์๋ N์ผ๊ฐ์ ๋งค์ถ"
-- "๋์ ํฉ๊ณ ๊ณ์ฐ"
-- "์ด๋ ํ๊ท ๊ณ์ฐ"
3. ๊ทธ๋ํ ํ์
sql
-- "์น๊ตฌ์ ์น๊ตฌ ์ฐพ๊ธฐ"
-- "์ถ์ฒ ์์คํ
์ ์ฐ๊ฒฐ ๊ณ ๋ฆฌ"
-- "๋คํธ์ํฌ ๋ถ์"
๋ฉด์ ์ดํ ํฌ์ธํธ
- ๋ณต์กํ ๊ณ์ธต ๊ตฌ์กฐ๋ฅผ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌ
- ๋ฐ๋ณต ๋ก์ง์ SQL๋ก ํจ์จ์ ๊ตฌํ
- ๋์ฉ๋ ๋ฐ์ดํฐ์์๋ ์์ ์ ์คํ
๐ก ์ค๋ฌด ํ์ฉ ์ฌ๋ก
์นดํ ๊ณ ๋ฆฌ ๊ณ์ธต ๊ตฌ์กฐ
sql
-- ์ ์์ ํ → ์ค๋งํธํฐ → iPhone → iPhone 15
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 AS depth
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT REPEAT(' ', depth-1) || name AS category_hierarchy
FROM category_tree
ORDER BY depth, name;
์ถ์ฒ ์์คํ
sql
-- "์ด ์ํ์ ๋ณธ ๊ณ ๊ฐ๋ค์ด ํจ๊ป ๋ณธ ์ํ" 3๋จ๊ณ๊น์ง
WITH RECURSIVE recommendations AS (
SELECT product_id, 1 AS step
FROM user_views
WHERE user_id = :target_user_id
UNION ALL
SELECT uv.product_id, r.step + 1
FROM user_views uv
INNER JOIN recommendations r ON uv.user_id IN (
SELECT user_id FROM user_views WHERE product_id = r.product_id
)
WHERE r.step < 3
)
๐ ๊ณ ๊ธ ํจํด
๊ฒฝ๋ก ์ถ์
sql
WITH RECURSIVE path_finder AS (
SELECT id, name, CAST(name AS CHAR(1000)) AS path
FROM nodes WHERE id = 1
UNION ALL
SELECT n.id, n.name,
CONCAT(pf.path, ' → ', n.name)
FROM nodes n
INNER JOIN path_finder pf ON n.parent_id = pf.id
)
๊ทธ๋ํ ์ฌ์ดํด ๊ฒ์ฌ
sql
WITH RECURSIVE cycle_check AS (
SELECT id, parent_id, CONCAT(',', id, ',') AS visited
FROM tree_table WHERE parent_id IS NULL
UNION ALL
SELECT t.id, t.parent_id,
CONCAT(cc.visited, t.id, ',')
FROM tree_table t
INNER JOIN cycle_check cc ON t.parent_id = cc.id
WHERE INSTR(cc.visited, CONCAT(',', t.id, ',')) = 0 -- ์ฌ์ดํด ๋ฐฉ์ง
)
ํต์ฌ ์์ฝ: ์ฌ๊ท CTE๋ ๊ณ์ธต ๊ตฌ์กฐ๋ ๋ฐ๋ณต ๋ก์ง์ SQL๋ก ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ ๋๋ค. ๋ณต์กํ ๋ฐ์ดํฐ ๋ถ์์ด ํ์ํ ํ๊ฒฝ์์๋ ํ์ ์คํฌ์ด์์! ๐ฏ
๋ฐ์ํ