DHDataHọc
Lý thuyếtBài 29/308 phút đọc+10 XP

Phân nhánh với CASE

Bạn sẽ học được

  • Viết biểu thức CASE để phân loại dữ liệu theo điều kiện.
  • Dùng CASE trong SELECT, GROUP BY và ORDER BY.
  • Đếm số dòng theo nhóm tự định nghĩa.
  • Xoay bảng bằng CASE để đưa nhiều nhóm thành nhiều cột.
  • Tránh các lỗi về NULL, kiểu dữ liệu và thứ tự điều kiện.

Khái niệm cốt lõi

CASE là biểu thức điều kiện của SQL — tương đương if/else trong ngôn ngữ lập trình, nhưng dùng được ngay trong câu truy vấn:

code
SELECT id,
       quantity * unit_price AS thanh_tien,
       CASE
         WHEN quantity * unit_price >= 3000000 THEN 'lớn'
         WHEN quantity * unit_price >= 1000000 THEN 'vừa'
         ELSE 'nhỏ'
       END AS muc
FROM order_items
ORDER BY thanh_tien DESC
LIMIT 5;

Kết quả năm dòng đầu lần lượt là 3.380.000 → lớn; 2.500.000, 1.780.000, 1.780.000, 1.690.000 → vừa. Chỉ dòng đạt ngưỡng ba triệu mới nhận nhãn lớn; các dòng còn lại rơi vào vừa vì đã qua nhánh đầu tiên mà không khớp.

Cú pháp có hai dạng:

DạngCú phápDùng khi
Tìm kiếm (searched)CASE WHEN dieu_kien THEN gia_tri ... ENDĐiều kiện phức tạp, so sánh nhiều cột
Đơn giản (simple)CASE cot WHEN gia_tri THEN ... ENDSo sánh một cột với các hằng số

Dạng đơn giản:

code
SELECT status,
       CASE status
         WHEN 'completed' THEN 'Hoàn tất'
         WHEN 'pending' THEN 'Đang chờ'
         WHEN 'cancelled' THEN 'Đã huỷ'
       END AS trang_thai
FROM orders
LIMIT 3;

Hai dạng cho kết quả giống nhau khi điều kiện là so sánh bằng với hằng số. Dạng tìm kiếm linh hoạt hơn và là dạng bạn nên dùng mặc định — nó đọc rõ ý hơn khi điều kiện có >=, IS NULL, hoặc nhiều cột.

Ba quy tắc bắt buộc:

  1. CASE trả về một giá trị, không phải một câu lệnh. Nó nằm trong SELECT, WHERE, ORDER BY, GROUP BY, hoặc trong một hàm tổng hợp.
  2. Các nhánh (THEN) phải cùng kiểu dữ liệu hoặc ép được về cùng kiểu. Trộn số và chuỗi có thể báo lỗi hoặc cho kết quả khó hiểu.
  3. Không có ELSE và không nhánh nào khớp thì kết quả là NULL. Luôn cân nhắc thêm ELSE — kể cả ELSE 'khác' — để dữ liệu mới không âm thầm thành NULL.

CASE đánh giá các nhánh theo thứ tự và dừng ở nhánh đầu tiên đúng. Vì vậy thứ tự điều kiện quan trọng:

code
CASE
  WHEN price >= 1000000 THEN 'cao'
  WHEN price >= 300000 THEN 'trung bình'
  ELSE 'thấp'
END

Nếu đảo hai nhánh đầu, sản phẩm 1.250.000 sẽ rơi vào nhánh >= 300000 và bị xếp trung bình — sai. Hãy sắp điều kiện từ hẹp đến rộng, hoặc từ ngưỡng cao xuống thấp khi các ngưỡng chồng nhau.

CASE kết hợp hàm tổng hợp cho phép đếm theo điều kiện trong một dòng:

code
SELECT COUNT(*) FILTER (WHERE status = 'completed') AS hoan_tat,
       COUNT(*) FILTER (WHERE status = 'pending') AS cho,
       COUNT(*) FILTER (WHERE status = 'cancelled') AS huy
FROM orders;

