Biểu thức điều kiện

← Kiểu dữ liệu chuyên sâu | → Lệnh psql

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

Viết logic rẽ nhánh ngay trong SQL bằng CASE, xử lý NULL bằng COALESCE và NULLIF, và chuyển đổi kiểu an toàn bằng CAST.


1. CASE

CASE là câu lệnh if-then-else của SQL. Nó là biểu thức, nên dùng được ở bất cứ đâu chấp nhận một giá trị.

1.1. Dạng tìm kiếm (searched CASE)

Linh hoạt nhất — mỗi nhánh là một điều kiện boolean độc lập:

CASE
   WHEN dieu_kien_1 THEN ket_qua_1
   WHEN dieu_kien_2 THEN ket_qua_2
   [ELSE ket_qua_mac_dinh]
END
SELECT title,
       length,
       CASE
          WHEN length < 50           THEN 'Rất ngắn'
          WHEN length BETWEEN 50 AND 120 THEN 'Trung bình'
          WHEN length > 120          THEN 'Dài'
          ELSE 'Không xác định'
       END AS phan_loai
FROM film
ORDER BY length
LIMIT 8;

CASE dừng ở nhánh đầu tiên đúng

Thứ tự các WHEN rất quan trọng:

-- ❌ Nhánh thứ hai không bao giờ chạy
CASE
   WHEN diem >= 5 THEN 'Đạt'
   WHEN diem >= 8 THEN 'Giỏi'     -- diem = 9 đã khớp nhánh trên rồi
END
 
-- ✅ Từ hẹp tới rộng
CASE
   WHEN diem >= 8 THEN 'Giỏi'
   WHEN diem >= 5 THEN 'Đạt'
   ELSE 'Chưa đạt'
END

Không có ELSE thì trả về NULL

SELECT CASE WHEN false THEN 'a' END;   -- NULL

Luôn viết ELSE trừ khi bạn thực sự muốn NULL.

1.2. Dạng đơn giản (simple CASE)

So sánh một biểu thức với các giá trị:

CASE bieu_thuc
   WHEN gia_tri_1 THEN ket_qua_1
   WHEN gia_tri_2 THEN ket_qua_2
   [ELSE ket_qua_mac_dinh]
END
SELECT title,
       rating,
       CASE rating
          WHEN 'G'     THEN 'Mọi lứa tuổi'
          WHEN 'PG'    THEN 'Cần hướng dẫn của phụ huynh'
          WHEN 'PG-13' THEN 'Không phù hợp dưới 13 tuổi'
          WHEN 'R'     THEN 'Dưới 17 cần người lớn đi kèm'
          WHEN 'NC-17' THEN 'Chỉ từ 18 tuổi'
          ELSE 'Chưa phân loại'
       END AS y_nghia
FROM film
LIMIT 5;

Dạng đơn giản không so sánh được với NULL

CASE cot WHEN NULL THEN 'rỗng' ELSE 'có' END
-- Luôn trả về 'có', kể cả khi cot IS NULL

Vì nó dùng phép =, mà NULL = NULL cho ra NULL. Phải dùng dạng tìm kiếm:

CASE WHEN cot IS NULL THEN 'rỗng' ELSE 'có' END

1.3. CASE trong hàm tổng hợp — bảng chéo (pivot)

SELECT
   count(*) AS tong,
   count(CASE WHEN rating = 'G'     THEN 1 END) AS g,
   count(CASE WHEN rating = 'PG'    THEN 1 END) AS pg,
   count(CASE WHEN rating = 'PG-13' THEN 1 END) AS pg13,
   count(CASE WHEN rating = 'R'     THEN 1 END) AS r,
   count(CASE WHEN rating = 'NC-17' THEN 1 END) AS nc17
FROM film;
 tong |  g  | pg  | pg13 |  r  | nc17
------+-----+-----+------+-----+------
 1000 | 178 | 194 |  223 | 195 |  210

Cách viết hiện đại hơn với FILTER:

SELECT
   count(*) AS tong,
   count(*) FILTER (WHERE rating = 'G')     AS g,
   count(*) FILTER (WHERE rating = 'PG')    AS pg,
   count(*) FILTER (WHERE rating = 'PG-13') AS pg13,
   count(*) FILTER (WHERE rating = 'R')     AS r,
   count(*) FILTER (WHERE rating = 'NC-17') AS nc17
