콘텐츠로 이동

7강: CASE 표현식

데이터를 조회하다 보면 '가격이 100만 원 이상이면 고가, 아니면 일반'처럼 조건에 따라 다른 값을 표시하고 싶을 때가 있습니다. CASE 표현식으로 SQL 안에서 조건 분기를 할 수 있습니다.

이미 알고 계신다면

CASE WHEN, 피벗, 범주화에 익숙하다면 8강: INNER JOIN으로 건너뛰세요.

CASE는 SQL의 조건 표현식으로, 프로그래밍 언어의 if/else와 유사합니다. 값 변환, 레이블 생성, 데이터 구간 분류, 조건부 집계 등을 단일 쿼리 안에서 모두 처리할 수 있습니다.

flowchart TD V["point_balance"] -->|">= 100,000"| A["'Heavy User'"] V -->|">= 10,000"| B["'Regular'"] V -->|"else"| C["'Light'"]

CASE는 SQL의 if-else입니다. 조건을 위에서 아래로 순서대로 검사합니다.

단순 CASE

단순(Simple) CASE는 하나의 칼럼 값을 고정된 값들과 비교합니다.

-- 주문 상태 코드를 읽기 쉬운 레이블로 변환
SELECT
    order_number,
    total_amount,
    CASE status
        WHEN 'pending'          THEN '결제 대기'
        WHEN 'paid'             THEN '결제 완료'
        WHEN 'preparing'        THEN '상품 준비 중'
        WHEN 'shipped'          THEN '배송 중'
        WHEN 'delivered'        THEN '배달 완료'
        WHEN 'confirmed'        THEN '구매 확정'
        WHEN 'cancelled'        THEN '취소됨'
        WHEN 'return_requested' THEN '반품 요청'
        WHEN 'returned'         THEN '반품 완료'
        ELSE status
    END AS status_label
FROM orders
ORDER BY ordered_at DESC
LIMIT 5;

결과:

order_number total_amount status_label
ORD-20251211-413965 409600.0 결제 대기
ORD-20251226-416837 1169700.0 결제 대기
ORD-20251231-417734 2076300.0 결제 대기
ORD-20251231-417696 814400.0 반품 요청
ORD-20251231-417737 550600.0 결제 대기
... ... ...

검색 CASE

검색(Searched) CASE는 독립적인 WHEN 조건을 평가하여 비교 및 표현식에 완전한 유연성을 제공합니다.

-- 상품을 가격대별로 분류
SELECT
    name,
    price,
    CASE
        WHEN price < 50           THEN '저가'
        WHEN price BETWEEN 50 AND 199.99  THEN '중가'
        WHEN price BETWEEN 200 AND 799.99 THEN '고가'
        ELSE '프리미엄'
    END AS price_tier
FROM products
WHERE is_active = 1
ORDER BY price ASC
LIMIT 10;

결과:

name price price_tier
TP-Link TL-SG108 실버 16500.0 프리미엄
TP-Link TG-3468 블랙 19800.0 프리미엄
삼성 무선 키보드 Trio 500 화이트 20300.0 프리미엄
TP-Link TL-SG1016D 화이트 20300.0 프리미엄
로지텍 G502 HERO 실버 20300.0 프리미엄
Razer Cobra 실버 20300.0 프리미엄
TP-Link Archer TX55E 실버 20500.0 프리미엄
로지텍 G402 20500.0 프리미엄
... ... ...

연령대 분류에 CASE 활용

-- 고객을 세대별로 분류
SELECT
    name,
    birth_date,
    CASE
        WHEN birth_date IS NULL THEN '미확인'
        WHEN CAST(SUBSTR(birth_date, 1, 4) AS INTEGER) >= 1997 THEN 'Z세대'
        WHEN CAST(SUBSTR(birth_date, 1, 4) AS INTEGER) >= 1981 THEN '밀레니얼'
        WHEN CAST(SUBSTR(birth_date, 1, 4) AS INTEGER) >= 1965 THEN 'X세대'
        ELSE '베이비붐+'
    END AS generation
FROM customers
LIMIT 8;
SELECT
    name,
    birth_date,
    CASE
        WHEN birth_date IS NULL THEN '미확인'
        WHEN YEAR(birth_date) >= 1997 THEN 'Z세대'
        WHEN YEAR(birth_date) >= 1981 THEN '밀레니얼'
        WHEN YEAR(birth_date) >= 1965 THEN 'X세대'
        ELSE '베이비붐+'
    END AS generation
