5. SQL - patterns

Jasonยท2026๋…„ 1์›” 12์ผ

SQL

๋ชฉ๋ก ๋ณด๊ธฐ
5/47
post-thumbnail




SQL ์‹ค์ „ ํŒจํ„ด โ€” ์ฝ”๋”ฉํ…Œ์ŠคํŠธ ์ž์ฃผ ๋‚˜์˜ค๋Š” ์œ ํ˜• ์ด์ •๋ฆฌ

๐Ÿ’ก ์‹ค์ œ ์ฝ”๋”ฉํ…Œ์ŠคํŠธ์—์„œ ๋ฐ˜๋ณต ์ถœ์ œ๋˜๋Š” ํŒจํ„ด์„ ์ •๋ฆฌํ•œ๋‹ค.


๐ŸŽฏ ์ด ๊ธ€์˜ ๋ชฉ์ 

์ฝ”๋”ฉํ…Œ์ŠคํŠธ์—์„œ ์ž์ฃผ ๋‚˜์˜ค๋Š” ํŒจํ„ด์„ ์ตํžˆ๋ฉด,
์ƒˆ๋กœ์šด ๋ฌธ์ œ๋„ "์•„, ์ด๊ฑด ์ € ํŒจํ„ด์ด๋„ค!" ํ•˜๊ณ  ํ’€ ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ“Š ๊ณตํ†ต ์˜ˆ์ œ ํ…Œ์ด๋ธ”

[users]
| user_id | name | created_at |
|---------|------|------------|
| 1 | ๊น€์ฒ ์ˆ˜ | 2024-01-01 |
| 2 | ์ด์˜ํฌ | 2024-01-15 |
| 3 | ๋ฐ•๋ฏผ์ˆ˜ | 2024-02-01 |
| 4 | ์ตœ์ง€์€ | 2024-02-15 |

[orders]
| order_id | user_id | amount | order_date |
|----------|---------|--------|------------|
| 101 | 1 | 150000 | 2024-01-10 |
| 102 | 1 | 50000 | 2024-01-20 |
| 103 | 2 | 200000 | 2024-02-05 |
| 104 | 2 | 80000 | 2024-02-10 |
| 105 | 1 | 100000 | 2024-02-15 |
| 106 | 3 | 300000 | 2024-02-20 |


1๏ธโƒฃ ๊ทธ๋ฃน๋ณ„ Top N

๋ฌธ์ œ ์œ ํ˜•

"๊ฐ ์œ ์ €๋ณ„ ๊ฐ€์žฅ ํฐ ์ฃผ๋ฌธ", "๋ถ€์„œ๋ณ„ ์—ฐ๋ด‰ 1์œ„", "์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค ์ƒ์œ„ 3๊ฐœ"

ํŒจํ„ด

WITH ranked AS (
    SELECT 
        *,
        ROW_NUMBER() OVER(
            PARTITION BY ๊ทธ๋ฃน์ปฌ๋Ÿผ 
            ORDER BY ์ •๋ ฌ์ปฌ๋Ÿผ DESC
        ) AS rn
    FROM ํ…Œ์ด๋ธ”
)
SELECT * FROM ranked WHERE rn <= N;

์˜ˆ์ œ: ์œ ์ €๋ณ„ ์ตœ๋Œ€ ์ฃผ๋ฌธ 1๊ฑด

WITH ranked AS (
    SELECT 
        *,
        ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY amount DESC) AS rn
    FROM orders
)
SELECT order_id, user_id, amount
FROM ranked 
WHERE rn = 1;
order_iduser_idamount
1011150000
1032200000
1063300000

2๏ธโƒฃ ์กฐ๊ฑด์— ๋งž๋Š” ํ–‰์ด ์—†๋Š” ๊ทธ๋ฃน ์ฐพ๊ธฐ

๋ฌธ์ œ ์œ ํ˜•