FILTER là cú pháp PostgreSQL gọn hơn SUM(CASE WHEN ... THEN 1 ELSE 0 END). Cả hai cho cùng kết quả; FILTER dễ đọc hơn, còn CASE chạy được trên mọi hệ quản trị. Trong khóa học dùng PostgreSQL nên FILTER là lựa chọn tự nhiên.

CASE trong GROUP BY cho phép gom theo nhóm tự định nghĩa:

code
SELECT CASE
         WHEN price >= 1000000 THEN 'cao'
         WHEN price >= 300000 THEN 'trung bình'
         ELSE 'thấp'
       END AS nhom_gia,
       COUNT(*) AS so_san_pham
FROM products
GROUP BY 1
ORDER BY 1;

GROUP BY 1 gom theo cột thứ nhất trong SELECT. Cách này tiện nhưng khó đọc khi câu lệnh dài; có thể lặp lại biểu thức CASE trong GROUP BY để rõ ràng hơn.

CASE trong ORDER BY tạo thứ tự tùy chỉnh — rất hữu ích khi muốn sắp theo nghiệp vụ thay vì alphabet:

code
SELECT id, status
FROM orders
ORDER BY CASE status
           WHEN 'pending' THEN 1
           WHEN 'completed' THEN 2
           ELSE 3
         END,
         id;

Đơn pending lên đầu, rồi completed, cuối là cancelled. Đây là cách sắp xếp mà ORDER BY status không làm được.

Ví dụ chạy được

Phân loại đơn hàng theo giá trị. Trước hết cần giá trị từng đơn — gom nhóm trước, rồi phân loại ở lớp ngoài:

code
SELECT id, tong, CASE
         WHEN tong >= 3000000 THEN 'lớn'
         WHEN tong >= 1000000 THEN 'vừa'
         ELSE 'nhỏ'
       END AS muc
FROM (
  SELECT o.id, SUM(oi.quantity * oi.unit_price) AS tong
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  GROUP BY o.id
) t
ORDER BY tong DESC;

Kết quả 16 dòng: hai đơn lớn (đơn 13 với 4.030.000 và đơn 7 với 3.960.000); mười một đơn vừa; ba đơn nhỏ (đơn 10 với 960.000, đơn 5 với 640.000, đơn 14 với 580.000). Cách làm này minh họa một nguyên tắc: CASE không thấy được kết quả SUM trong cùng cấp SELECT, nên phải gom nhóm ở lớp con rồi phân loại ở lớp ngoài.

Đếm số đơn theo mức cần gom nhóm hai lần. Thử cách ngây thơ trước:

code
SELECT CASE
         WHEN SUM(oi.quantity * oi.unit_price) >= 3000000 THEN 'lớn'
         WHEN SUM(oi.quantity * oi.unit_price) >= 1000000 THEN 'vừa'
         ELSE 'nhỏ'
       END AS muc,
       COUNT(*) AS so_dong_hang
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;

Câu này chạy nhưng trả 16 dòng, mỗi đơn một dòng, và COUNT(*) là số dòng hàng của đơn đó — không phải số đơn theo mức. Muốn đếm đơn, gom lớp trong theo đơn rồi gom lớp ngoài theo mức:

code
SELECT muc, COUNT(*) AS so_don
FROM (
  SELECT o.id,
         CASE
           WHEN SUM(oi.quantity * oi.unit_price) >= 3000000 THEN 'lớn'
           WHEN SUM(oi.quantity * oi.unit_price) >= 1000000 THEN 'vừa'
           ELSE 'nhỏ'
         END AS muc
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  GROUP BY o.id
) t
GROUP BY muc
ORDER BY muc;

Kết quả: lớn 2, nhỏ 3, vừa 11. Đây là mẫu "gom nhóm hai lần" rất phổ biến trong phân tích: lớp trong tính một chỉ số cho từng thực thể, lớp ngoài đếm thực thể theo chỉ số đó.

Xoay trạng thái thành cột:

code
SELECT COUNT(*) FILTER (WHERE status = 'completed') AS hoan_tat,
       COUNT(*) FILTER (WHERE status = 'pending') AS dang_cho,
       COUNT(*) FILTER (WHERE status = 'cancelled') AS da_huy