FROM customers
LIMIT 8;
SELECT
    name,
    birth_date,
    CASE
        WHEN birth_date IS NULL THEN '미확인'
        WHEN EXTRACT(YEAR FROM birth_date) >= 1997 THEN 'Z세대'
        WHEN EXTRACT(YEAR FROM birth_date) >= 1981 THEN '밀레니얼'
        WHEN EXTRACT(YEAR FROM birth_date) >= 1965 THEN 'X세대'
        ELSE '베이비붐+'
    END AS generation
FROM customers
LIMIT 8;

결과:

name birth_date generation
김민수 1989-04-12 밀레니얼
이지은 (NULL) 미확인
박서준 1972-08-27 X세대
최유리 2000-01-15 Z세대
...

GROUP BY 및 집계에서의 CASE

CASE를 그룹화 표현식으로 사용하거나 집계 함수 내에서 활용할 수 있습니다.

graph LR subgraph "원본 (세로 형태)" A["1월 | confirmed | 100"] B["1월 | cancelled | 20"] C["2월 | confirmed | 150"] D["2월 | cancelled | 10"] end subgraph "피벗 (가로 형태)" E["1월 | 확정=100 | 취소=20"] F["2월 | 확정=150 | 취소=10"] end A & B --> E C & D --> F

SUM(CASE WHEN status = 'confirmed' THEN 1 END) 패턴으로 세로 데이터를 가로로 변환합니다.

-- 가격대별 상품 수
SELECT
    CASE
        WHEN price < 50           THEN '저가 (5만원 미만)'
        WHEN price BETWEEN 50 AND 199.99  THEN '중가 (5만~20만원)'
        WHEN price BETWEEN 200 AND 799.99 THEN '고가 (20만~80만원)'
        ELSE '프리미엄 (80만원 이상)'
    END AS price_tier,
    COUNT(*)   AS product_count,
    AVG(price) AS avg_price
FROM products
WHERE is_active = 1
GROUP BY price_tier
ORDER BY avg_price;

결과:

price_tier product_count avg_price
프리미엄 (80만원 이상) 2175 678774.8505747126
-- 피벗: 주문 상태별 건수를 칼럼으로 표시
SELECT
    SUBSTR(ordered_at, 1, 7) AS year_month,
    COUNT(CASE WHEN status = 'confirmed' THEN 1 END) AS confirmed,
    COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled,
    COUNT(CASE WHEN status = 'returned'  THEN 1 END) AS returned,
    COUNT(*) AS total
FROM orders
WHERE ordered_at LIKE '2024%'
GROUP BY SUBSTR(ordered_at, 1, 7)
ORDER BY year_month;
SELECT
    DATE_FORMAT(ordered_at, '%Y-%m') AS year_month,
    COUNT(CASE WHEN status = 'confirmed' THEN 1 END) AS confirmed,
    COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled,
    COUNT(CASE WHEN status = 'returned'  THEN 1 END) AS returned,
    COUNT(*) AS total
FROM orders
WHERE ordered_at >= '2024-01-01'
  AND ordered_at <  '2025-01-01'
GROUP BY DATE_FORMAT(ordered_at, '%Y-%m')
ORDER BY year_month;
SELECT
    TO_CHAR(ordered_at, 'YYYY-MM') AS year_month,
    COUNT(CASE WHEN status = 'confirmed' THEN 1 END) AS confirmed,
    COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled,
    COUNT(CASE WHEN status = 'returned'  THEN 1 END) AS returned,
    COUNT(*) AS total
FROM orders
WHERE ordered_at >= '2024-01-01'
  AND ordered_at <  '2025-01-01'
GROUP BY TO_CHAR(ordered_at, 'YYYY-MM')
ORDER BY year_month;

결과:

year_month confirmed cancelled returned total
2024-01 198 42 12 312
2024-02 183 38 9 289
2024-03 261 57 14 405
...

ORDER BY에서의 CASE

계산된 표현식으로 정렬할 수 있습니다.

-- 활성 상태 우선, 종료 상태 나중에 정렬
SELECT order_number, status, total_amount
FROM orders
ORDER BY
    CASE status
        WHEN 'pending'   THEN 1
        WHEN 'paid'      THEN 2
        WHEN 'preparing' THEN 3
        WHEN 'shipped'   THEN 4
        ELSE 5
    END,
    total_amount DESC
LIMIT 10;

정리

개념 설명 예시

레슨 복습 문제

이 레슨에서 배운 개념을 바로 확인하는 간단한 문제입니다. 여러 개념을 종합하는 실전 연습은 연습 문제 섹션을 참고하세요.

문제 1

주문의 notes 칼럼이 NULL인 경우 '메모 없음'으로 표시하세요. CASE 표현식을 사용하여 order_number, status, memo(notes가 NULL이면 '메모 없음', 아니면 notes 값)를 반환하세요. 최근 주문 15건으로 제한하세요.

