๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€๋ฅผ ์œ„ํ•œ SQL: ์œˆ๋„์šฐ ํ•จ์ˆ˜, CTE, ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ ๊ธฐ๋ฒ•

SQL ์œˆ๋„์šฐ ํ•จ์ˆ˜, CTE(๊ณตํ†ต ํ…Œ์ด๋ธ” ์‹), ๊ณ ๊ธ‰ ๋ถ„์„ ์ฟผ๋ฆฌ๋ฅผ ์‹ค์šฉ์ ์ธ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘ ์ค€๋น„์™€ ์‹ค๋ฌด์— ํ•„์ˆ˜์ ์ธ ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€๋ฅผ ์œ„ํ•œ SQL: ์œˆ๋„์šฐ ํ•จ์ˆ˜, CTE, ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ

SQL ์œˆ๋„์šฐ ํ•จ์ˆ˜, CTE(๊ณตํ†ต ํ…Œ์ด๋ธ” ์‹), ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ ํŒจํ„ด์€ ๋ถ„์„ SQL์˜ ํ•ต์‹ฌ ๊ธฐ๋ฐ˜์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘์„ ์ค€๋น„ํ•˜๋“ , ๋ณต์žกํ•œ ๋ฆฌํฌํŒ… ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜๋“ , ์ด๋Ÿฌํ•œ ๊ธฐ๋ฒ•์„ ์ˆ™๋‹ฌํ•˜๋ฉด ์žฅํ™ฉํ•˜๊ณ  ์ฝ๊ธฐ ์–ด๋ ค์šด ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ณ  ์„ฑ๋Šฅ์ด ์šฐ์ˆ˜ํ•œ SQL๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ๊ฐœ๋…

์œˆ๋„์šฐ ํ•จ์ˆ˜๋Š” ํ˜„์žฌ ํ–‰๊ณผ ๊ด€๋ จ๋œ ํ–‰ ์ง‘ํ•ฉ์— ๋Œ€ํ•ด ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. GROUP BY์ฒ˜๋Ÿผ ๊ฒฐ๊ณผ๋ฅผ ๋‹จ์ผ ํ–‰์œผ๋กœ ์ถ•์†Œํ•˜์ง€ ์•Š๊ณ , ๊ฐ ํ–‰์„ ์œ ์ง€ํ•œ ์ฑ„ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ํŠน์ง•์ž…๋‹ˆ๋‹ค. CTE์™€ ๊ฒฐํ•ฉํ•˜๋ฉด ๋ณต์žกํ•œ ๋ถ„์„ ์ฟผ๋ฆฌ๋„ ์ฝ๊ธฐ ์‰ฝ๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

SQL ์œˆ๋„์šฐ ํ•จ์ˆ˜์™€ OVER ์ ˆ์˜ ๊ธฐ๋ณธ ์ดํ•ด

์œˆ๋„์šฐ ํ•จ์ˆ˜๋Š” ์ •์˜๋œ "์œˆ๋„์šฐ"(ํ–‰์˜ ๋ฒ”์œ„)์— ๋Œ€ํ•ด ๊ณ„์‚ฐ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. OVER ์ ˆ์€ ํ•ด๋‹น ์œˆ๋„์šฐ์— ํฌํ•จ๋˜๋Š” ํ–‰๊ณผ ์ •๋ ฌ ์ˆœ์„œ๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค. GROUP BY๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ง‘๊ณ„ ํ•จ์ˆ˜์™€ ๋‹ฌ๋ฆฌ, ์œˆ๋„์šฐ ํ•จ์ˆ˜๋Š” ๊ฒฐ๊ณผ ์ง‘ํ•ฉ์˜ ๋ชจ๋“  ๊ฐœ๋ณ„ ํ–‰์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์˜ ๊ตฌ๋ฌธ์€ PostgreSQL, MySQL 8 ์ด์ƒ, BigQuery, SQL Server, Snowflake ๋“ฑ ์ฃผ์š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