FROM film;

Cùng kết quả, ngắn gọn và rõ ràng hơn. → Nhóm dữ liệu - GROUP BY, HAVING, hàm tổng hợp

Bảng chéo doanh thu theo tháng:

SELECT
   staff_id,
   round(sum(amount) FILTER (WHERE EXTRACT(MONTH FROM payment_date) = 2), 2) AS thang_2,
   round(sum(amount) FILTER (WHERE EXTRACT(MONTH FROM payment_date) = 3), 2) AS thang_3,
   round(sum(amount) FILTER (WHERE EXTRACT(MONTH FROM payment_date) = 4), 2) AS thang_4,
   round(sum(amount), 2) AS tong
FROM payment
GROUP BY staff_id
ORDER BY staff_id;

sum vs count với CASE

count(CASE ... THEN 1 END) đếm đúng vì count bỏ qua NULL.
sum(CASE WHEN ... THEN 1 ELSE 0 END) cũng đúng.
Nhưng count(CASE WHEN ... THEN 1 ELSE 0 END) sai — nó đếm cả các giá trị 0.

1.4. CASE trong ORDER BY — sắp xếp tùy chỉnh

-- Sắp theo mức độ ưu tiên nghiệp vụ, không phải bảng chữ cái
SELECT title, rating
FROM film
ORDER BY CASE rating
            WHEN 'G'     THEN 1
            WHEN 'PG'    THEN 2
            WHEN 'PG-13' THEN 3
            WHEN 'R'     THEN 4
            WHEN 'NC-17' THEN 5
         END,
         title;

Mẹo đưa một nhóm lên đầu:

ORDER BY CASE WHEN trang_thai = 'khan_cap' THEN 0 ELSE 1 END,
         ngay_tao DESC;

1.5. CASE trong UPDATE

UPDATE film
SET rental_rate = CASE
       WHEN length > 150 THEN 4.99
       WHEN length >  90 THEN 2.99
       ELSE 0.99
    END
WHERE rating = 'PG';

1.6. CASE trong GROUP BY

SELECT
   CASE
      WHEN amount <  2 THEN 'Nhỏ'
      WHEN amount <  5 THEN 'Vừa'
      WHEN amount < 10 THEN 'Lớn'
      ELSE 'Rất lớn'
   END AS phan_khuc,
   count(*) AS so_giao_dich,
   round(sum(amount), 2) AS tong_tien
FROM payment
GROUP BY phan_khuc
ORDER BY tong_tien DESC;

PostgreSQL cho phép dùng bí danh trong GROUP BY — tiện hơn so với phải lặp lại toàn bộ CASE.

1.7. Mọi nhánh phải cùng kiểu

-- ❌ LỖI
SELECT CASE WHEN true THEN 1 ELSE 'không' END;
-- ERROR: invalid input syntax for type integer: "không"
 
-- ✅ Ép về cùng kiểu
SELECT CASE WHEN true THEN 1::text ELSE 'không' END;

2. COALESCE

Trả về đối số không NULL đầu tiên.

COALESCE(gia_tri_1, gia_tri_2, ..., gia_tri_n)
SELECT COALESCE(NULL, NULL, 'xin chào', 'tạm biệt');   -- 'xin chào'
SELECT COALESCE(NULL, NULL);                            -- NULL

2.1. Ứng dụng phổ biến

-- Thay NULL bằng giá trị mặc định khi hiển thị
SELECT customer_id,
       COALESCE(return_date::text, 'Chưa trả') AS trang_thai
FROM rental
LIMIT 5;
 
-- Tránh NULL trong phép tính
SELECT COALESCE(sum(amount), 0) AS doanh_thu
FROM payment
WHERE customer_id = 99999;    -- không có dòng nào → sum trả NULL → COALESCE cho 0
 
-- Chọn giá trị theo thứ tự ưu tiên
SELECT COALESCE(ten_hien_thi, biet_danh, ho_ten, 'Ẩn danh') AS ten
FROM nguoi_dung;
 