"์ฃผ๋ฌธ ์—†๋Š” ์œ ์ €", "ํŒ๋งค ์•ˆ ๋œ ์ƒํ’ˆ", "์˜ˆ์•ฝ ์—†๋Š” ๋‚ ์งœ"

ํŒจํ„ด A: LEFT JOIN + IS NULL

SELECT a.*
FROM ๊ธฐ์ค€ํ…Œ์ด๋ธ” a
LEFT JOIN ์กฐ๊ฑดํ…Œ์ด๋ธ” b ON a.key = b.key
WHERE b.key IS NULL;

ํŒจํ„ด B: NOT EXISTS

SELECT *
FROM ๊ธฐ์ค€ํ…Œ์ด๋ธ” a
WHERE NOT EXISTS (
    SELECT 1 FROM ์กฐ๊ฑดํ…Œ์ด๋ธ” b
    WHERE b.key = a.key
);

์˜ˆ์ œ: ์ฃผ๋ฌธ ์—†๋Š” ์œ ์ €

-- ๋ฐฉ๋ฒ• 1: LEFT JOIN
SELECT u.*
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;

-- ๋ฐฉ๋ฒ• 2: NOT EXISTS
SELECT *
FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o
    WHERE o.user_id = u.user_id
);

3๏ธโƒฃ ์—ฐ์† N์ผ ์กฐ๊ฑด

๋ฌธ์ œ ์œ ํ˜•

"3์ผ ์—ฐ์† ๋กœ๊ทธ์ธํ•œ ์œ ์ €", "์—ฐ์† ๋งค์ถœ ์ƒ์Šน ๊ธฐ๊ฐ„"

ํŒจํ„ด

WITH numbered AS (
    SELECT 
        *,
        ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY date) AS rn,
        DATE_SUB(date, INTERVAL ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY date) DAY) AS grp
    FROM ํ…Œ์ด๋ธ”
)
SELECT user_id, MIN(date), MAX(date), COUNT(*) AS consecutive_days
FROM numbered
GROUP BY user_id, grp
HAVING COUNT(*) >= N;

์˜ˆ์ œ: ์—ฐ์† ๋กœ๊ทธ์ธ ์ฐพ๊ธฐ

[logins]
| user_id | login_date |
|---------|------------|
| 1 | 2024-01-01 |
| 1 | 2024-01-02 |
| 1 | 2024-01-03 |
| 1 | 2024-01-05 |
| 2 | 2024-01-01 |

WITH numbered AS (
    SELECT 
        user_id,
        login_date,
        DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_date) DAY) AS grp
    FROM logins
)
SELECT 
    user_id,
    MIN(login_date) AS start_date,
    MAX(login_date) AS end_date,
    COUNT(*) AS consecutive_days
FROM numbered
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;

ํ•ต์‹ฌ ์•„์ด๋””์–ด:

  • ์—ฐ์†๋œ ๋‚ ์งœ์—์„œ ROW_NUMBER๋ฅผ ๋นผ๋ฉด ๊ฐ™์€ ๊ฐ’์ด ๋‚˜์˜ด!
  • ์ด ๊ฐ’์œผ๋กœ GROUP BY ํ•˜๋ฉด ์—ฐ์† ๊ตฌ๊ฐ„์„ ์ฐพ์„ ์ˆ˜ ์žˆ์Œ

4๏ธโƒฃ ์ด์ „/๋‹ค์Œ ๊ฐ’๊ณผ ๋น„๊ต

๋ฌธ์ œ ์œ ํ˜•

"์ „์ผ ๋Œ€๋น„ ์ฆ๊ฐ€ํ•œ ๋‚ ", "์ด์ „ ์ฃผ๋ฌธ๋ณด๋‹ค ๊ธˆ์•ก์ด ํฐ ์ฃผ๋ฌธ"

ํŒจํ„ด