sql
-- window_function_syntax.sql
SELECT
  employee_id,
  department,
  salary,
  -- Ranks employees within each department by salary
  RANK() OVER (
    PARTITION BY department
    ORDER BY salary DESC
  ) AS dept_salary_rank,
  -- Running total of salaries within department
  SUM(salary) OVER (
    PARTITION BY department
    ORDER BY salary DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_salary_total
FROM employees;

PARTITION BY๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ฃน์œผ๋กœ ๋ถ„ํ• ํ•ฉ๋‹ˆ๋‹ค(GROUP BY์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ ํ–‰์„ ์ถ•์†Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค). ORDER BY๋Š” ๊ฐ ํŒŒํ‹ฐ์…˜ ๋‚ด์˜ ์ˆœ์„œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์„ ํƒ์  ํ”„๋ ˆ์ž„ ์ ˆ(ROWS BETWEEN ...)์€ ์œˆ๋„์šฐ๋ฅผ ํŠน์ • ํ–‰ ๋ฒ”์œ„๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

ROW_NUMBER, RANK, DENSE_RANK ๋น„๊ต

์ด ์„ธ ๊ฐ€์ง€ ์ˆœ์œ„ ํ•จ์ˆ˜๋Š” ์™ธ๊ด€์ƒ ์œ ์‚ฌํ•˜์ง€๋งŒ, ๋™์ผ ์ˆœ์œ„(ํƒ€์ด)๊ฐ€ ์กด์žฌํ•  ๋•Œ ๋™์ž‘ ๋ฐฉ์‹์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ํ•จ์ˆ˜๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์€ ๋ถ„์„ ์ฟผ๋ฆฌ์—์„œ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฒ„๊ทธ์˜ ์›์ธ์ž…๋‹ˆ๋‹ค.

sql
-- ranking_comparison.sql
SELECT
  product_name,
  category,
  revenue,
  ROW_NUMBER() OVER (ORDER BY revenue DESC) AS row_num,
  RANK()       OVER (ORDER BY revenue DESC) AS rank_val,
  DENSE_RANK() OVER (ORDER BY revenue DESC) AS dense_rank_val
FROM products;
product_namerevenuerow_numrank_valdense_rank_val
Widget A50000111
Widget B50000211
Widget C40000332
Widget D30000443

ROW_NUMBER๋Š” ํ•ญ์ƒ ๊ณ ์œ ํ•œ ์ˆœ์ฐจ ์ •์ˆ˜๋ฅผ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค. ๋™์ผํ•œ ๊ฐ’์˜ ํ–‰์ด ์žˆ์–ด๋„ ํ•˜๋‚˜์˜ ํ–‰์— ์ž„์˜๋กœ ๋” ์ž‘์€ ๋ฒˆํ˜ธ๊ฐ€ ๋ถ€์—ฌ๋ฉ๋‹ˆ๋‹ค. RANK๋Š” ๋™์ผ ์ˆœ์œ„ ํ–‰์— ๊ฐ™์€ ๋ฒˆํ˜ธ๋ฅผ ํ• ๋‹นํ•˜์ง€๋งŒ ํ›„์† ๋ฒˆํ˜ธ๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค(1, 1, 3). DENSE_RANK๋„ ๋™์ผ ์ˆœ์œ„๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ ๋ฒˆํ˜ธ๋ฅผ ๊ฑด๋„ˆ๋›ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค(1, 1, 2). ์ƒ์œ„ N๊ฐœ ์ฟผ๋ฆฌ์—์„œ ์ค‘๋ณต ๊ฐ’์ด ๊ฐ™์€ ์ˆœ์œ„๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, DENSE_RANK๊ฐ€ ์ ์ ˆํ•œ ์„ ํƒ์ž…๋‹ˆ๋‹ค.

LAG์™€ LEAD๋ฅผ ํ™œ์šฉํ•œ ๊ธฐ๊ฐ„ ๋Œ€๋น„ ๋ถ„์„

LAG์™€ LEAD๋Š” ์…€ํ”„ ์กฐ์ธ ์—†์ด ์ด์ „ ๋˜๋Š” ์ดํ›„ ํ–‰์˜ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ „๊ธฐ ๋Œ€๋น„ ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ, ํŠธ๋ Œ๋“œ ๊ฐ์ง€, ๋ณ€ํ™” ์‹๋ณ„์— ํ•„์ˆ˜์ ์ธ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

sql
-- period_over_period.sql
SELECT
  month,
  revenue,
  -- Previous month revenue
  LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
  -- Month-over-month growth rate
  ROUND(
    (revenue - LAG(revenue, 1) OVER (ORDER BY month)) * 100.0
    / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0),
    2
  ) AS mom_growth_pct,
  -- Next month revenue (forward-looking)
  LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_sales
