๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์ ์ํ ๊ณ ๊ธ SQL: ์๋ธ์ฟผ๋ฆฌ, ํผ๋ฒ, ์ฟผ๋ฆฌ ์ต์ ํ ์๋ฒฝ ๊ฐ์ด๋ (2026๋ )
๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ ๊ณ ๊ธ SQL ์ฃผ์ ๋ฅผ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃน๋๋ค. ์๊ด ์๋ธ์ฟผ๋ฆฌ, ์กฐ๊ฑด๋ถ ์ง๊ณ๋ฅผ ํ์ฉํ ํผ๋ฒ ์ฟผ๋ฆฌ, EXPLAIN ๋ถ์, ์ธ๋ฑ์ฑ ์ ๋ต, ๊ทธ๋ฆฌ๊ณ ํํ ์ํฐํจํด๊น์ง ์ค์ ์ฝ๋์ ํจ๊ป ์ ๋ฆฌํฉ๋๋ค.

๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์์ SQL์ ๋จ์ํ SELECT ๋ฌธ์ ๋์ด ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ ์ฟผ๋ฆฌ๋ก ํํํ๋ ๋ฅ๋ ฅ์ ํ๊ฐํฉ๋๋ค. 2026๋ ํ์ฌ ๊ธฐ์ ๋ค์ ์๋ธ์ฟผ๋ฆฌ์ ์คํ ์๋ฆฌ๋ฅผ ์ดํดํ๊ณ , ํ ๋ฐ์ดํฐ๋ฅผ ์ด๋ก ๋ณํํ๋ ํผ๋ฒ ๊ธฐ๋ฒ์ ๊ตฌ์ฌํ๋ฉฐ, ์คํ ๊ณํ์ ๋ถ์ํ์ฌ ์ฑ๋ฅ ๋ณ๋ชฉ์ ํด๊ฒฐํ ์ ์๋ ๋ถ์๊ฐ๋ฅผ ์ฐพ๊ณ ์์ต๋๋ค. ์ด ๊ธ์์๋ ๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์์ ๊ฐ์ฅ ๋น๋ฒํ๊ฒ ๋ฑ์ฅํ๋ ๊ณ ๊ธ SQL ์ฃผ์ ๋ค์ ์ค์ ์ฝ๋์ ํจ๊ป ์ฒด๊ณ์ ์ผ๋ก ์ ๋ฆฌํฉ๋๋ค.
๋ฉด์ ๊ด์ ๋จ์ํ ์ ๋ต ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ ๋ฅ๋ ฅ๋ง ๋ณด์ง ์์ต๋๋ค. ์๋ธ์ฟผ๋ฆฌ์ JOIN์ ์ฑ๋ฅ ์ฐจ์ด๋ฅผ ์ค๋ช ํ ์ ์๋์ง, ์ ํน์ ์ ๊ทผ๋ฒ์ ์ ํํ๋์ง ๋ ผ๋ฆฌ์ ์ผ๋ก ๊ทผ๊ฑฐ๋ฅผ ์ ์ํ ์ ์๋์ง, ๊ทธ๋ฆฌ๊ณ ๋๊ท๋ชจ ๋ฐ์ดํฐ์ ์์ ์ฟผ๋ฆฌ๊ฐ ์ด๋ป๊ฒ ๋์ํ ์ง ์์ธกํ ์ ์๋์ง๋ฅผ ์ข ํฉ์ ์ผ๋ก ํ๊ฐํฉ๋๋ค.
์๊ด ์๋ธ์ฟผ๋ฆฌ์ ์ผ๋ฐ ์๋ธ์ฟผ๋ฆฌ์ ์ฐจ์ด
์๋ธ์ฟผ๋ฆฌ๋ ๋ค๋ฅธ ์ฟผ๋ฆฌ ๋ด๋ถ์ ์ค์ฒฉ๋ SELECT ๋ฌธ์ ๋๋ค. ์ผ๋ฐ ์๋ธ์ฟผ๋ฆฌ(non-correlated subquery)๋ ์ธ๋ถ ์ฟผ๋ฆฌ์ ๋ ๋ฆฝ์ ์ผ๋ก ํ ๋ฒ๋ง ์คํ๋ฉ๋๋ค. ํ๊ท ์ฃผ๋ฌธ ๊ธ์ก๋ณด๋ค ํฐ ์ฃผ๋ฌธ์ ์ฐพ๋ ๋ค์ ์์์์, ๋ด๋ถ SELECT๋ ์ ์ฒด ํ ์ด๋ธ์ ๋ํด ๋จ ํ ๋ฒ ์คํ๋์ด ์ค์นผ๋ผ ๊ฐ์ ๋ฐํํฉ๋๋ค.
-- regular_subquery_threshold.sql
-- Find all orders above the average order value
SELECT
order_id,
customer_id,
total_amount
FROM orders
WHERE total_amount > (
-- Executes once, returns a single scalar value
SELECT AVG(total_amount)
FROM orders
);๋ฐ๋ฉด, ์๊ด ์๋ธ์ฟผ๋ฆฌ(correlated subquery)๋ ์ธ๋ถ ์ฟผ๋ฆฌ์ ๊ฐ ํ์ ๋ํด ๋ฐ๋ณต ์คํ๋ฉ๋๋ค. ์๋ ์ฟผ๋ฆฌ๋ ๊ฐ ์ง์์ ๋ถ์ ํ๊ท ๊ธ์ฌ๋ฅผ ๊ธฐ์ค์ผ๋ก ๋น๊ตํ๊ธฐ ๋๋ฌธ์, ์ธ๋ถ ์ฟผ๋ฆฌ๊ฐ ์ฒ๋ฆฌํ๋ ํ๋ง๋ค ๋ด๋ถ ์ฟผ๋ฆฌ๊ฐ ์ฌํ๊ฐ๋ฉ๋๋ค.
-- correlated_subquery_department.sql
-- Employees earning above their department average
SELECT
e.employee_id,
e.name,
e.department,
e.salary
FROM employees e
WHERE e.salary > (
-- Re-evaluated for each employee's department
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department = e.department
);์๊ด ์๋ธ์ฟผ๋ฆฌ๋ ์ง๊ด์ ์ด์ง๋ง, ํ ์๊ฐ ์ฆ๊ฐํ๋ฉด O(N*M) ๋ณต์ก๋๋ก ์ฑ๋ฅ์ด ๊ธ๊ฒฉํ ์ ํ๋ฉ๋๋ค. ๋ฉด์ ์์๋ ์ด๋ฅผ CTE(Common Table Expression)๋ ์๋์ฐ ํจ์๋ก ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์ ์ํ๋ฉด ๋์ ์ ์๋ฅผ ๋ฐ์ ์ ์์ต๋๋ค. ์๋๋ ๋์ผํ ๊ฒฐ๊ณผ๋ฅผ ๋จ์ผ ํจ์ค๋ก ์ฐ์ถํ๋ CTE ํ์ฉ ์์์ ๋๋ค.
-- optimized_with_cte.sql
-- Same result, single pass over the data
WITH dept_avg AS (
SELECT
department,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT
e.employee_id,
e.name,
e.department,
e.salary
FROM employees e
JOIN dept_avg d ON e.department = d.department
WHERE e.salary > d.avg_salary;CTE๋ฅผ ์ฌ์ฉํ๋ฉด ๋ถ์๋ณ ํ๊ท ์ ํ ๋ฒ๋ง ๊ณ์ฐํ๊ณ JOIN์ผ๋ก ์ฐ๊ฒฐํ๋ฏ๋ก, ํ ์ด๋ธ ์ค์บ ํ์๊ฐ ํฌ๊ฒ ์ค์ด๋ญ๋๋ค. ๋ฉด์ ์ํฉ์์ ์๊ด ์๋ธ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ํ ์ฆ์ CTE ๊ธฐ๋ฐ ์ต์ ํ ๋ฒ์ ์ ์ ์ํ๋ฉด, ์ฑ๋ฅ ์์(performance awareness)์ ํจ๊ณผ์ ์ผ๋ก ๋ณด์ฌ์ค ์ ์์ต๋๋ค.
๋ฐ์ดํฐ ๋ถ์๊ฐ ๊ธฐ์ ์คํฌ๋ฆฌ๋์ ์๋ธ์ฟผ๋ฆฌ ํจํด
๋ฉด์ ์์ ์์ฃผ ๋ฑ์ฅํ๋ ๋ ๋ค๋ฅธ ํจํด์ ์กด์ฌ ์ฌ๋ถ ํ์ธ์ ๋๋ค. 2026๋ ์ ์ฃผ๋ฌธ์ ํ ๊ณ ๊ฐ์ ์ฐพ๋ ๋ ๊ฐ์ง ์ ๊ทผ๋ฒ์ ๋น๊ตํ๋ฉด, EXISTS๋ ์ฒซ ๋ฒ์งธ ๋งค์นญ ํ์ ๋ฐ๊ฒฌํ๋ ์ฆ์ ํ์์ ์ค๋จํ๋ ๋ฐ๋ฉด, IN์ ์๋ธ์ฟผ๋ฆฌ์ ์ ์ฒด ๊ฒฐ๊ณผ ์งํฉ์ ๋จผ์ ๊ตฌ์ฑํฉ๋๋ค.
-- exists_vs_in.sql
-- Customers who placed at least one order in 2026 (EXISTS โ preferred)
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_date >= '2026-01-01'
);
-- Equivalent with IN (slower on large datasets)
SELECT customer_id, name
FROM customers
WHERE customer_id IN (
SELECT customer_id
FROM orders
WHERE order_date >= '2026-01-01'
);๋๊ท๋ชจ ๋ฐ์ดํฐ์ ์์๋ EXISTS๊ฐ IN๋ณด๋ค ํจ์จ์ ์ธ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ์ตํฐ๋ง์ด์ ๊ฐ semi-join์ผ๋ก ๋ณํํ ์ ์์ผ๋ฉฐ, ์กฐ๊ธฐ ์ข ๋ฃ(early termination)๊ฐ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ๋ฉด์ ์์ ์ด ์ฐจ์ด๋ฅผ ์ค๋ช ํ ์ ์์ผ๋ฉด ์ฟผ๋ฆฌ ์์ง ๋ด๋ถ ๋์์ ๋ํ ์ดํด๋๋ฅผ ์ฆ๋ช ํ ์ ์์ต๋๋ค.
SELECT ์ ๋ด์ ์ค์นผ๋ผ ์๋ธ์ฟผ๋ฆฌ๋ ๋น์ถ ํจํด์ ๋๋ค. ๊ฐ ์ ํ๊ณผ ํด๋น ์นดํ ๊ณ ๋ฆฌ์ ์ด ๋งค์ถ์ ํจ๊ป ์กฐํํ๋ ๋ค์ ์์๋, ๋ถ์ ๋ฆฌํฌํธ ์์ฑ ์ ์์ฃผ ๋ง์ฃผ์น๋ ์๊ตฌ์ฌํญ์ ๋ฐ์ํฉ๋๋ค.
-- scalar_subquery_select.sql
-- Each product with its category's total revenue
SELECT
p.product_id,
p.product_name,
p.category,
(
SELECT SUM(oi.quantity * oi.unit_price)
FROM order_items oi
JOIN products p2 ON oi.product_id = p2.product_id
WHERE p2.category = p.category
) AS category_total_revenue
FROM products p;์ด ์ฟผ๋ฆฌ ์ญ์ ์๊ด ์๋ธ์ฟผ๋ฆฌ์ด๋ฏ๋ก ์นดํ ๊ณ ๋ฆฌ ์๊ฐ ๋ง์์ง๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ค๋ฌด์์๋ CTE๋ ์๋์ฐ ํจ์(SUM OVER PARTITION BY)๋ก ๋์ฒดํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ ๋๋ค.
Data Analytics ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
์กฐ๊ฑด๋ถ ์ง๊ณ๋ฅผ ํ์ฉํ ํผ๋ฒ ์ฟผ๋ฆฌ
ํ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ฅผ ์ด ๊ธฐ๋ฐ์ผ๋ก ๋ณํํ๋ ํผ๋ฒ(Pivot)์ ๋ฐ์ดํฐ ๋ถ์ ๋ฉด์ ์ ํต์ฌ ์ฃผ์ ์ ๋๋ค. ํ์ค SQL์์๋ CASE WHEN๊ณผ ์ง๊ณ ํจ์๋ฅผ ๊ฒฐํฉํ์ฌ ํผ๋ฒ์ ๊ตฌํํฉ๋๋ค. ์๋ ์์๋ ์ ํ๋ณ ์๊ฐ ๋งค์ถ์ ํ๋์ ํ์ ์๋ณ ์ด๋ก ํ์ํฉ๋๋ค.
-- pivot_monthly_revenue.sql
-- Monthly revenue pivot: one row per product, one column per month
SELECT
product_id,
product_name,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1
THEN quantity * unit_price ELSE 0 END) AS jan_revenue,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2
THEN quantity * unit_price ELSE 0 END) AS feb_revenue,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3
THEN quantity * unit_price ELSE 0 END) AS mar_revenue,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4
THEN quantity * unit_price ELSE 0 END) AS apr_revenue,
-- Repeat for remaining months
SUM(quantity * unit_price) AS total_revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date >= '2026-01-01'
GROUP BY product_id, product_name
ORDER BY total_revenue DESC;์ด ํจํด์ DBMS์ ๊ด๊ณ์์ด ๋์ํ๋ฏ๋ก ๋ฉด์ ์์ ๊ฐ์ฅ ์์ ํ ์ ๊ทผ๋ฒ์ ๋๋ค. ์กฐ๊ฑด๋ถ ์ง๊ณ๋ ๋งค์ถ ๋ถ์ ์ธ์๋ ์ฌ์ฉ์ ํ๋ ๋ถ์์ ๊ด๋ฒ์ํ๊ฒ ํ์ฉ๋ฉ๋๋ค. ๋ค์์ ๋๋ฐ์ด์ค ์ ํ๋ณ ์ธ์ ๋ถํฌ๋ฅผ ๋ถ์ํ๋ ์์์ ๋๋ค.
-- pivot_user_activity.sql
-- User activity: sessions by day of week and device type
SELECT
user_id,
COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) AS mobile_sessions,
COUNT(CASE WHEN device_type = 'desktop' THEN 1 END) AS desktop_sessions,
COUNT(CASE WHEN device_type = 'tablet' THEN 1 END) AS tablet_sessions,
ROUND(
COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) * 100.0 / COUNT(*),
1
) AS mobile_pct
FROM user_sessions
WHERE session_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY user_id
HAVING COUNT(*) >= 5 -- Only users with meaningful activity
ORDER BY mobile_pct DESC;HAVING ์ ์ ํตํด ์๋ฏธ ์๋ ํ๋๋์ ๊ฐ์ง ์ฌ์ฉ์๋ง ํํฐ๋งํ๋ ๊ฒ์ ๋ ธ์ด์ฆ๋ฅผ ์ ๊ฑฐํ๋ ์ค๋ฌด์ ๊ธฐ๋ฒ์ ๋๋ค. ๋ฉด์ ์์ ์ด๋ฐ ์ธ๋ถ ์ฌํญ์ ์์ฐ์ค๋ฝ๊ฒ ํฌํจํ๋ฉด ์ค๋ฌด ๊ฒฝํ์ ๊ฐ์ ์ ์ผ๋ก ์ดํํ ์ ์์ต๋๋ค.
PostgreSQL์ CROSSTAB์ ํ์ฉํ ๋์ ํผ๋ฒ
PostgreSQL ํ๊ฒฝ์์๋ tablefunc ํ์ฅ์ CROSSTAB ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ฒด๊ณ์ ์ธ ํผ๋ฒ์ ๊ตฌํํ ์ ์์ต๋๋ค. ๋ถ๊ธฐ๋ณ ์ ํ ๋งค์ถ์ ํผ๋ฒํ๋ ๋ค์ ์์๋ PostgreSQL ํนํ ๋ฉด์ ์์ ์ฐจ๋ณํ ํฌ์ธํธ๊ฐ ๋ฉ๋๋ค.
-- crosstab_dynamic_pivot.sql
-- Enable the extension (once per database)
CREATE EXTENSION IF NOT EXISTS tablefunc;
-- Revenue by product and quarter using CROSSTAB
SELECT *
FROM crosstab(
$$
SELECT
product_name,
'Q' || EXTRACT(QUARTER FROM order_date)::TEXT AS quarter,
SUM(quantity * unit_price) AS revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE EXTRACT(YEAR FROM o.order_date) = 2026
GROUP BY product_name, quarter
ORDER BY product_name, quarter
$$,
$$ VALUES ('Q1'), ('Q2'), ('Q3'), ('Q4') $$
) AS pivot_table(
product_name TEXT,
q1_revenue NUMERIC,
q2_revenue NUMERIC,
q3_revenue NUMERIC,
q4_revenue NUMERIC
);CROSSTAB์ ์ฒซ ๋ฒ์งธ ์ธ์๋ ํ ์๋ณ์, ์นดํ ๊ณ ๋ฆฌ, ๊ฐ ์์๋ก ์ ๋ ฌ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ ์ฟผ๋ฆฌ์ด๋ฉฐ, ๋ ๋ฒ์งธ ์ธ์๋ ํผ๋ฒ ์ด์ด ๋ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก์ ๋๋ค. ๊ฒฐ๊ณผ ์คํค๋ง๋ AS ์ ์์ ๋ช ์์ ์ผ๋ก ์ ์ํฉ๋๋ค. CROSSTAB์ ์กฐ๊ฑด๋ถ ์ง๊ณ๋ณด๋ค ๊ฐ๋ ์ฑ์ด ๋์ง๋ง, PostgreSQL ์ ์ฉ์ด๋ผ๋ ์ ์์ ๋ฉด์ ์ DBMS ๋ฒ์ฉ์ฑ์ ํจ๊ป ์ธ๊ธํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
SQL ์ฟผ๋ฆฌ ์ต์ ํ: EXPLAIN ๊ณํ๊ณผ ๋น์ฉ ๋ถ์
์ฟผ๋ฆฌ ์ต์ ํ ์ญ๋์ ์๋์ด ๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์์ ํ์์ ์ผ๋ก ๊ฒ์ฆํ๋ ํญ๋ชฉ์ ๋๋ค. EXPLAIN ANALYZE๋ ์ฟผ๋ฆฌ ์ตํฐ๋ง์ด์ ๊ฐ ์์ฑํ ์คํ ๊ณํ๊ณผ ์ค์ ์คํ ํต๊ณ๋ฅผ ํจ๊ป ๋ณด์ฌ์ค๋๋ค.
-- explain_analyze_example.sql
-- Analyze a slow query to identify bottlenecks
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT
c.customer_id,
c.name,
COUNT(o.order_id) AS order_count,
SUM(o.total_amount) AS lifetime_value
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= '2025-01-01'
GROUP BY c.customer_id, c.name
HAVING SUM(o.total_amount) > 1000
ORDER BY lifetime_value DESC;EXPLAIN ์ถ๋ ฅ์์ ํ์ธํด์ผ ํ ํต์ฌ ์งํ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. Seq Scan์ ์ธ๋ฑ์ค ์์ด ์ ์ฒด ํ ์ด๋ธ์ ์์ฐจ ํ์ํ๋ ๊ฒ์ผ๋ก, ๋ํ ํ ์ด๋ธ์์ ๋ํ๋๋ฉด ์ธ๋ฑ์ค ์ถ๊ฐ๋ฅผ ๊ฒํ ํด์ผ ํฉ๋๋ค. Nested Loop์ ์๊ท๋ชจ ๊ฒฐ๊ณผ์ ์๋ ์ ํฉํ์ง๋ง, ์์ชฝ ํ ์ด๋ธ์ด ํฌ๋ฉด Hash Join์ด๋ Merge Join์ด ๋ ํจ์จ์ ์ ๋๋ค. BUFFERS ์ต์ ์ ๋์คํฌ I/O์ ์บ์ ํํธ ๋น์จ์ ๋ณด์ฌ์ฃผ์ด ๋ฉ๋ชจ๋ฆฌ ํ๋ ๋ฐฉํฅ์ ์ ์ํฉ๋๋ค.
๋ฉด์ ์์ EXPLAIN ์ถ๋ ฅ์ ํด์ํ๋ ๋ฅ๋ ฅ์ ๋ณด์ฌ์ฃผ๋ฉด, ๋จ์ํ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ ์์ค์ ๋์ด ์ด์ ํ๊ฒฝ์์์ ์ฑ๋ฅ ๊ด๋ฆฌ ์ญ๋๊น์ง ์ ์ฆํ ์ ์์ต๋๋ค.
๋ฉด์ ์์ค์ SQL ์ต์ ํ๋ฅผ ์ํ ์ธ๋ฑ์ฑ ์ ๋ต
์ ์ ํ ์ธ๋ฑ์ค๋ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ ๊ทน์ ์ผ๋ก ๊ฐ์ ํฉ๋๋ค. ๋ฉด์ ์์๋ ์ด๋ค ์ด์ ์ธ๋ฑ์ค๋ฅผ ์์ฑํด์ผ ํ๋์ง, ๋ณตํฉ ์ธ๋ฑ์ค์ ์ด ์์๊ฐ ์ ์ค์ํ์ง, ์ปค๋ฒ๋ง ์ธ๋ฑ์ค๊ฐ ๋ฌด์์ธ์ง๋ฅผ ์ค๋ช ํ ์ ์์ด์ผ ํฉ๋๋ค.
-- indexing_strategies.sql
-- Index on order_date: filters a small percentage of rows
CREATE INDEX idx_orders_date ON orders (order_date);
-- Composite index for queries filtering on both columns
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date);
-- Covering index: includes columns needed in SELECT
-- Enables Index Only Scan (no table access)
CREATE INDEX idx_orders_covering
ON orders (customer_id, order_date)
INCLUDE (total_amount, status);๋ณตํฉ ์ธ๋ฑ์ค์์ ์ด ์์๋ ์ ํ๋(selectivity)์ ์ฟผ๋ฆฌ ํจํด์ ๋ฐ๋ผ ๊ฒฐ์ ํฉ๋๋ค. WHERE ์ ์์ ๋ฑํธ ์กฐ๊ฑด์ผ๋ก ์ฌ์ฉ๋๋ ์ด์ ์์, ๋ฒ์ ์กฐ๊ฑด์ผ๋ก ์ฌ์ฉ๋๋ ์ด์ ๋ค์ ๋ฐฐ์นํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ธ ์์น์ ๋๋ค. ์ปค๋ฒ๋ง ์ธ๋ฑ์ค์ INCLUDE ์ ์ ์ธ๋ฑ์ค์ ์ถ๊ฐ ์ด์ ํฌํจ์์ผ ํ ์ด๋ธ ํ ์ ๊ทผ ์์ด Index Only Scan์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ ์ ์๊ฒ ํฉ๋๋ค.
๋ถ๋ถ ์ธ๋ฑ์ค(Partial Index)๋ ํน์ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ํ๋ง ์ธ๋ฑ์ฑํ์ฌ ์ธ๋ฑ์ค ํฌ๊ธฐ๋ฅผ ์ค์ด๊ณ ์ฐ๊ธฐ ์ฑ๋ฅ ์ค๋ฒํค๋๋ฅผ ์ต์ํํฉ๋๋ค.
-- partial_index.sql
-- Only index active orders (smaller, faster index)
CREATE INDEX idx_orders_active
ON orders (customer_id, order_date)
WHERE status = 'active';
-- Only index recent data for dashboard queries
CREATE INDEX idx_orders_recent
ON orders (order_date)
WHERE order_date >= '2026-01-01';๋์๋ณด๋ ์ฟผ๋ฆฌ๊ฐ ํญ์ ์ต๊ทผ ๋ฐ์ดํฐ๋ง ์กฐํํ๋ค๋ฉด, ๋ถ๋ถ ์ธ๋ฑ์ค๋ฅผ ํตํด ์ ์ฒด ํ ์ด๋ธ ๋๋น ํจ์ฌ ์์ ์ธ๋ฑ์ค๋ก ๋์ผํ ์ฑ๋ฅ์ ํ๋ณดํ ์ ์์ต๋๋ค.
์ธ๋ฑ์ค๋ฅผ ๋ฌด์กฐ๊ฑด ๋ง์ด ์์ฑํ๋ ๊ฒ์ ํด๊ฒฐ์ฑ ์ด ์๋๋๋ค. ๊ฐ ์ธ๋ฑ์ค๋ INSERT, UPDATE, DELETE ์ฐ์ฐ๋ง๋ค ์ ์ง๋ณด์ ๋น์ฉ์ ๋ฐ์์ํต๋๋ค. ์ฌ์ฉํ์ง ์๋ ์ธ๋ฑ์ค๋ ์ฐ๊ธฐ ์ฑ๋ฅ๋ง ์ ํ์ํค๋ฏ๋ก, pg_stat_user_indexes ๋ทฐ๋ฅผ ํตํด ์ธ๋ฑ์ค ์ฌ์ฉ ํํฉ์ ์ฃผ๊ธฐ์ ์ผ๋ก ๋ชจ๋ํฐ๋งํด์ผ ํฉ๋๋ค.
ํํ ์ฟผ๋ฆฌ ์ํฐํจํด
๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ ์ํฐํจํด ์ค ํ๋๋ ์ธ๋ฑ์ค ์ด์ ํจ์๋ฅผ ์ ์ฉํ๋ ๊ฒ์ ๋๋ค. WHERE ์ ์์ ์ด์ ํจ์๋ฅผ ์ ์ฉํ๋ฉด ์ตํฐ๋ง์ด์ ๊ฐ ํด๋น ์ด์ ์ธ๋ฑ์ค๋ฅผ ํ์ฉํ ์ ์๊ฒ ๋ฉ๋๋ค.
-- anti_patterns.sql
-- BAD: function on indexed column disables the index
SELECT * FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2026;
-- GOOD: range comparison uses the index
SELECT * FROM orders
WHERE order_date >= '2026-01-01'
AND order_date < '2027-01-01';EXTRACT ํจ์๋ฅผ ์ ์ฉํ๋ฉด order_date ์ด์ ๋ชจ๋ ๊ฐ์ ๋ณํํ ํ ๋น๊ตํด์ผ ํ๋ฏ๋ก ์ธ๋ฑ์ค ์ค์บ์ด ๋ถ๊ฐ๋ฅํด์ง๋๋ค. ๋ฒ์ ๋น๊ต๋ก ๋ณํํ๋ฉด B-tree ์ธ๋ฑ์ค์ ์ ๋ ฌ ์์๋ฅผ ๊ทธ๋๋ก ํ์ฉํ ์ ์์ต๋๋ค.
๋ ๋ค๋ฅธ ์ํํ ์ํฐํจํด์ NOT IN๊ณผ NULL ๊ฐ์ ์กฐํฉ์ ๋๋ค. ์๋ธ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์ NULL์ด ํ๋๋ผ๋ ํฌํจ๋๋ฉด NOT IN ์กฐ๊ฑด์ ํญ์ ๋น ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค. SQL์ 3๊ฐ ๋ ผ๋ฆฌ(TRUE, FALSE, UNKNOWN) ๋๋ฌธ์ NULL๊ณผ์ ๋น๊ต๋ UNKNOWN์ ๋ฐํํ๊ณ , NOT IN์ ๋ชจ๋ ๋น๊ต๊ฐ TRUE์ผ ๋๋ง ํ์ ํฌํจ์ํค๊ธฐ ๋๋ฌธ์ ๋๋ค.
-- not_in_null_trap.sql
-- BAD: returns empty if any customer_id is NULL in subquery
SELECT * FROM customers
WHERE customer_id NOT IN (
SELECT customer_id FROM orders
);
-- GOOD: NOT EXISTS handles NULLs correctly
SELECT * FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id
);NOT EXISTS๋ NULL ๊ฐ์ ์ํฅ์ ๋ฐ์ง ์์ผ๋ฉฐ, ์ตํฐ๋ง์ด์ ๊ฐ anti-join์ผ๋ก ํจ์จ์ ์ผ๋ก ๋ณํํ ์ ์์ต๋๋ค. ๋ฉด์ ์์ NOT IN์ NULL ํจ์ ์ ์๋ฐ์ ์ผ๋ก ์ธ๊ธํ๋ฉด SQL์ ๊ทผ๋ณธ์ ์ธ ๋์ ์๋ฆฌ๋ฅผ ๊น์ด ์ดํดํ๊ณ ์์์ ๋ณด์ฌ์ค ์ ์์ต๋๋ค.
๊ณ ๊ธ SQL ๊ฐ๋ ์ ํ์ตํ๋ ๊ฒ๋ง์ผ๋ก๋ ์ถฉ๋ถํ์ง ์์ต๋๋ค. ์ค์ ๋ฐ์ดํฐ์ ์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ณ , EXPLAIN ์ถ๋ ฅ์ ๋ถ์ํ๋ฉฐ, ๋ค์ํ ์ธ๋ฑ์ฑ ์ ๋ต์ ์คํํด ๋ณด๋ ๊ณผ์ ์ด ๋ฉด์ ํฉ๊ฒฉ์ ์ด์ ์ ๋๋ค. ๋ฐ๋ณต์ ์ธ ์ค์ต์ ํตํด ํจํด์ ์ฒดํํ๋ฉด ๋ฉด์ ์ํฉ์์๋ ์์ฐ์ค๋ฝ๊ฒ ์ต์ ์ ์ ๊ทผ๋ฒ์ ์ ์ํ ์ ์์ต๋๋ค.
Data Analytics ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
๊ฒฐ๋ก
๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์์ ๊ณ ๊ธ SQL ์ญ๋์ ๊ธฐ์ ์ ์ฐจ๋ณํ์ ํต์ฌ ์์์ ๋๋ค. ์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ์ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ์๊ด ์๋ธ์ฟผ๋ฆฌ vs ์ผ๋ฐ ์๋ธ์ฟผ๋ฆฌ: ์คํ ๋ฐฉ์์ ์ฐจ์ด๋ฅผ ์ดํดํ๊ณ , ์๊ด ์๋ธ์ฟผ๋ฆฌ์ ์ฑ๋ฅ ํ๊ณ๋ฅผ CTE๋ ์๋์ฐ ํจ์๋ก ๊ทน๋ณตํ๋ ๋ฐฉ๋ฒ์ ์ ์ํ ์ ์์ด์ผ ํฉ๋๋ค
- EXISTS vs IN: ๋๊ท๋ชจ ๋ฐ์ดํฐ์ ์์ EXISTS๊ฐ ์ ํธ๋๋ ์ด์ ์ semi-join ์ต์ ํ ์๋ฆฌ๋ฅผ ์ค๋ช ํ ์ ์์ด์ผ ํฉ๋๋ค
- ์กฐ๊ฑด๋ถ ์ง๊ณ ํผ๋ฒ: CASE WHEN + ์ง๊ณ ํจ์ ํจํด์ DBMS์ ๊ด๊ณ์์ด ๋์ํ๋ ๋ฒ์ฉ ํผ๋ฒ ๊ธฐ๋ฒ์ ๋๋ค
- CROSSTAB: PostgreSQL ํ๊ฒฝ์์ ๋ ์ฒด๊ณ์ ์ธ ํผ๋ฒ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ผ๋ก, DBMS ํนํ ์ง์์ ๋ณด์ฌ์ค ์ ์์ต๋๋ค
- EXPLAIN ANALYZE: ์คํ ๊ณํ์ ์ฝ๊ณ Seq Scan, Nested Loop, ๋ฒํผ ์ฌ์ฉ๋ ๋ฑ ํต์ฌ ์งํ๋ฅผ ํด์ํ๋ ๋ฅ๋ ฅ์ ์๋์ด ๋ ๋ฒจ์ ํ์ ์ญ๋์ ๋๋ค
- ์ธ๋ฑ์ฑ ์ ๋ต: ๋ณตํฉ ์ธ๋ฑ์ค์ ์ด ์์, ์ปค๋ฒ๋ง ์ธ๋ฑ์ค, ๋ถ๋ถ ์ธ๋ฑ์ค์ ํ์ฉ ์๋๋ฆฌ์ค๋ฅผ ๊ตฌ์ฒด์ ์ผ๋ก ์ค๋ช ํ ์ ์์ด์ผ ํฉ๋๋ค
- ์ํฐํจํด ํํผ: ์ธ๋ฑ์ค ์ด์ ํจ์ ์ ์ฉ ๊ธ์ง, NOT IN์ NULL ํจ์ ๋ฑ ์ค๋ฌด์์ ํํ ๋ฐ์ํ๋ ํจ์ ์ ์ธ์ํ๊ณ ์ฌ๋ฐ๋ฅธ ๋์์ ์ ์ํ ์ ์์ด์ผ ํฉ๋๋ค
์ด๋ฌํ ์ฃผ์ ๋ค์ ๊น์ด ์๊ฒ ์ค๋นํ๋ฉด, ๋ฉด์ ์์ ๋จ์ํ ์ฟผ๋ฆฌ ์์ฑ์๊ฐ ์๋ ๋ฐ์ดํฐ ์์ง๋์ด๋ง ๊ฐ๊ฐ์ ๊ฐ์ถ ๋ถ์๊ฐ๋ก ํ๊ฐ๋ฐ์ ์ ์์ต๋๋ค.
์ฐ์ต์ ์์ํ์ธ์!
๋ฉด์ ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ์ ํ ์คํธ๋ก ์ง์์ ํ ์คํธํ์ธ์.
ํ๊ทธ
๊ณต์
๊ด๋ จ ๊ธฐ์ฌ