WITH with_prev AS (
    SELECT 
        *,
        LAG(๋น„๊ต์ปฌ๋Ÿผ) OVER(ORDER BY ์ •๋ ฌ์ปฌ๋Ÿผ) AS prev_value
    FROM ํ…Œ์ด๋ธ”
)
SELECT *
FROM with_prev
WHERE ๋น„๊ต์ปฌ๋Ÿผ > prev_value;

์˜ˆ์ œ: ์ „์ผ ๋Œ€๋น„ ๋งค์ถœ ์ฆ๊ฐ€ํ•œ ๋‚ 

WITH daily_sales AS (
    SELECT 
        order_date,
        SUM(amount) AS daily_amount
    FROM orders
    GROUP BY order_date
),
with_prev AS (
    SELECT 
        *,
        LAG(daily_amount) OVER(ORDER BY order_date) AS prev_amount
    FROM daily_sales
)
SELECT *
FROM with_prev
WHERE daily_amount > prev_amount;

5๏ธโƒฃ ๋ˆ„์ ํ•ฉ / ๋ˆ„์  ๋น„์œจ

๋ฌธ์ œ ์œ ํ˜•

"๋ˆ„์  ๋งค์ถœ", "์ƒ์œ„ N%์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ"

ํŒจํ„ด: ๋ˆ„์ ํ•ฉ

SELECT 
    *,
    SUM(amount) OVER(ORDER BY date) AS cumulative_sum
FROM ํ…Œ์ด๋ธ”;

ํŒจํ„ด: ๋ˆ„์  ๋น„์œจ

SELECT 
    *,
    SUM(amount) OVER(ORDER BY amount DESC) * 100.0 / SUM(amount) OVER() AS cumulative_pct
FROM ํ…Œ์ด๋ธ”;

์˜ˆ์ œ: ๋งค์ถœ ์ƒ์œ„ 80%๋ฅผ ์ฐจ์ง€ํ•˜๋Š” ์ƒํ’ˆ

WITH product_sales AS (
    SELECT 
        product,
        SUM(amount) AS total_amount
    FROM orders
    GROUP BY product
),
with_cumulative AS (
    SELECT 
        *,
        SUM(total_amount) OVER(ORDER BY total_amount DESC) * 100.0 
            / SUM(total_amount) OVER() AS cumulative_pct
    FROM product_sales
)
SELECT *
FROM with_cumulative
WHERE cumulative_pct <= 80;

6๏ธโƒฃ ๊ธฐ๊ฐ„๋ณ„ ์ง‘๊ณ„

๋ฌธ์ œ ์œ ํ˜•

"์›”๋ณ„ ๋งค์ถœ", "์ฃผ์ฐจ๋ณ„ ๊ฐ€์ž…์ž", "์‹œ๊ฐ„๋Œ€๋ณ„ ์ฃผ๋ฌธ"

ํŒจํ„ด

SELECT 
    DATE_FORMAT(date_column, 'ํฌ๋งท') AS period,
    ์ง‘๊ณ„ํ•จ์ˆ˜()
FROM ํ…Œ์ด๋ธ”
GROUP BY DATE_FORMAT(date_column, 'ํฌ๋งท');

๋‚ ์งœ ํฌ๋งท

ํฌ๋งท์˜๋ฏธ์˜ˆ์‹œ
%Y์—ฐ๋„ 4์ž๋ฆฌ2024
%m์›” 2์ž๋ฆฌ01, 12
%d์ผ 2์ž๋ฆฌ01, 31
%Y-%m์—ฐ-์›”2024-01
%Y-%U์—ฐ-์ฃผ์ฐจ2024-01

์˜ˆ์ œ: ์›”๋ณ„ ๋งค์ถœ

SELECT 
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;

7๏ธโƒฃ ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

๋ฌธ์ œ ์œ ํ˜•

"์ค‘๋ณต ์ œ๊ฑฐ", "์ค‘๋ณต ๋ฐ์ดํ„ฐ ์ค‘ ์ตœ์‹  ๊ฒƒ๋งŒ"