ORDER BY month;

LAG/LEAD์˜ ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” ์˜คํ”„์…‹(๊ธฐ๋ณธ๊ฐ’ 1)์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ์„ธ ๋ฒˆ์งธ ์„ ํƒ์  ์ธ์ˆ˜๋Š” ํ•ด๋‹น ์˜คํ”„์…‹์— ํ–‰์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜๋ฉฐ, ์ฒซ ๋ฒˆ์งธ/๋งˆ์ง€๋ง‰ ํ–‰์—์„œ NULL์„ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์„ฑ์žฅ๋ฅ  ๊ณ„์‚ฐ์˜ NULLIF๋Š” 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

NTILE์„ ํ™œ์šฉํ•œ ๋ฐฑ๋ถ„์œ„ ๋ถ„ํ• 

NTILE์€ ์ง€์ •๋œ ์ˆ˜์˜ ๊ฑฐ์˜ ๋™์ผํ•œ ๊ทธ๋ฃน์œผ๋กœ ํ–‰์„ ๋ถ„๋ฐฐํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€๋Š” ์‚ฌ๋ถ„์œ„ ๋ถ„์„, ์‹ญ๋ถ„์œ„ ์Šค์ฝ”์–ด๋ง, ๊ณ ๊ฐ ์„ธ๋ถ„ํ™”์— ์ด ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

sql
-- customer_segmentation.sql
SELECT
  customer_id,
  total_spend,
  -- Split customers into 4 quartiles by spend
  NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile,
  -- Decile scoring for finer granularity
  NTILE(10) OVER (ORDER BY total_spend DESC) AS spend_decile
FROM (
  SELECT
    customer_id,
    SUM(order_total) AS total_spend
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY customer_id
) customer_totals;

์‚ฌ๋ถ„์œ„ 1์—๋Š” ์ง€์ถœ ๊ธˆ์•ก์ด ๊ฐ€์žฅ ๋†’์€ ๊ณ ๊ฐ์ด, ์‚ฌ๋ถ„์œ„ 4์—๋Š” ๊ฐ€์žฅ ๋‚ฎ์€ ๊ณ ๊ฐ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ๋งˆ์ผ€ํŒ… ๋ถ„์„์—์„œ ์‚ฌ์šฉ๋˜๋Š” RFM(์ตœ๊ทผ์„ฑ, ๋นˆ๋„, ๊ธˆ์•ก) ์„ธ๋ถ„ํ™”์— ์ง์ ‘ ๋Œ€์‘๋ฉ๋‹ˆ๋‹ค.

Data Analytics ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

๊ณตํ†ต ํ…Œ์ด๋ธ” ์‹(CTE): ์ค‘์ฒฉ ์„œ๋ธŒ์ฟผ๋ฆฌ์˜ ๋Œ€์ฒด

CTE(WITH ์ ˆ)๋Š” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ด๋ฆ„์ด ์ง€์ •๋œ ์ฝ๊ธฐ ์‰ฌ์šด ๋‹จ๊ณ„๋กœ ๋ถ„ํ•ดํ•ฉ๋‹ˆ๋‹ค. ๊ฐ CTE๋Š” ํ•ด๋‹น ์ฟผ๋ฆฌ์˜ ์‹คํ–‰ ๊ธฐ๊ฐ„ ๋™์•ˆ์—๋งŒ ์กด์žฌํ•˜๋Š” ์ž„์‹œ ๋ช…๋ช… ๊ฒฐ๊ณผ ์ง‘ํ•ฉ์œผ๋กœ ๊ธฐ๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ ์™ธ์—๋„, ๊ฐ ๋‹จ๊ณ„๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด ๋””๋ฒ„๊น…์ด ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค.

