재고 관리
사용 테이블
products — 상품 (이름, 가격, 재고, 브랜드)
categories — 카테고리 (부모-자식 계층)
suppliers — 공급업체 (업체명, 연락처)
inventory_transactions — 재고 입출고 (유형, 수량)
order_items — 주문 상세 (수량, 단가)
orders — 주문 (상태, 금액, 일시)
학습 범위
Conditional Aggregation, Window Functions, ABC Analysis, Pareto, CTE
1. 현재 재고 현황과 재고 부족 상품
물류팀이 현재 재고가 부족한 상품 목록을 요청했습니다. 재고 수량이 10개 이하인 활성 상품을 찾아, 상품명, 카테고리, 현재 재고, 가격, 공급업체를 표시하세요. 재고가 0인 상품은 "품절"로 표시합니다.
힌트 1: - products.stock_qty로 현재 재고 확인
- products -> categories, products -> suppliers JOIN
- CASE로 "품절" 표시
- products.is_active = 1로 활성 상품만
정답
SELECT
p.name AS product_name,
cat.name AS category,
s.company_name AS supplier,
p.stock_qty,
CASE
WHEN p.stock_qty = 0 THEN '품절'
WHEN p.stock_qty <= 5 THEN '긴급'
ELSE '부족'
END AS stock_status,
p.price
FROM products AS p
INNER JOIN categories AS cat ON p.category_id = cat.id
INNER JOIN suppliers AS s ON p.supplier_id = s.id
WHERE p.is_active = 1
AND p.stock_qty <= 10
ORDER BY p.stock_qty ASC, p.price DESC;
실행 결과 (3행)
| product_name | category | supplier | stock_qty | stock_status | price |
|---|---|---|---|---|---|
| Arctic Freezer 36 A-RGB 화이트 | 공랭 | 아틱코리아 | 0 | 품절 | 23,000.00 |
| 삼성 SPA-KFG0BUB | 멤브레인 | 삼성전자 공식 유통 | 4 | 긴급 | 30,700.00 |
| 로지텍 G502 HERO 실버 | 유선 | 로지텍코리아 | 8 | 부족 | 71,100.00 |
2. 재고 입출고 흐름 분석
지난 1년(2025년)간 상품별 입고량과 출고량을 비교하여 재고 흐름을 파악하세요. 순변동이 마이너스인 상품만 보여주세요.
힌트 1: - inventory_transactions의 type 칼럼: 'inbound' (입고), 'outbound' (출고)
- 조건부 집계: SUM(CASE WHEN type='inbound' THEN quantity ELSE 0 END)
- products와 JOIN하여 현재 재고 포함
정답
SELECT
p.name AS product_name,
p.stock_qty AS current_stock,
SUM(CASE WHEN it.type = 'inbound' THEN it.quantity ELSE 0 END) AS total_in,
SUM(CASE WHEN it.type = 'outbound' THEN it.quantity ELSE 0 END) AS total_out,
SUM(CASE WHEN it.type = 'inbound' THEN it.quantity ELSE 0 END)
- SUM(CASE WHEN it.type = 'outbound' THEN it.quantity ELSE 0 END) AS net_change
FROM inventory_transactions AS it
INNER JOIN products AS p ON it.product_id = p.id
WHERE it.created_at LIKE '2025%'
GROUP BY p.id, p.name, p.stock_qty
HAVING net_change < 0
ORDER BY net_change ASC;
3. 상품 ABC 분석 (파레토 80/20)
매출 기여도에 따라 상품을 A/B/C 등급으로 분류하세요. 전체 매출의 80%를 차지하는 상품을 A등급, 다음 15%를 B등급, 나머지를 C등급으로 분류합니다.
힌트 1: - 상품별 매출을 계산한 후 내림차순 정렬
- 누적 비율 계산에 SUM() OVER (ORDER BY ...)
- 전체 매출 대비 누적 비율로 A/B/C 분류
정답
WITH product_revenue AS (
SELECT
p.id,
p.name,
cat.name AS category,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items AS oi
INNER JOIN orders AS o ON oi.order_id = o.id
INNER JOIN products AS p ON oi.product_id = p.id
INNER JOIN categories AS cat ON p.category_id = cat.id
WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY p.id, p.name, cat.name
),
ranked AS (
SELECT *,
SUM(revenue) OVER () AS total_revenue,
SUM(revenue) OVER (ORDER BY revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue
FROM product_revenue
)
SELECT
name AS product_name,
category,
revenue,
ROUND(100.0 * revenue / total_revenue, 2) AS pct_of_total,
ROUND(100.0 * cumulative_revenue / total_revenue, 2) AS cumulative_pct,
CASE
WHEN 100.0 * cumulative_revenue / total_revenue <= 80 THEN 'A'
WHEN 100.0 * cumulative_revenue / total_revenue <= 95 THEN 'B'
ELSE 'C'
END AS abc_class
FROM ranked
ORDER BY revenue DESC
LIMIT 30;
실행 결과 (총 30행 중 상위 7행)
| product_name | category | revenue | pct_of_total | cumulative_pct | abc_class |
|---|---|---|---|---|---|
| Razer Blade 18 블랙 | 게이밍 노트북 | 1,079,568,800.00 | 3.08 | 3.08 | A |
| Razer Blade 16 실버 | 게이밍 노트북 | 859,072,800.00 | 2.45 | 5.52 | A |
| ASUS Dual RTX 4060 Ti 블랙 | NVIDIA | 858,610,800.00 | 2.45 | 7.97 | A |
| Razer Blade 18 블랙 | 게이밍 노트북 | 839,487,500.00 | 2.39 | 10.36 | A |
| Razer Blade 18 화이트 | 게이밍 노트북 | 665,604,800.00 | 1.90 | 12.26 | A |
| MSI GeForce RTX 4070 Ti Super GAMING X | NVIDIA | 647,024,000.00 | 1.84 | 14.11 | A |
| MSI Radeon RX 7900 XTX GAMING X 화이트 | AMD | 585,793,600.00 | 1.67 | 15.77 | A |
4. 공급업체 성과 평가
각 공급업체의 공급 상품 수, 총 매출, 반품률, 평균 고객 평점을 산출하세요. 반품률이 높은 공급업체를 식별합니다.
힌트 1: - suppliers -> products -> order_items -> orders 순으로 JOIN
- 반품률: 반품 수 / 전체 판매 수
- reviews는 LEFT JOIN으로 평점 포함
정답
WITH supplier_sales AS (
SELECT
s.id AS supplier_id,
s.company_name AS supplier_name,
COUNT(DISTINCT p.id) AS product_count,
SUM(oi.quantity) AS units_sold,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS total_revenue
FROM suppliers AS s
INNER JOIN products AS p ON s.id = p.supplier_id
INNER JOIN order_items AS oi ON p.id = oi.product_id
INNER JOIN orders AS o ON oi.order_id = o.id
WHERE o.status NOT IN ('cancelled')
GROUP BY s.id, s.company_name
),
supplier_returns AS (
SELECT
s.id AS supplier_id,
COUNT(ret.id) AS return_count
FROM suppliers AS s
INNER JOIN products AS p ON s.id = p.supplier_id
INNER JOIN order_items AS oi ON p.id = oi.product_id
INNER JOIN orders AS o2 ON oi.order_id = o2.id
INNER JOIN returns AS ret ON ret.order_id = o2.id
GROUP BY s.id
),
supplier_reviews AS (
SELECT
s.id AS supplier_id,
ROUND(AVG(r.rating), 2) AS avg_rating,
COUNT(r.id) AS review_count
FROM suppliers AS s
INNER JOIN products AS p ON s.id = p.supplier_id
INNER JOIN reviews AS r ON p.id = r.product_id
GROUP BY s.id
)
SELECT
ss.supplier_name,
ss.product_count,
ss.units_sold,
ss.total_revenue,
COALESCE(sr.return_count, 0) AS return_count,
ROUND(100.0 * COALESCE(sr.return_count, 0) / ss.units_sold, 2) AS return_rate_pct,
COALESCE(srev.avg_rating, 0) AS avg_rating,
srev.review_count
FROM supplier_sales AS ss
LEFT JOIN supplier_returns AS sr ON ss.supplier_id = sr.supplier_id
LEFT JOIN supplier_reviews AS srev ON ss.supplier_id = srev.supplier_id
ORDER BY return_rate_pct DESC;
실행 결과 (총 45행 중 상위 7행)
| supplier_name | product_count | units_sold | total_revenue | return_count | return_rate_pct | avg_rating | review_count |
|---|---|---|---|---|---|---|---|
| 브라더코리아 | 1 | 21 | 4,254,600.00 | 2 | 9.52 | 4.00 | 1 |
| 한성컴퓨터 | 4 | 295 | 311,361,100.00 | 19 | 6.44 | 3.75 | 32 |
| 델코리아 | 3 | 566 | 528,065,100.00 | 27 | 4.77 | 4.21 | 63 |
| 레노버코리아 | 5 | 577 | 996,843,200.00 | 25 | 4.33 | 3.58 | 59 |
| 레이저코리아 | 9 | 2958 | 4,004,408,800.00 | 120 | 4.06 | 3.89 | 272 |
| 캐논코리아 | 5 | 892 | 292,499,200.00 | 36 | 4.04 | 3.94 | 97 |
| 주연테크 | 4 | 255 | 357,712,900.00 | 10 | 3.92 | 4.00 | 25 |
5. 월별 재고 회전율 추이
2024년 월별 재고 회전율을 보여주세요. 재고 회전율 = 해당 월 출고 수량 합계 / 월말 누적 재고.
힌트 1: - 월별 출고량: inventory_transactions에서 type='outbound' 필터
- 윈도우 함수로 누적 재고 계산
정답
WITH monthly_flow AS (
SELECT
SUBSTR(created_at, 1, 7) AS year_month,
SUM(CASE WHEN type = 'outbound' THEN quantity ELSE 0 END) AS total_out,
SUM(CASE WHEN type = 'inbound' THEN quantity ELSE 0 END) AS total_in
FROM inventory_transactions
WHERE created_at LIKE '2024%'
GROUP BY SUBSTR(created_at, 1, 7)
),
with_cumulative AS (
SELECT
year_month,
total_out,
total_in,
SUM(total_in - total_out) OVER (
ORDER BY year_month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_net_stock
FROM monthly_flow
)
SELECT
year_month,
total_in,
total_out,
cumulative_net_stock,
CASE
WHEN cumulative_net_stock > 0
THEN ROUND(1.0 * total_out / cumulative_net_stock, 2)
ELSE NULL
END AS turnover_ratio
FROM with_cumulative
ORDER BY year_month;
실행 결과 (총 12행 중 상위 7행)
| year_month | total_in | total_out | cumulative_net_stock | turnover_ratio |
|---|---|---|---|---|
| 2024-01 | 1996 | -199 | 2195 | -0.09 |
| 2024-02 | 935 | -163 | 3293 | -0.05 |
| 2024-03 | 3500 | -248 | 7041 | -0.04 |
| 2024-04 | 3513 | -122 | 10,676 | -0.01 |
| 2024-05 | 3394 | -253 | 14,323 | -0.02 |
| 2024-06 | 2671 | -171 | 17,165 | -0.01 |
| 2024-07 | 2899 | -138 | 20,202 | -0.01 |
6. 보너스: 카테고리별 ABC 분석과 재고 부족 비율
ABC 분석을 카테고리 단위로 수행하세요. 각 카테고리의 ABC 등급과 함께, 상품 수, 재고 부족(stock_qty <= 10) 상품 비율을 표시합니다.
힌트 1: - 카테고리별 매출과 재고 부족 비율을 한 CTE에서 계산 - 누적 매출로 ABC 등급 분류
정답
WITH category_revenue AS (
SELECT
cat.id AS category_id,
cat.name AS category,
COUNT(DISTINCT p.id) AS product_count,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue,
ROUND(100.0 * SUM(CASE WHEN p.stock_qty <= 10 THEN 1 ELSE 0 END)
/ COUNT(DISTINCT p.id), 1) AS low_stock_pct
FROM categories AS cat
INNER JOIN products AS p ON cat.id = p.category_id
INNER JOIN order_items AS oi ON p.id = oi.product_id
INNER JOIN orders AS o ON oi.order_id = o.id
WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY cat.id, cat.name
),
ranked AS (
SELECT *,
SUM(revenue) OVER () AS total_revenue,
SUM(revenue) OVER (ORDER BY revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative
FROM category_revenue
)
SELECT
category,
product_count,
revenue,
ROUND(100.0 * cumulative / total_revenue, 1) AS cumulative_pct,
CASE
WHEN 100.0 * cumulative / total_revenue <= 80 THEN 'A'
WHEN 100.0 * cumulative / total_revenue <= 95 THEN 'B'
ELSE 'C'
END AS abc_class,
low_stock_pct
FROM ranked
ORDER BY revenue DESC;
실행 결과 (총 40행 중 상위 7행)
| category | product_count | revenue | cumulative_pct | abc_class | low_stock_pct |
|---|---|---|---|---|---|
| 게이밍 노트북 | 9 | 4,684,236,900.00 | 13.30 | A | 0.0 |
| NVIDIA | 7 | 2,695,883,800.00 | 21.00 | A | 0.0 |
| 게이밍 모니터 | 10 | 2,645,570,200.00 | 28.60 | A | 0.0 |
| AMD | 8 | 2,419,754,300.00 | 35.50 | A | 0.0 |
| 일반 노트북 | 10 | 2,324,971,800.00 | 42.10 | A | 0.0 |
| 2in1 | 9 | 1,852,395,500.00 | 47.40 | A | 0.0 |
| Intel 소켓 | 13 | 1,506,501,500.00 | 51.70 | A | 0.0 |