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
CASEtrongSELECT,GROUP BYvà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:
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ạng | Cú pháp | Dù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 ... END | So sánh một cột với các hằng số |
Dạng đơn giản:
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:
CASEtrả về một giá trị, không phải một câu lệnh. Nó nằm trongSELECT,WHERE,ORDER BY,GROUP BY, hoặc trong một hàm tổng hợp.- 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. - Không có
ELSEvà không nhánh nào khớp thì kết quả làNULL. Luôn cân nhắc thêmELSE— kể cảELSE 'khác'— để dữ liệu mới không âm thầm thànhNULL.
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:
CASE
WHEN price >= 1000000 THEN 'cao'
WHEN price >= 300000 THEN 'trung bình'
ELSE 'thấp'
ENDNế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:
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:
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:
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:
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:
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:
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:
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:
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ụ:
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
- Viết
CASEphân loại sản phẩm theo giá:caotừ 1.000.000,trung bìnhtừ 300.000, còn lạithấp. Đếm số sản phẩm mỗi nhóm: cao 2, trung bình 3, thấp 4. - Xoay bảng đếm số khách theo
segmentthành ba cột trong một dòng: retail 7, vip 3, wholesale 2. - 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ạimới. VớiLEFT JOIN, khách 0 đơn vẫn thuộcmới. - Sắp đơn theo thứ tự
pending,completed,cancelled, trong mỗi nhóm sắp theoorder_dategiảm dần.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Nhóm bị xếp sai | Điều kiện rộng đứng trước điều kiện hẹp | Sắp từ ngưỡng cao xuống thấp |
Kết quả NULL bất ngờ | Thiếu ELSE | Thêm ELSE rõ ràng |
CASE trong WHERE dùng bí danh lỗi | Bí danh SELECT chưa tồn tại | Lặp biểu thức hoặc bọc truy vấn con |
| Lỗi kiểu dữ liệu | Các nhánh trả về kiểu khác nhau | Ép kiểu cho đồng nhất |
GROUP BY thiếu cột | CASE ở SELECT nhưng không có trong GROUP BY | Thê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 ... ENDtrả 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
ELSEthì kết quả không khớp làNULL. COUNT(*) FILTER (WHERE ...)hoặcSUM(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.
Use ← → to move between lessons