sql
-- cte_sales_analysis.sql
WITH monthly_revenue AS (
  -- Step 1: Aggregate raw orders into monthly totals
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    product_category,
    SUM(amount) AS revenue,
    COUNT(DISTINCT customer_id) AS unique_customers
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY DATE_TRUNC('month', order_date), product_category
),
ranked_categories AS (
  -- Step 2: Rank categories within each month
  SELECT
    month,
    product_category,
    revenue,
    unique_customers,
    RANK() OVER (
      PARTITION BY month
      ORDER BY revenue DESC
    ) AS category_rank
  FROM monthly_revenue
)
-- Step 3: Final output โ€” top 3 categories per month
SELECT
  month,
  product_category,
  revenue,
  unique_customers,
  category_rank
FROM ranked_categories
WHERE category_rank <= 3
ORDER BY month, category_rank;

์ด 3๋‹จ๊ณ„ ์ ‘๊ทผ ๋ฐฉ์‹์€ ๊นŠ์ด ์ค‘์ฒฉ๋œ ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค. ๊ฐ CTE๋Š” ์ง‘๊ณ„, ์ˆœ์œ„ ๋งค๊ธฐ๊ธฐ, ํ•„ํ„ฐ๋ง์ด๋ผ๋Š” ๋ช…ํ™•ํ•œ ์ฑ…์ž„์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์žฌ๊ท€ CTE๋ฅผ ํ™œ์šฉํ•œ ๊ณ„์ธต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

์žฌ๊ท€ CTE๋Š” ๊ณ„์ธต ๊ตฌ์กฐ ๋˜๋Š” ๊ทธ๋ž˜ํ”„ํ˜• ๋ฐ์ดํ„ฐ์™€ ๊ด€๋ จ๋œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์กฐ์ง๋„, ์นดํ…Œ๊ณ ๋ฆฌ ํŠธ๋ฆฌ, BOM(์ž์žฌ๋ช…์„ธ์„œ), ๊ฒฝ๋กœ ํƒ์ƒ‰ ์ฟผ๋ฆฌ ๋“ฑ์ด ๋Œ€ํ‘œ์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค.

sql
-- recursive_org_chart.sql
WITH RECURSIVE org_hierarchy AS (
  -- Base case: top-level managers (no manager above them)
  SELECT
    employee_id,
    employee_name,
    manager_id,
    1 AS depth,
    employee_name AS management_chain
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive case: join employees to their managers
  SELECT
    e.employee_id,
    e.employee_name,
    e.manager_id,
    oh.depth + 1,
    oh.management_chain || ' > ' || e.employee_name
  FROM employees e
  INNER JOIN org_hierarchy oh ON e.manager_id = oh.employee_id
)
SELECT
  employee_id,
  employee_name,
  depth,
  management_chain
FROM org_hierarchy
ORDER BY management_chain;

๋ฒ ์ด์Šค ์ผ€์ด์Šค๋Š” ๋ฃจํŠธ ๋…ธ๋“œ(์ƒ์œ„ ๊ด€๋ฆฌ์ž๊ฐ€ ์—†๋Š” ์ง์›)๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ์žฌ๊ท€ ์ผ€์ด์Šค๋Š” CTE ์ž์‹ ์— ๋‹ค์‹œ ์กฐ์ธํ•˜์—ฌ ๊ณ„์ธต์„ ๋ ˆ๋ฒจ๋ณ„๋กœ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. management_chain ์—ด์€ ์ด๋ฆ„์„ ์—ฐ๊ฒฐํ•˜์—ฌ ์ „์ฒด ๋ณด๊ณ  ๊ฒฝ๋กœ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋ฌดํ•œ ๋ฃจํ”„๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์žฌ๊ท€ ๊นŠ์ด๋ฅผ ์ œํ•œํ•˜๋ฉฐ, PostgreSQL์˜ ๊ธฐ๋ณธ๊ฐ’์€ 100ํšŒ์ž…๋‹ˆ๋‹ค.