ํŒจํ„ด A: ์ค‘๋ณต ์ฐพ๊ธฐ

SELECT column, COUNT(*)
FROM ํ…Œ์ด๋ธ”
GROUP BY column
HAVING COUNT(*) > 1;

ํŒจํ„ด B: ์ค‘๋ณต ์ค‘ ํ•˜๋‚˜๋งŒ ๋‚จ๊ธฐ๊ธฐ

WITH ranked AS (
    SELECT 
        *,
        ROW_NUMBER() OVER(PARTITION BY ์ค‘๋ณต๊ธฐ์ค€ ORDER BY ์šฐ์„ ์ˆœ์œ„) AS rn
    FROM ํ…Œ์ด๋ธ”
)
SELECT * FROM ranked WHERE rn = 1;

์˜ˆ์ œ: ์œ ์ €๋ณ„ ๊ฐ€์žฅ ์ตœ๊ทผ ์ฃผ๋ฌธ๋งŒ

WITH ranked AS (
    SELECT 
        *,
        ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY order_date DESC) AS rn
    FROM orders
)
SELECT * FROM ranked WHERE rn = 1;

8๏ธโƒฃ ์กฐ๊ฑด๋ณ„ ์ง‘๊ณ„ (CASE WHEN)

๋ฌธ์ œ ์œ ํ˜•

"์„ฑ๋ณ„๋ณ„ ์ง‘๊ณ„", "์ƒํƒœ๋ณ„ ์นด์šดํŠธ", "์กฐ๊ฑด๋ณ„ ํ•ฉ๊ณ„"

ํŒจํ„ด

SELECT 
    SUM(CASE WHEN ์กฐ๊ฑด1 THEN 1 ELSE 0 END) AS count_1,
    SUM(CASE WHEN ์กฐ๊ฑด2 THEN amount ELSE 0 END) AS sum_2
FROM ํ…Œ์ด๋ธ”;

์˜ˆ์ œ: ๊ธˆ์•ก ๊ตฌ๊ฐ„๋ณ„ ์ฃผ๋ฌธ ์ˆ˜

SELECT 
    SUM(CASE WHEN amount < 100000 THEN 1 ELSE 0 END) AS '10๋งŒ์› ๋ฏธ๋งŒ',
    SUM(CASE WHEN amount >= 100000 AND amount < 200000 THEN 1 ELSE 0 END) AS '10~20๋งŒ์›',
    SUM(CASE WHEN amount >= 200000 THEN 1 ELSE 0 END) AS '20๋งŒ์› ์ด์ƒ'
FROM orders;

9๏ธโƒฃ ์ž๊ธฐ ์ฐธ์กฐ (Self Join)

๋ฌธ์ œ ์œ ํ˜•

"๊ฐ™์€ ๋‚  ์ฃผ๋ฌธํ•œ ๋‹ค๋ฅธ ์œ ์ €", "๋งค๋‹ˆ์ €-์ง์› ๊ด€๊ณ„"

ํŒจํ„ด

SELECT a.*, b.*
FROM ํ…Œ์ด๋ธ” a
JOIN ํ…Œ์ด๋ธ” b ON a.๊ณตํ†ต์ปฌ๋Ÿผ = b.๊ณตํ†ต์ปฌ๋Ÿผ
WHERE a.id != b.id;  -- ์ž๊ธฐ ์ž์‹  ์ œ์™ธ

์˜ˆ์ œ: ๊ฐ™์€ ๋‚  ์ฃผ๋ฌธํ•œ ์œ ์ € ์Œ

SELECT 
    a.user_id AS user1,
    b.user_id AS user2,
    a.order_date
FROM orders a
JOIN orders b ON a.order_date = b.order_date
WHERE a.user_id < b.user_id;  -- ์ค‘๋ณต ์ œ๊ฑฐ + ์ž๊ธฐ ์ž์‹  ์ œ์™ธ