-- Nối chuỗi an toàn
SELECT COALESCE(ho, '') || ' ' || COALESCE(ten, '') FROM nguoi_dung;
-- Hoặc gọn hơn:
SELECT concat_ws(' ', ho, ten) FROM nguoi_dung;

2.2. Ví dụ tự chứa

DROP TABLE IF EXISTS items;
 
CREATE TABLE items (
   id SERIAL PRIMARY KEY,
   product VARCHAR(100) NOT NULL,
   price NUMERIC NOT NULL,
   discount NUMERIC
);
 
INSERT INTO items (product, price, discount)
VALUES ('A', 1000, 10),
       ('B', 1500, 20),
       ('C',  800, 5),
       ('D',  500, NULL);
-- ❌ SAI: sản phẩm D cho ra NULL
SELECT product, (price - discount) AS gia_cuoi FROM items;
 product | gia_cuoi
---------+----------
 A       |      990
 B       |     1480
 C       |      795
 D       |   [NULL]
-- ✅ ĐÚNG
SELECT product, (price - COALESCE(discount, 0)) AS gia_cuoi FROM items;
 product | gia_cuoi
---------+----------
 A       |      990
 B       |     1480
 C       |      795
 D       |      500

2.3. COALESCE đánh giá lười

COALESCE dừng ngay khi gặp giá trị không NULL — các đối số sau không được tính:

SELECT COALESCE(1, 1/0);   -- 1, không lỗi chia cho 0

Hữu ích khi đối số sau là subquery đắt đỏ.

2.4. COALESCE không thay được chuỗi rỗng

SELECT COALESCE('', 'mặc định');   -- ''  (chuỗi rỗng KHÔNG phải NULL)

Kết hợp với NULLIF:

SELECT COALESCE(NULLIF(ten, ''), 'Ẩn danh') FROM nguoi_dung;

3. NULLIF

NULLIF(gia_tri_1, gia_tri_2)
  • Trả về NULL nếu hai giá trị bằng nhau.
  • Ngược lại trả về gia_tri_1.
SELECT NULLIF(1, 1);          -- NULL
SELECT NULLIF(1, 0);          -- 1
SELECT NULLIF('a', 'b');      -- 'a'
SELECT NULLIF('', 'x');       -- ''

3.1. Chống chia cho 0 — ứng dụng kinh điển

-- ❌ ERROR: division by zero
SELECT tu_so / mau_so FROM t;
 
-- ✅ Trả về NULL thay vì lỗi
SELECT tu_so / NULLIF(mau_so, 0) FROM t;
 
-- ✅ Trả về 0 thay vì NULL
SELECT COALESCE(tu_so / NULLIF(mau_so, 0), 0) FROM t;

Ví dụ thực tế — tính tỷ lệ:

SELECT c.name AS the_loai,
       count(*) AS so_phim,
       round(100.0 * count(*) FILTER (WHERE f.length > 120)
             / NULLIF(count(*), 0), 1) AS phan_tram_phim_dai
FROM category c
JOIN film_category fc ON fc.category_id = c.category_id
JOIN film f           ON f.film_id = fc.film_id
GROUP BY c.name
ORDER BY phan_tram_phim_dai DESC;

3.2. Chuỗi rỗng thành NULL

Cực kỳ hữu ích khi nhập dữ liệu từ CSV:

INSERT INTO nhan_vien (ho_ten, luong, ngay_vao)
SELECT trim(ho_ten),
       NULLIF(trim(luong), '')::numeric,
       NULLIF(trim(ngay_vao), '')::date
FROM staging_raw;

Không có NULLIF, chuỗi rỗng ''::numeric sẽ báo lỗi.

→ Nhập và xuất dữ liệu CSV

3.3. Ví dụ tự chứa

DROP TABLE IF EXISTS posts;
 
CREATE TABLE posts (
   id SERIAL PRIMARY KEY,
   title VARCHAR(255) NOT NULL,
   excerpt VARCHAR(150),
   body TEXT NOT NULL
);
 
INSERT INTO posts (title, excerpt, body)
VALUES ('Bài 1', 'Tóm tắt bài 1', 'Nội dung bài 1'),
       ('Bài 2', NULL,            'Nội dung bài 2'),
       ('Bài 3', '',              'Nội dung bài 3');