์žฌ๊ท€ CTE ์„ฑ๋Šฅ ์ฃผ์˜

์žฌ๊ท€ CTE๋Š” ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์…‹์—์„œ ์ฒ˜๋ฆฌ ์†๋„๊ฐ€ ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ ๊นŠ์ด ์ œํ•œ(WHERE depth < 10)์„ ํฌํ•จํ•˜๊ณ , ์กฐ์ธ ์—ด(manager_id)์— ์ธ๋ฑ์Šค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งค์šฐ ๊นŠ์€ ๊ณ„์ธต ๊ตฌ์กฐ์˜ ๊ฒฝ์šฐ, ๊ตฌ์ฒดํ™”๋œ ๊ฒฝ๋กœ(Materialized Path) ๋˜๋Š” ์ค‘์ฒฉ ์ง‘ํ•ฉ(Nested Set) ํŒจํ„ด ๋„์ž…์„ ๊ฒ€ํ† ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๊ณ ๊ธ‰ ๋ถ„์„ ํŒจํ„ด: ๊ฐญ๊ณผ ์•„์ผ๋žœ๋“œ, ๋ˆ„์  ํ•ฉ๊ณ„

๊ฐญ๊ณผ ์•„์ผ๋žœ๋“œ ํŒจํ„ด์€ ๋ฐ์ดํ„ฐ ๋‚ด์˜ ์—ฐ์†์ ์ธ ์‹œํ€€์Šค๋ฅผ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. ํ™œ์„ฑ ๊ตฌ๋… ๊ธฐ๊ฐ„, ์—ฐ์† ๋กœ๊ทธ์ธ ์ผ์ˆ˜, ์ค‘๋‹จ ์—†๋Š” ๊ฐ€๋™ ๊ธฐ๊ฐ„ ๋“ฑ์ด ํ•ด๋‹น๋ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋ฒ•์€ ROW_NUMBER์™€ ๋‚ ์งœ ์—ฐ์‚ฐ์„ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.

sql
-- consecutive_login_streaks.sql
WITH login_days AS (
  SELECT DISTINCT
    user_id,
    DATE(login_timestamp) AS login_date
  FROM user_sessions
),
streaks AS (
  SELECT
    user_id,
    login_date,
    -- Subtracting row_number from date creates a constant for consecutive days
    login_date - (ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY login_date
    ))::int AS streak_group
  FROM login_days
)
SELECT
  user_id,
  MIN(login_date) AS streak_start,
  MAX(login_date) AS streak_end,
  COUNT(*) AS streak_length
FROM streaks
GROUP BY user_id, streak_group
HAVING COUNT(*) >= 3
ORDER BY streak_length DESC;

์ด ๊ธฐ๋ฒ•์˜ ํ•ต์‹ฌ ์›๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์—ฐ์†๋œ ๋‚ ์งœ์—์„œ ์ฆ๊ฐ€ํ•˜๋Š” ํ–‰ ๋ฒˆํ˜ธ๋ฅผ ๋นผ๋ฉด ํ•ญ์ƒ ๋™์ผํ•œ ๊ฐ’์ด ์‚ฐ์ถœ๋ฉ๋‹ˆ๋‹ค. ๊ฐญ์ด ๋ฐœ์ƒํ•˜๋ฉด ๊ฒฐ๊ณผ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์–ด ์ƒˆ๋กœ์šด ๊ทธ๋ฃน์ด ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ด ์ฟผ๋ฆฌ๋Š” ์‚ฌ์šฉ์ž๋ณ„๋กœ 3์ผ ์ด์ƒ์˜ ์—ฐ์† ๋กœ๊ทธ์ธ ์ŠคํŠธ๋ฆญ์„ ๋ชจ๋‘ ๊ฒ€์ถœํ•ฉ๋‹ˆ๋‹ค.