정답
SELECT
order_number,
status,
CASE
WHEN notes IS NULL THEN '메모 없음'
ELSE notes
END AS memo
FROM orders
ORDER BY ordered_at DESC
LIMIT 15;

실행 결과 (총 15행 중 상위 7행)

order_number status memo
ORD-20251231-37555 pending 메모 없음
ORD-20251231-37543 pending 층간소음 주의, 살짝 노크해주세요
ORD-20251231-37552 pending 메모 없음
ORD-20251231-37548 pending 메모 없음
ORD-20251231-37542 pending 회사 정문 경비실로 보내주세요
ORD-20251231-37546 pending 경비실에 맡겨주세요
ORD-20251231-37547 pending 파손 주의 부탁드립니다

문제 2

직원(staff) 목록을 정렬하되, role'manager'인 직원이 먼저, 그 다음 'staff', 나머지가 마지막에 오도록 하세요. 같은 역할 내에서는 name 오름차순으로 정렬합니다. name, department, role을 반환하고, 활성 직원만 포함하세요.

정답
SELECT name, department, role
FROM staff
WHERE is_active = 1
ORDER BY
CASE role
WHEN 'manager' THEN 1
WHEN 'staff'   THEN 2
ELSE 3
END,
name ASC;

실행 결과 (5행)

name department role
권영희 마케팅 manager
이준혁 영업 manager
박경수 경영 admin
장주원 경영 admin
한민재 경영 admin

문제 3

결제 수단(payments.method)을 단순 CASE로 한글 레이블로 변환하세요: 'card''신용카드', 'bank_transfer''계좌이체', 'cash''현금', 그 외 → '기타'. id, amount, method_label을 반환하고 10행으로 제한하세요.

정답
SELECT
id,
amount,
CASE method
WHEN 'card'          THEN '신용카드'
WHEN 'bank_transfer' THEN '계좌이체'
WHEN 'cash'          THEN '현금'
ELSE '기타'
END AS method_label
FROM payments
LIMIT 10;

실행 결과 (총 10행 중 상위 7행)

id amount method_label
1 167,000.00 신용카드
2 211,800.00 신용카드
3 704,800.00 신용카드
4 167,000.00 신용카드
5 534,490.00 기타
6 167,000.00 신용카드
7 687,400.00 신용카드

문제 4

상품 목록에 stock_status 칼럼을 추가하세요: stock_qty = 0이면 '품절', 1~10이면 '재고 부족', 11~100이면 '재고 있음', 100 초과면 '재고 충분'. 활성 상품 전체의 name, stock_qty, stock_status를 반환하세요.

정답
SELECT
name,
stock_qty,
CASE
WHEN stock_qty = 0         THEN '품절'
WHEN stock_qty <= 10       THEN '재고 부족'
WHEN stock_qty <= 100      THEN '재고 있음'
ELSE '재고 충분'
END AS stock_status
FROM products
WHERE is_active = 1
ORDER BY stock_qty ASC;

실행 결과 (총 219행 중 상위 7행)

name stock_qty stock_status
Arctic Freezer 36 A-RGB 화이트 0 품절
삼성 SPA-KFG0BUB 4 재고 부족
로지텍 G502 HERO 실버 8 재고 부족
FK 테스트 10 재고 부족
ASUS ROG Strix Scar 16 18 재고 있음
MSI MPG X870E CARBON WIFI 블랙 21 재고 있음
LG 27UQ85R 블랙 26 재고 있음

문제 5

세대별 분포 보고서를 만드세요: 활성 고객이 각 세대(Z세대: 1997년 이후 출생, 밀레니얼: 1981~1996, X세대: 1965~1980, 베이비붐+: 1965년 이전, 미확인: birth_date가 NULL)에 몇 명씩 있는지 집계하세요. generationcustomer_count를 반환하세요.

정답
SELECT
CASE
WHEN birth_date IS NULL THEN '미확인'
WHEN CAST(SUBSTR(birth_date, 1, 4) AS INTEGER) >= 1997 THEN 'Z세대'
WHEN CAST(SUBSTR(birth_date, 1, 4) AS INTEGER) >= 1981 THEN '밀레니얼'
WHEN CAST(SUBSTR(birth_date, 1, 4) AS INTEGER) >= 1965 THEN 'X세대'
ELSE '베이비붐+'
END AS generation,
COUNT(*) AS customer_count
FROM customers
WHERE is_active = 1
GROUP BY generation
ORDER BY customer_count DESC;

실행 결과 (5행)