-- ❌ COALESCE không xử lý được chuỗi rỗng của Bài 3
SELECT id, title, COALESCE(excerpt, left(body, 40)) AS tom_tat FROM posts;
 id | title |    tom_tat
----+-------+----------------
  1 | Bài 1 | Tóm tắt bài 1
  2 | Bài 2 | Nội dung bài 2
  3 | Bài 3 |                  ← rỗng!
-- ✅ Kết hợp NULLIF + COALESCE
SELECT id, title,
       COALESCE(NULLIF(excerpt, ''), left(body, 40)) AS tom_tat
FROM posts;
 id | title |    tom_tat
----+-------+----------------
  1 | Bài 1 | Tóm tắt bài 1
  2 | Bài 2 | Nội dung bài 2
  3 | Bài 3 | Nội dung bài 3

COALESCE(NULLIF(x, ''), mac_dinh) là mẫu chuẩn

Ghi nhớ mẫu này — bạn sẽ dùng nó rất nhiều khi xử lý dữ liệu nhập từ form hoặc file.


4. GREATEST và LEAST

Chọn giá trị lớn nhất / nhỏ nhất trong danh sách (theo chiều ngang, khác max()/min() theo chiều dọc):

SELECT GREATEST(1, 5, 3);       -- 5
SELECT LEAST(1, 5, 3);          -- 1
SELECT GREATEST(1, NULL, 3);    -- 3  (bỏ qua NULL)
-- Giới hạn giá trị trong khoảng [0, 100]
SELECT LEAST(GREATEST(diem, 0), 100) AS diem_hop_le FROM ket_qua;
 
-- Lấy thời điểm cập nhật gần nhất trong nhiều cột
SELECT GREATEST(sua_luc, tao_luc, xoa_luc) AS thay_doi_cuoi FROM t;

5. CAST — chuyển đổi kiểu

Hai cú pháp tương đương:

CAST (bieu_thuc AS kieu_du_lieu)   -- chuẩn SQL
bieu_thuc::kieu_du_lieu             -- riêng PostgreSQL, ngắn gọn

5.1. Ví dụ

SELECT CAST('100' AS INTEGER);              -- 100
SELECT '100'::INTEGER;                       -- 100
SELECT '2026-07-30'::DATE;
SELECT '10.5'::NUMERIC;
SELECT 'true'::BOOLEAN;
SELECT 123::TEXT;
SELECT now()::DATE;                          -- bỏ phần giờ
SELECT '{"a":1}'::JSONB;
SELECT '1,2,3'::TEXT;
SELECT string_to_array('1,2,3', ',')::INT[];
SELECT '1 day'::INTERVAL;

5.2. Ép kiểu thất bại

SELECT 'abc'::INTEGER;
-- ERROR: invalid input syntax for type integer: "abc"
 
SELECT ''::INTEGER;
-- ERROR: invalid input syntax for type integer: ""
 
SELECT '30/07/2026'::DATE;
-- ERROR (nếu datestyle không phải DMY)

Cách an toàn:

-- Kiểm tra trước bằng regex
SELECT CASE
          WHEN gia_tri ~ '^-?\d+$' THEN gia_tri::INTEGER
          ELSE NULL
       END AS so
FROM staging;
 
-- Chuỗi rỗng → NULL trước khi ép
SELECT NULLIF(trim(gia_tri), '')::NUMERIC FROM staging;
 
-- Ngày với định dạng cụ thể
SELECT to_date('30/07/2026', 'DD/MM/YYYY');
SELECT to_timestamp('30/07/2026 14:30', 'DD/MM/YYYY HH24:MI');

5.3. Cách làm mạnh nhất — hàm tự viết

CREATE OR REPLACE FUNCTION ep_so_an_toan(txt TEXT)
RETURNS NUMERIC AS $$
BEGIN
   RETURN txt::NUMERIC;
EXCEPTION WHEN OTHERS THEN
   RETURN NULL;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
 
SELECT ep_so_an_toan('123.45');   -- 123.45
SELECT ep_so_an_toan('abc');      -- NULL

Từ PostgreSQL 16 có sẵn cú pháp chuẩn:

SELECT CAST('abc' AS INTEGER DEFAULT NULL ON ERROR);   -- NULL
SELECT CAST('abc' AS INTEGER DEFAULT -1 ON ERROR);     -- -1

5.4. Ép kiểu ngầm định và trình tối ưu hóa

Ép kiểu ở cột làm mất tác dụng index

-- ❌ Index trên ma_kh không dùng được
WHERE ma_kh::TEXT = '12345'
 
-- ✅ Ép ở phía hằng số
WHERE ma_kh = '12345'::INT

Quy tắc chung: giữ cột nguyên vẹn ở một vế, ép kiểu ở vế còn lại.

Tương tự với hàm:

WHERE date_trunc('day', tao_luc) = '2026-07-30'    -- ❌ không dùng index
WHERE tao_luc >= '2026-07-30' AND tao_luc < '2026-07-31'   -- ✅

5.5. Ép kiểu trong ORDER BY

-- Cột TEXT chứa số → sắp xếp sai
SELECT ma FROM t ORDER BY ma;
-- '1', '10', '2', '20', '3'
 
-- Ép về số để sắp đúng
SELECT ma FROM t ORDER BY ma::INT;
-- '1', '2', '3', '10', '20'

6. Bảng tổng hợp

HàmCông dụng
CASE WHEN ... THEN ... ENDRẽ nhánh logic tổng quát
COALESCE(a, b, c)Giá trị không NULL đầu tiên
NULLIF(a, b)NULL nếu a = b
GREATEST(...) / LEAST(...)Lớn nhất / nhỏ nhất theo chiều ngang
CAST(x AS t) / x::tChuyển đổi kiểu
FILTER (WHERE ...)Điều kiện cho hàm tổng hợp
IS DISTINCT FROMSo sánh coi NULL là giá trị bình thường

Ba mẫu ghi nhớ:

COALESCE(NULLIF(x, ''), 'mac_dinh')   -- xử lý cả NULL lẫn chuỗi rỗng
a / NULLIF(b, 0)                       -- chống chia cho 0
LEAST(GREATEST(x, min), max)           -- giới hạn giá trị trong khoảng

7. Bài tập

  1. Phân loại phim theo rental_rate: dưới 1 = “Rẻ”, 1–3 = “Trung bình”, trên 3 = “Đắt”. Đếm số phim mỗi loại.
  2. Tạo bảng chéo: số phim theo rating (hàng) × phân loại độ dài (cột).
  3. Tính tỷ lệ phần trăm phim dài trên 120 phút của từng thể loại, tránh lỗi chia cho 0.
  4. Hiển thị return_date của bảng rental, thay NULL bằng “Chưa trả”.
  5. Sắp xếp phim theo thứ tự rating: G → PG → PG-13 → R → NC-17.
  6. Viết truy vấn an toàn ép cột text sang số, dòng nào không hợp lệ trả NULL.
  7. Vì sao câu sau luôn trả về 'có giá trị' dù ghi_chu IS NULL? Sửa lại:
    SELECT CASE ghi_chu WHEN NULL THEN 'rỗng' ELSE 'có giá trị' END FROM t;
  8. Cho COALESCE(a, b) và CASE WHEN a IS NOT NULL THEN a ELSE b END — chúng có luôn tương đương không?

8. Tóm tắt

  • CASE dừng ở nhánh đúng đầu tiên — sắp xếp điều kiện từ hẹp tới rộng.
  • Không có ELSE thì CASE trả NULL.
  • Dạng CASE đơn giản không so sánh được với NULL — dùng dạng tìm kiếm với IS NULL.
  • FILTER (WHERE ...) rõ ràng hơn CASE trong hàm tổng hợp.
  • COALESCE trả giá trị không NULL đầu tiên, đánh giá lười.
  • NULLIF(a, 0) chống chia cho 0; NULLIF(x, '') biến chuỗi rỗng thành NULL.
  • Mẫu vàng: COALESCE(NULLIF(x, ''), 'mac_dinh').
  • :: là cú pháp ép kiểu riêng của PostgreSQL, ngắn gọn hơn CAST.
  • Ép kiểu ở phía cột làm mất tác dụng index — luôn ép ở phía hằng số.

← Kiểu dữ liệu chuyên sâu | → Lệnh psql