์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

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
-- "์นœ๊ตฌ์˜ ์นœ๊ตฌ ์ฐพ๊ธฐ"
-- "์ถ”์ฒœ ์‹œ์Šคํ…œ์˜ ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ"
-- "๋„คํŠธ์›Œํฌ ๋ถ„์„"

๋ฉด์ ‘ ์–ดํ•„ ํฌ์ธํŠธ

  1. ๋ณต์žกํ•œ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
  2. ๋ฐ˜๋ณต ๋กœ์ง์„ SQL๋กœ ํšจ์œจ์  ๊ตฌํ˜„
  3. ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ๋„ ์•ˆ์ •์  ์‹คํ–‰

๐Ÿ’ก ์‹ค๋ฌด ํ™œ์šฉ ์‚ฌ๋ก€

์นดํ…Œ๊ณ ๋ฆฌ ๊ณ„์ธต ๊ตฌ์กฐ

 
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๋กœ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๋ถ„์„์ด ํ•„์š”ํ•œ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•„์ˆ˜ ์Šคํ‚ฌ์ด์—์š”! ๐ŸŽฏ

๋ฐ˜์‘ํ˜•