FROM orders;

Một dòng: 13 | 2 | 1. Thay vì ba dòng dọc, báo cáo có ba cột ngang — dạng bảng hay gặp trên dashboard.

Phân loại khách theo chi tiêu, xử lý cả khách chưa mua:

code
SELECT c.name,
       COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS chi_tieu,
       CASE
         WHEN COALESCE(SUM(oi.quantity * oi.unit_price), 0) >= 4000000 THEN 'VIP'
         WHEN COALESCE(SUM(oi.quantity * oi.unit_price), 0) >= 1000000 THEN 'thường xuyên'
         ELSE 'mới'
       END AS nhom
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
LEFT JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.id, c.name
ORDER BY chi_tieu DESC;

Kết quả: bốn khách VIP (Bình 6.750.000, Giang 5.490.000, Chi 4.270.000, Minh 4.030.000); bốn khách thường xuyên (An 3.530.000, Dũng 2.150.000, Khánh 1.640.000, Linh 1.190.000); bốn khách mới (Em 640.000, Nga 580.000, Inh 960.000, Hà 0). COALESCE xuất hiện cả trong phép tính và trong CASE để khách chưa mua có giá trị 0 thay vì NULL — nếu thiếu nó, Hà Bùi sẽ rơi vào nhánh ELSE với chi_tieu = NULL, hiển thị trống trên báo cáo.

Xếp trạng thái theo thứ tự nghiệp vụ:

code
SELECT id, status
FROM orders
ORDER BY CASE status
           WHEN 'pending' THEN 1
           WHEN 'completed' THEN 2
           WHEN 'cancelled' THEN 3
         END,
         id
LIMIT 5;

Năm dòng đầu là các đơn pending (5, 14), sau đó ba đơn completed đầu tiên theo id. Đây là cách trình bày "việc cần xử lý trước" trên giao diện quản trị.

Thực hành

  1. Viết CASE phân loại sản phẩm theo giá: cao từ 1.000.000, trung bình từ 300.000, còn lại thấp. Đếm số sản phẩm mỗi nhóm: cao 2, trung bình 3, thấp 4.
  2. Xoay bảng đếm số khách theo segment thành ba cột trong một dòng: retail 7, vip 3, wholesale 2.
  3. Phân loại khách theo số đơn: từ 3 đơn là thân thiết, 2 đơn là quay lại, còn lại mới. Với LEFT JOIN, khách 0 đơn vẫn thuộc mới.
  4. Sắp đơn theo thứ tự pending, completed, cancelled, trong mỗi nhóm sắp theo order_date giảm dần.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
Nhóm bị xếp saiĐiều kiện rộng đứng trước điều kiện hẹpSắp từ ngưỡng cao xuống thấp
Kết quả NULL bất ngờThiếu ELSEThêm ELSE rõ ràng
CASE trong WHERE dùng bí danh lỗiBí danh SELECT chưa tồn tạiLặp biểu thức hoặc bọc truy vấn con
Lỗi kiểu dữ liệuCác nhánh trả về kiểu khác nhauÉp kiểu cho đồng nhất
GROUP BY thiếu cộtCASE ở SELECT nhưng không có trong GROUP BYThêm biểu thức hoặc dùng GROUP BY 1

Lỗi đầu tiên là lỗi logic, không phải lỗi cú pháp: câu lệnh chạy, kết quả trông hợp lý, nhưng phân loại sai. Hãy kiểm tra bằng cách đếm tổng các nhóm — chúng phải cộng lại bằng số dòng gốc.

Tóm tắt

  • CASE WHEN ... THEN ... ELSE ... END trả về một giá trị và dùng được ở mọi mệnh đề.
  • Điều kiện được xét theo thứ tự; đặt ngưỡng hẹp trước ngưỡng rộng.
  • Thiếu ELSE thì kết quả không khớp là NULL.
  • COUNT(*) FILTER (WHERE ...) hoặc SUM(CASE ...) để đếm theo điều kiện.
  • Gom nhóm hai tầng khi cần phân loại dựa trên kết quả tổng hợp.

Dùng ← → để chuyển bài