๐Ÿ”Ÿ ํ”ผ๋ฒ— (Pivot)

๋ฌธ์ œ ์œ ํ˜•

"์›”๋ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์—ด๋กœ ๋ณ€ํ™˜", "์„ธ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๋กœ๋กœ"

ํŒจํ„ด

SELECT 
    ๊ทธ๋ฃน์ปฌ๋Ÿผ,
    SUM(CASE WHEN ํ”ผ๋ฒ—์กฐ๊ฑด = '๊ฐ’1' THEN amount END) AS ๊ฐ’1,
    SUM(CASE WHEN ํ”ผ๋ฒ—์กฐ๊ฑด = '๊ฐ’2' THEN amount END) AS ๊ฐ’2
FROM ํ…Œ์ด๋ธ”
GROUP BY ๊ทธ๋ฃน์ปฌ๋Ÿผ;

์˜ˆ์ œ: ์œ ์ €๋ณ„ ์›”๋ณ„ ์ฃผ๋ฌธ๊ธˆ์•ก

SELECT 
    user_id,
    SUM(CASE WHEN DATE_FORMAT(order_date, '%Y-%m') = '2024-01' THEN amount ELSE 0 END) AS '2024-01',
    SUM(CASE WHEN DATE_FORMAT(order_date, '%Y-%m') = '2024-02' THEN amount ELSE 0 END) AS '2024-02'
FROM orders
GROUP BY user_id;
user_id2024-012024-02
1200000100000
20280000
30300000

๐Ÿ† ์ฝ”ํ…Œ ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํŒจํ„ดํ•ต์‹ฌ ํ‚ค์›Œ๋“œ์‚ฌ์šฉ ๊ธฐ์ˆ 
๊ทธ๋ฃน๋ณ„ Top N"~๋ณ„ 1์œ„", "~๋ณ„ ์ƒ์œ„ N๊ฐœ"ROW_NUMBER + PARTITION BY
์—†๋Š” ๊ฒƒ ์ฐพ๊ธฐ"~์—†๋Š”", "~์•ˆ ํ•œ"LEFT JOIN + IS NULL
์—ฐ์† N์ผ"์—ฐ์†", "N์ผ ์—ฐ์†"ROW_NUMBER + DATE ์—ฐ์‚ฐ
์ด์ „ ๋น„๊ต"์ „์ผ ๋Œ€๋น„", "์ด์ „๋ณด๋‹ค"LAG
๋ˆ„์ "๋ˆ„์ ", "Running"SUM OVER(ORDER BY)
๊ธฐ๊ฐ„๋ณ„"์›”๋ณ„", "์ฃผ๋ณ„", "์ผ๋ณ„"DATE_FORMAT + GROUP BY
์ค‘๋ณต ์ฒ˜๋ฆฌ"์ค‘๋ณต", "์ตœ์‹  ๊ฒƒ๋งŒ"ROW_NUMBER
์กฐ๊ฑด๋ณ„ ์ง‘๊ณ„"~๋ณ„๋กœ ์นด์šดํŠธ"CASE WHEN
ํ”ผ๋ฒ—"์—ด๋กœ ๋ณ€ํ™˜", "๊ฐ€๋กœ๋กœ"CASE WHEN + GROUP BY

๐Ÿ’ก ๋ฌธ์ œ ํ’€์ด ํŒ

1. ๋ฌธ์ œ ์ฝ๊ณ  ํŒจํ„ด ํŒŒ์•…

"๊ฐ ๋ถ€์„œ๋ณ„ ์ตœ๊ณ  ์—ฐ๋ด‰์ž" 
โ†’ ๊ทธ๋ฃน๋ณ„ Top 1 ํŒจํ„ด!
โ†’ ROW_NUMBER + PARTITION BY

2. ๋‹จ๊ณ„๋ณ„๋กœ ์ชผ๊ฐœ๊ธฐ