2026๋ ๋ฐ์ดํฐ ๋ถ์๊ฐ๋ฅผ ์ํ dbt ์๋ฒฝ ๊ฐ์ด๋: ๋ชจ๋ธ๋ง, ํ ์คํธ, ๋ฉด์ ์ง๋ฌธ
dbt ํ๋ก์ ํธ ๊ตฌ์กฐ, ๋จธํฐ๋ฆฌ์ผ๋ผ์ด์ ์ด์ ์ ๋ต, ๋ฐ์ดํฐ ํ์ง ํ ์คํธ, Jinja ๋งคํฌ๋ก, ๊ทธ๋ฆฌ๊ณ ์ค๋ฌด ๋ฉด์ ์์ ์์ฃผ ๋ฑ์ฅํ๋ ์ง๋ฌธ๊น์ง ํฌ๊ด์ ์ผ๋ก ๋ค๋ฃน๋๋ค.

๋ฐ์ดํฐ ๋ถ์๊ฐ๋ฅผ ์ํ SQL: ์๋์ฐ ํจ์, CTE, ๊ณ ๊ธ ์ฟผ๋ฆฌ ๊ธฐ๋ฒ
SQL ์๋์ฐ ํจ์, CTE(๊ณตํต ํ ์ด๋ธ ์), ๊ณ ๊ธ ๋ถ์ ์ฟผ๋ฆฌ๋ฅผ ์ค์ฉ์ ์ธ ์ฝ๋ ์์ ์ ํจ๊ป ์ค๋ช ํฉ๋๋ค. ๋ฐ์ดํฐ ๋ถ์๊ฐ ๋ฉด์ ์ค๋น์ ์ค๋ฌด์ ํ์์ ์ธ ๊ธฐ๋ฒ์ ๋๋ค.

2026๋ ๋ฐ์ดํฐ ์ ๋๋ฆฌํฑ์ค ๋ฉด์ ์ง๋ฌธ TOP 25
2026๋ ๋ฐ์ดํฐ ์ ๋๋ฆฌํฑ์ค ๋ฉด์ ๋๋น ๊ฐ์ด๋์ ๋๋ค. SQL, Python, Power BI, ํต๊ณ, ํ๋ ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ 25๊ฐ ์ง๋ฌธ์ ์ฝ๋ ์์์ ํจ๊ป ์์ธํ ํด์คํฉ๋๋ค.