์œˆ๋„์šฐ ํ•จ์ˆ˜์™€ CASE ์‹์˜ ๊ฒฐํ•ฉ์„ ํ†ตํ•œ ์กฐ๊ฑด๋ถ€ ๋ถ„์„

์‹ค๋ฌด ๋ถ„์„ ์ฟผ๋ฆฌ์—์„œ๋Š” ์œˆ๋„์šฐ ํ•จ์ˆ˜์™€ CASE ์‹์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๋™์ผํ•œ ์ฟผ๋ฆฌ ๋‚ด์—์„œ ์กฐ๊ฑด๋ถ€ ๋ฉ”ํŠธ๋ฆญ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋นˆ๋ฒˆํ•ฉ๋‹ˆ๋‹ค.

sql
-- conditional_analytics.sql
SELECT
  order_date,
  product_category,
  amount,
  -- 7-day moving average
  AVG(amount) OVER (
    PARTITION BY product_category
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d,
  -- Cumulative sum resetting each quarter
  SUM(amount) OVER (
    PARTITION BY product_category, DATE_TRUNC('quarter', order_date)
    ORDER BY order_date
  ) AS qtd_cumulative,
  -- Flag if current row exceeds 2x the category average
  CASE
    WHEN amount > 2 * AVG(amount) OVER (PARTITION BY product_category)
    THEN 'OUTLIER'
    ELSE 'NORMAL'
  END AS outlier_flag
FROM daily_sales
ORDER BY product_category, order_date;

์ด ๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ 7์ผ ์ด๋™ ํ‰๊ท , ๋ถ„๊ธฐ๋ณ„ ๋ฆฌ์…‹ ๋ˆ„์  ํ•ฉ๊ณ„, ์ด์ƒ์น˜ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋™์‹œ์— ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ธŒ์ฟผ๋ฆฌ๋‚˜ ์…€ํ”„ ์กฐ์ธ์ด ์ „ํ˜€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์œˆ๋„์šฐ ํ•จ์ˆ˜ ์‹คํ–‰ ์ˆœ์„œ

์œˆ๋„์šฐ ํ•จ์ˆ˜๋Š” WHERE, GROUP BY, HAVING ์ดํ›„์— ์‹คํ–‰๋˜๋ฉฐ, ORDER BY์™€ LIMIT ์ด์ „์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ WHERE ์ ˆ์—์„œ ์œˆ๋„์šฐ ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์œˆ๋„์šฐ ํ•จ์ˆ˜ ๊ฒฐ๊ณผ๋กœ ํ•„ํ„ฐ๋งํ•˜๋ ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ CTE ๋˜๋Š” ์„œ๋ธŒ์ฟผ๋ฆฌ๋กœ ๋ž˜ํ•‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ถ„์„ ์ฟผ๋ฆฌ์˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”

์œˆ๋„์šฐ ํ•จ์ˆ˜์™€ CTE๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ด์ง€๋งŒ ๋Œ€๊ทœ๋ชจ ํ…Œ์ด๋ธ”์—์„œ๋Š” ๋ณ‘๋ชฉ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทœ๋ชจ์— ๊ด€๊ณ„์—†์ด ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ฃผ์š” ๊ธฐ๋ฒ•์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

์ฒซ์งธ, PARTITION BY ๋ฐ ORDER BY ์ ˆ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์—ด์— ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์œˆ๋„์šฐ ์ •์˜์™€ ์ผ์น˜ํ•˜๋Š” ๋ณตํ•ฉ ์ธ๋ฑ์Šค๊ฐ€ ์žˆ์œผ๋ฉด ์ •๋ ฌ ์—ฐ์‚ฐ์„ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‘˜์งธ, ์œˆ๋„์šฐ ํ•จ์ˆ˜๋ฅผ ์ ์šฉํ•˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. WHERE ์ ˆ(์œˆ๋„์šฐ ํ•จ์ˆ˜ ํ‰๊ฐ€ ์ „์— ์‹คํ–‰๋จ)์— ์กฐ๊ฑด์„ ๋ฐฐ์น˜ํ•˜๋ฉด ์œˆ๋„์šฐ๊ฐ€ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์…‹์งธ, ์ค‘๋ณต๋œ ์œˆ๋„์šฐ ์ •์˜๋ฅผ ํ”ผํ•ฉ๋‹ˆ๋‹ค. ๋ช…๋ช…๋œ ์œˆ๋„์šฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ˜๋ณต์„ ์ค„์ด๊ณ , ์˜ตํ‹ฐ๋งˆ์ด์ €์—๊ฒŒ ์—ฌ๋Ÿฌ ํ•จ์ˆ˜๊ฐ€ ๋™์ผํ•œ ํ”„๋ ˆ์ž„์„ ๊ณต์œ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

sql
-- named_window.sql
SELECT
  department,
  employee_id,
  salary,
  RANK() OVER dept_window AS dept_rank,
  SUM(salary) OVER dept_window AS dept_total,
  AVG(salary) OVER dept_window AS dept_avg
FROM employees
WINDOW dept_window AS (
  PARTITION BY department
  ORDER BY salary DESC
);

WINDOW ์ ˆ(PostgreSQL, MySQL 8 ์ด์ƒ, BigQuery์—์„œ ์ง€์›)์€ ์œˆ๋„์šฐ๋ฅผ ํ•œ ๋ฒˆ ์ •์˜ํ•˜๊ณ  ์—ฌ๋Ÿฌ ํ•จ์ˆ˜์—์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ๊ณผ ์ฟผ๋ฆฌ ํ”Œ๋žœ ์ตœ์ ํ™”์— ๋ชจ๋‘ ๊ธฐ์—ฌํ•ฉ๋‹ˆ๋‹ค.

Data Analytics ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

๊ฒฐ๋ก 

  • ์œˆ๋„์šฐ ํ•จ์ˆ˜(ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE)๋Š” GROUP BY ์ง‘๊ณ„์™€ ๋‹ฌ๋ฆฌ ํ–‰์„ ์ถ•์†Œํ•˜์ง€ ์•Š๊ณ  ํ–‰ ์ง‘ํ•ฉ์— ๋Œ€ํ•ด ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค
  • CTE๋Š” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋ช…๋ช…๋œ, ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๋‹จ๊ณ„๋กœ ๋ถ„ํ•ดํ•˜๊ณ  ๊นŠ์ด ์ค‘์ฒฉ๋œ ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค
  • ์žฌ๊ท€ CTE๋Š” ์กฐ์ง๋„๋‚˜ ์นดํ…Œ๊ณ ๋ฆฌ ํŠธ๋ฆฌ์™€ ๊ฐ™์€ ๊ณ„์ธต ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ๊นŠ์ด ์ œํ•œ๊ณผ ์ธ๋ฑ์Šค๋œ ์กฐ์ธ ์—ด์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค
  • ๊ฐญ๊ณผ ์•„์ผ๋žœ๋“œ ๊ธฐ๋ฒ•(ROW_NUMBER + ๋‚ ์งœ ์—ฐ์‚ฐ)์€ ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ์˜ ์—ฐ์† ์‹œํ€€์Šค๋ฅผ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค
  • ๋ช…๋ช…๋œ ์œˆ๋„์šฐ(WINDOW ์ ˆ)๋Š” ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ด๊ณ  ์ฟผ๋ฆฌ ํ”Œ๋žœ ์ตœ์ ํ™”๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • WHERE ์ ˆ์—์„œ ์œˆ๋„์šฐ ํ•จ์ˆ˜ ํ‰๊ฐ€ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋Œ€๊ทœ๋ชจ ํ…Œ์ด๋ธ”์—์„œ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
  • ์ด๋Ÿฌํ•œ ํŒจํ„ด์€ ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘์—์„œ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ถœ์ œ๋˜๋ฉฐ, ์ผ์ƒ์ ์ธ ๋ฆฌํฌํŒ…, ์„ธ๋ถ„ํ™”, ํŠธ๋ Œ๋“œ ๋ถ„์„์—๋„ ์ง์ ‘ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€

Data Analytics ์ฝ”๋“œ์˜ ๋ฒ„๊ทธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋‚˜์š”

์‹ค์ œ ์ฝ”๋“œ ํ•œ ์กฐ๊ฐ, ์ˆจ์€ ๋ฒ„๊ทธ ํ•˜๋‚˜, ํ•˜๋ฃจ ํ•œ ๋ฒˆ. ๊ณ„์ • ์—†์ด ๋ฐ”๋กœ ๋„์ „ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Anthony Fillion-Maillet

์ž‘์„ฑ์ž

Anthony Fillion-Maillet

SharpSkill ์ฐฝ์—…์ž

10๋…„ ์ด์ƒ ํ’€์Šคํƒ ๊ฐœ๋ฐœ์„ ํ•ด์™”์Šต๋‹ˆ๋‹ค. SharpSkill์„ ์šด์˜ํ•˜๋ฉฐ ์ด๊ณณ์— ๊ฒŒ์‹œ๋˜๋Š” ๋ชจ๋“  ๋‚ด์šฉ์— ์ฑ…์ž„์„ ์ง‘๋‹ˆ๋‹ค.

2026๋…„ 4์›” 9์ผ ์—…๋ฐ์ดํŠธ

ํƒœ๊ทธ

#sql
#data-analytics
#window-functions
#cte
#interview

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

Data Analyst Interview Questions Italy 2026

2026๋…„ ์ดํƒˆ๋ฆฌ์•„ ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘ ์งˆ๋ฌธ: SQL, Python, ๋ถ„์„ ์Šคํ‚ฌ ์™„์ „ ๊ฐ€์ด๋“œ

2026๋…„ ์ดํƒˆ๋ฆฌ์•„์—์„œ ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€๋กœ ์ทจ์—…ํ•˜๊ธฐ ์œ„ํ•œ ๋ฉด์ ‘ ์ค€๋น„ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. SQL, Python, pandas, ๋น„์ฆˆ๋‹ˆ์Šค ๋ถ„์„์— ๊ด€ํ•œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ์งˆ๋ฌธ๊ณผ ๋ชจ๋ฒ” ๋‹ต๋ณ€์„ ์ƒ์„ธํžˆ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘ ์งˆ๋ฌธ 2026๋…„ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘ ์งˆ๋ฌธ 2026๋…„ ์™„๋ฒฝ ๊ฐ€์ด๋“œ: SQL, Python, ๋ถ„์„ ์Šคํ‚ฌ

2026๋…„ ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€ ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” SQL, Python, ํ†ต๊ณ„ ๋ถ„์„ ์งˆ๋ฌธ๊ณผ ๋ชจ๋ฒ” ๋‹ต๋ณ€์„ ์ƒ์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์‹ค์ „ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•ต์‹ฌ ํฌ์ธํŠธ๋กœ ์ทจ์—… ์„ฑ๊ณต์„ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

dbt data build tool for data analysts modeling testing

2026๋…„ ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€๋ฅผ ์œ„ํ•œ dbt ์™„๋ฒฝ ๊ฐ€์ด๋“œ: ๋ชจ๋ธ๋ง, ํ…Œ์ŠคํŠธ, ๋ฉด์ ‘ ์งˆ๋ฌธ

dbt ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ, ๋จธํ‹ฐ๋ฆฌ์–ผ๋ผ์ด์ œ์ด์…˜ ์ „๋žต, ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ํ…Œ์ŠคํŠธ, Jinja ๋งคํฌ๋กœ, ๊ทธ๋ฆฌ๊ณ  ์‹ค๋ฌด ๋ฉด์ ‘์—์„œ ์ž์ฃผ ๋“ฑ์žฅํ•˜๋Š” ์งˆ๋ฌธ๊นŒ์ง€ ํฌ๊ด„์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.