๋ณต์žกํ•œ ๋ฌธ์ œ = ์—ฌ๋Ÿฌ ๋‹จ์ˆœํ•œ ๋‹จ๊ณ„

1๋‹จ๊ณ„: ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ JOIN
2๋‹จ๊ณ„: ์กฐ๊ฑด ํ•„ํ„ฐ๋ง (WHERE)
3๋‹จ๊ณ„: ๊ทธ๋ฃนํ™” (GROUP BY)
4๋‹จ๊ณ„: ์œˆ๋„์šฐ ํ•จ์ˆ˜ ์ ์šฉ
5๋‹จ๊ณ„: ์ตœ์ข… ํ•„ํ„ฐ๋ง

3. CTE๋กœ ๊ฐ€๋…์„ฑ ๋†’์ด๊ธฐ

-- ํ•œ ๋ฒˆ์— ์“ฐ๋ฉด ๋ณต์žก
SELECT * FROM (SELECT * FROM (SELECT ...)) ...

-- CTE๋กœ ๋‹จ๊ณ„๋ณ„๋กœ
WITH step1 AS (...),
     step2 AS (...),
     step3 AS (...)
SELECT * FROM step3;

4. ์˜ˆ์™ธ ์ผ€์ด์Šค ์ฒดํฌ

  • NULL ์ฒ˜๋ฆฌํ–ˆ๋‚˜?
  • ๋™์  ์ฒ˜๋ฆฌ๋Š”? (RANK vs ROW_NUMBER)
  • ๊ฒฝ๊ณ„๊ฐ’์€? (>=, >, <, <=)
  • ๋นˆ ๊ฒฐ๊ณผ์ผ ๋•Œ๋Š”?

๐Ÿ“ ์ตœ์ข… ์ •๋ฆฌ

[์ž์ฃผ ๋‚˜์˜ค๋Š” ์ˆœ์„œ]
1. ๊ทธ๋ฃน๋ณ„ Top N (ROW_NUMBER)
2. ์—†๋Š” ๊ฒƒ ์ฐพ๊ธฐ (LEFT JOIN + NULL)
3. ๊ธฐ๊ฐ„๋ณ„ ์ง‘๊ณ„ (DATE_FORMAT + GROUP BY)
4. ์ด์ „ ๊ฐ’ ๋น„๊ต (LAG)
5. ์กฐ๊ฑด๋ณ„ ์ง‘๊ณ„ (CASE WHEN)

[ํ•ต์‹ฌ ๊ธฐ์ˆ ]
- CTE (WITH์ ˆ): ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ
- ROW_NUMBER: ์ˆœ์œ„ ๋งค๊ธฐ๊ธฐ์˜ ๊ธฐ๋ณธ
- LAG/LEAD: ์ด์ „/๋‹ค์Œ ํ–‰ ์ฐธ์กฐ
- CASE WHEN: ์กฐ๊ฑด๋ณ„ ์ฒ˜๋ฆฌ

๐Ÿ“ ํŒจํ„ด์„ ๋งŽ์ด ์•Œ์ˆ˜๋ก ์ƒˆ๋กœ์šด ๋ฌธ์ œ๋„ ์‰ฝ๊ฒŒ ํ’€ ์ˆ˜ ์žˆ๋‹ค.
์œ„ 10๊ฐ€์ง€ ํŒจํ„ด๋งŒ ํ™•์‹คํžˆ ์ตํ˜€๋„ ๋Œ€๋ถ€๋ถ„์˜ SQL ์ฝ”๋”ฉํ…Œ์ŠคํŠธ์— ๋Œ€์‘ ๊ฐ€๋Šฅ!

profile
Data Analyst | Thoughts Become Things. ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•  ์ˆ˜ ์—†๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ํ•  ์ˆ˜ ์—†๋‹ค. | www.linkedin.com/in/๋ช…์ˆ˜-์ œ-7ab843200

0๊ฐœ์˜ ๋Œ“๊ธ€