generation customer_count
밀레니얼 1762
Z세대 776
X세대 561
미확인 497
베이비붐+ 64

문제 6

리뷰의 rating을 텍스트 레이블로 변환하세요: 5 → '최고', 4 → '좋음', 3 → '보통', 2 → '불만', 1 → '최악'. 레이블별 리뷰 수와 평균 평점을 구하세요. rating_label, review_count, avg_ratingavg_rating 내림차순으로 반환하세요.

정답
SELECT
CASE rating
WHEN 5 THEN '최고'
WHEN 4 THEN '좋음'
WHEN 3 THEN '보통'
WHEN 2 THEN '불만'
WHEN 1 THEN '최악'
END AS rating_label,
COUNT(*)            AS review_count,
ROUND(AVG(rating), 2) AS avg_rating
FROM reviews
GROUP BY rating
ORDER BY avg_rating DESC;

실행 결과 (5행)

rating_label review_count avg_rating
최고 3433 5.00
좋음 2575 4.00
보통 1265 3.00
불만 839 2.00
최악 434 1.00

문제 7

고객의 point_balance를 3단계로 분류하세요: 10만 이상 '헤비 유저', 1만 이상 '일반', 그 외 '라이트'. grade별로 각 단계에 해당하는 고객 수를 집계하세요. grade, heavy_count, regular_count, light_count를 반환하세요.

정답
SELECT
grade,
COUNT(CASE WHEN point_balance >= 100000 THEN 1 END) AS heavy_count,
COUNT(CASE WHEN point_balance >= 10000
AND point_balance < 100000 THEN 1 END) AS regular_count,
COUNT(CASE WHEN point_balance < 10000 THEN 1 END)  AS light_count
FROM customers
WHERE is_active = 1
GROUP BY grade
ORDER BY grade;

실행 결과 (4행)

grade heavy_count regular_count light_count
BRONZE 187 597 1505
GOLD 231 293 0
SILVER 130 289 60
VIP 293 75 0

문제 8

주문 금액 구간별(total_amount 기준: 10만 미만 '소액', 10만~50만 미만 '중간', 50만 이상 '고액') 주문 수와 총 매출을 집계하고, 고액 주문이 위에 오도록 정렬하세요. amount_tier, order_count, total_revenue를 반환하세요.

정답
SELECT
CASE
WHEN total_amount < 100      THEN '소액'
WHEN total_amount < 500      THEN '중간'
ELSE '고액'
END AS amount_tier,
COUNT(*)          AS order_count,
SUM(total_amount) AS total_revenue
FROM orders
WHERE status NOT IN ('cancelled', 'returned')
GROUP BY amount_tier
ORDER BY
CASE
WHEN total_amount < 100 THEN 3
WHEN total_amount < 500 THEN 2
ELSE 1
END;

실행 결과 (1행)

amount_tier order_count total_revenue
고액 35,205 35,580,915,707.00

문제 9

결제 수단별 '성공'(status = 'completed')과 '실패'(그 외) 건수를 피벗하세요. method, success_count, fail_count, success_rate(성공률, 소수점 1자리)를 반환하고, 성공률 내림차순으로 정렬하세요.

정답
SELECT
method,
COUNT(CASE WHEN status = 'completed' THEN 1 END) AS success_count,
COUNT(CASE WHEN status != 'completed' THEN 1 END) AS fail_count,
ROUND(
COUNT(CASE WHEN status = 'completed' THEN 1 END) * 100.0
/ COUNT(*),
1
) AS success_rate
FROM payments
GROUP BY method
ORDER BY success_rate DESC;

실행 결과 (6행)

method success_count fail_count success_rate
card 15,556 1285 92.40
naver_pay 5270 445 92.20
bank_transfer 3429 289 92.20
point 1770 151 92.10
kakao_pay 6886 600 92.00
virtual_account 1705 171 90.90

문제 10

orders 테이블에서 주문 상태(status)별로 건수를 세되, CASE를 이용해 한국어 라벨(대기, 결제완료, 배송중, 완료, 취소, 기타)로 분류하세요. 건수가 많은 순서로 정렬합니다.

정답
SELECT
    CASE status
        WHEN 'pending' THEN '대기'
        WHEN 'paid' THEN '결제완료'
        WHEN 'shipped' THEN '배송중'
        WHEN 'delivered' THEN '완료'
        WHEN 'cancelled' THEN '취소'
        ELSE '기타'
    END AS status_label,
    COUNT(*) AS cnt
FROM orders
GROUP BY status_label
ORDER BY cnt DESC;

실행 결과 (6행)

status_label cnt
기타 35,417
취소 1859
완료 125
대기 82
배송중 51
결제완료 23