Hàm COALESCE trong PostgreSQL
Xem thửTình huống mở đầu
Cà Phê Chú Tư in thẻ khách quen với dòng tên dạng An (Annie): tên thật rồi biệt danh trong ngoặc. Nhân viên ghép chuỗi bằng toán tử || và cho chạy trên năm khách. Khách nào có biệt danh thì thẻ đẹp. Khách nào chưa có biệt danh thì thẻ trống trơn, không còn cả tên thật, vì một giá trị NULL trong phép nối làm cả chuỗi thành NULL.
Hãy đoán trước: với COALESCE(nickname, name), khách Chi có nickname là chuỗi rỗng (không phải NULL) sẽ hiển thị tên Chi hay một dòng trống?
Ôn nhanh bài trước: CASE thiếu ELSE mà không nhánh nào khớp thì trả gì?
Đáp án: NULL. Bài này dạy một hàm ngắn gọn cho chính xử lý NULL mà CASE làm dài dòng.
Mục tiêu bài học
Sau bài này bạn có thể:
- Viết
COALESCEvới nhiều đối số và chỉ ra đối số nào được chọn. - Phân biệt
NULL, chuỗi rỗng và số 0: chỉNULLbịCOALESCEbỏ qua. - Giải thích kiểu kết quả, các lỗi
42804và22P02, và vì sao đối số sau không bị đánh giá (trừ hằng số). - Dùng
COALESCEđúng trong phép tính và hàm tổng hợp, và biết nó khiến chỉ mục thường không dùng được.
Bức tranh lớn
Xuyên suốt bài, COALESCE là danh sách số điện thoại để gọi lần lượt: gọi số thứ nhất, ai nghe máy thì dừng, không ai nghe thì gọi số tiếp theo. NULL là chuông reo mà không ai bắt máy. Chuỗi rỗng là bắt máy nhưng không nói gì, vẫn tính là đã nghe máy. Dự án xuyên suốt là thẻ khách quen của Cà Phê Chú Tư, database tên shop, với bảng customers năm khách và bảng orders sáu đơn.
Hình dưới trả lời một câu hỏi: COALESCE đọc các đối số theo thứ tự nào và dừng ở đâu?
Đang tải sơ đồ…
Cách đọc hình: đọc từ ô xanh dương trên cùng xuống. Mỗi ô cam hỏi một đối số có phải NULL không. Trả lời "no" thì ra ô xanh lá và dừng, các đối số sau không được đọc. Trả lời "yes" thì xuống đối số kế tiếp. Hết danh sách mà vẫn NULL thì ra ô đỏ. Chữ trong ô mới là nội dung chính, màu chỉ hỗ trợ.
Chỗ ẩn dụ không còn đúng: người thật nghe máy im lặng sẽ biết là chưa ai trả lời. COALESCE không biết, chuỗi rỗng đối với nó là một câu trả lời.
Khái niệm cốt lõi
Bài này có bốn khái niệm.
1. Trả về đối số đầu tiên khác NULL
COALESCE(a, b, c, ...) duyệt từ trái sang phải và trả về đối số đầu tiên không phải NULL. Nếu mọi đối số đều NULL thì kết quả là NULL. Nó tương đương CASE WHEN a IS NOT NULL THEN a WHEN b IS NOT NULL THEN b ... END, nhưng ngắn hơn nhiều. Danh sách thường kết thúc bằng một hằng số để kết quả không bao giờ là NULL.
2. Chỉ NULL bị bỏ qua
Chuỗi rỗng '', số 0 và false đều là giá trị thật. COALESCE thấy chúng là đã có giá trị và dừng ngay. Vì vậy dữ liệu nhập từ biểu mẫu, nơi "không điền" có thể thành chuỗi rỗng chứ không phải NULL, cần được xử lý riêng.
3. Kiểu kết quả và đánh giá lười
Các đối số được hợp nhất về một kiểu chung giống CASE: text với integer là lỗi 42804. Các đối số đứng sau đối số được chọn không được tính (đánh giá lười), nhưng biểu thức toàn hằng số như 1 / 0 bị tính sớm khi lập kế hoạch nên vẫn gây lỗi.
4. Dùng trong phép tính và chỉ mục
Phép tính với NULL cho NULL, nên hay bọc cột bằng COALESCE(cột, 0). Cái giá là bọc cột trong hàm làm chỉ mục thường trên cột đó không dùng được, trừ khi tạo chỉ mục biểu thức cho đúng biểu thức đó.
Ví dụ đầu tiên
Bắt đầu từ lỗi của nhân viên: ghép chuỗi bằng || khi nickname có thể là NULL. Dữ liệu: nickname của năm khách là Annie, NULL, chuỗi rỗng, NULL, Evie.
SELECT id, name,
name || ' (' || nickname || ')' AS card
FROM customers
ORDER BY id;
Dự đoán trước khi xem kết quả: những khách nào có thẻ trống?
Kết quả [Đã chạy: PostgreSQL 16.15]:
id | name | card
----+------+------------
1 | An | An (Annie)
2 | Binh |
3 | Chi | Chi ()
4 | Dung |
5 | Eva | Eva (Evie)
(5 rows)
Binh và Dung có nickname là NULL nên cả chuỗi thành NULL. Chi có chuỗi rỗng nên thẻ in Chi (), dở nhưng không trống. Giờ đến COALESCE: hiển thị biệt danh, nếu chưa có thì tên thật.
SELECT id, name, nickname,
COALESCE(nickname, name) AS shown -- nickname là NULL thì lấy name
FROM customers
ORDER BY id;
Kết quả [Đã chạy: PostgreSQL 16.15]:
id | name | nickname | shown
----+------+----------+-------
1 | An | Annie | Annie
2 | Binh | | Binh
3 | Chi | |
4 | Dung | | Dung
5 | Eva | Evie | Evie
(5 rows)
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
COALESCE( | Mở hàm, sẽ duyệt đối số từ trái sang phải | Cho phép nhiều đối số, ít nhất một |
nickname, | Đối số thứ nhất: nếu khác NULL thì chọn và dừng | Ưu tiên biệt danh nếu khách đã đặt |
name) | Đối số cuối: chỉ được đọc khi nickname là NULL | Cột name có NOT NULL nên kết quả không bao giờ NULL |
AS shown | Đặt tên cho cột kết quả | Không đặt tên thì cột mang tên coalesce |
Binh và Dung nhận tên thật. Nhưng Chi hiển thị trống: nickname của Chi là chuỗi rỗng, COALESCE coi nó là giá trị thật và dừng ngay, không đi đến name. Bạn sẽ gặp lại điểm này ở phần Thử phá nó đi.
Danh sách dài hơn: thông tin liên hệ ưu tiên biệt danh, rồi điện thoại, rồi email, rồi một chuỗi dự phòng.
SELECT id,
COALESCE(nickname, phone, email, 'no contact') AS contact -- hằng cuối để không bao giờ NULL
FROM customers
ORDER BY id;
id | contact
----+------------
1 | Annie
2 | 0902
3 |
4 | no contact
5 | Evie
(5 rows)
Chuỗi cuối no contact bảo đảm kết quả không bao giờ NULL. Chi vẫn nhận chuỗi rỗng ở cột đầu, bài học nhỏ của cùng một bẫy. Cách đúng cho thẻ khách quen, dùng COALESCE bọc cả phần ngoặc:
SELECT id,
name || COALESCE(' (' || nickname || ')', '') AS card -- nickname NULL thì cả phần ngoặc thành rỗng
FROM customers
ORDER BY id;
id | card
----+------------
1 | An (Annie)
2 | Binh
3 | Chi ()
4 | Dung
5 | Eva (Evie)
(5 rows)
Khi nickname là NULL thì ' (' || nickname || ')' là NULL, COALESCE thay bằng chuỗi rỗng, và thẻ chỉ còn tên thật. Tiếp theo là phép tính tiền. Cột discount là số tiền giảm, NULL nghĩa là chưa ghi nhận:
SELECT id, total, discount,
total - COALESCE(discount, 0) AS pay, -- thiếu giảm giá coi như không giảm
total - discount AS pay_naive
FROM orders
ORDER BY id;
id | total | discount | pay | pay_naive
----+--------+----------+--------+-----------
1 | 250.00 | | 250.00 |
2 | 40.00 | 5.00 | 35.00 | 35.00
3 | 120.00 | 0.00 | 120.00 | 120.00
4 | 90.00 | 10.00 | 80.00 | 80.00
5 | | | |
6 | 10.00 | | 10.00 |
(6 rows)
Cột pay_naive trống ở mọi đơn có discount NULL. Cột pay coi đơn thiếu discount như không giảm, nên vẫn ra số tiền. Đây là quyết định nghiệp vụ, không phải luật của PostgreSQL: bạn chỉ nên đổi NULL thành 0 khi "không biết" thật sự có nghĩa là "không giảm".
Xem từng bước
Animation dưới đây cho ba khách đi qua cùng một COALESCE bốn đối số. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: PostgreSQL 16.15, COALESCE(nickname, phone, email, 'no contact')]:
| Khách | nickname | phone | Kết quả | |
|---|---|---|---|---|
| Binh | NULL | 0902 | NULL | 0902, từ đối số thứ hai |
| Chi | chuỗi rỗng | NULL | chi@mail.com | chuỗi rỗng, từ đối số thứ nhất |
| Dung | NULL | NULL | NULL | no contact, từ hằng cuối |
Bạn vừa làm được: chỉ ra đối số nào của COALESCE được chọn và vì sao hai đối số sau không được đọc.
Concept Check
Dùng bảng customers năm khách ở trên.
Câu 1. COALESCE(nickname, name) trả gì cho Chi, có nickname là chuỗi rỗng và name là Chi?
- A.
Chi, vì chuỗi rỗng đượcCOALESCEcoi là thiếu giá trị. - B.
NULL, vì chuỗi rỗng làm cả hàm không chọn được gì. - C. Chuỗi rỗng, vì nó là giá trị thật, khác
NULL. - D. Lỗi, vì chuỗi rỗng không hợp lệ làm đối số hàm.
Câu 2. COALESCE(nickname, phone, email) cho Dung (cả ba cột đều NULL) trả gì?
- A. Chuỗi rỗng, vì
COALESCEđổiNULLcuối cùng thành rỗng. - B.
NULL, vì không có đối số nào khácNULL. - C. Lỗi
no value, vì hàm bắt buộc phải tìm được một giá trị. - D.
0, vìCOALESCEmặc định thayNULLbằng số không.
Câu 3. SELECT sum(COALESCE(total, 0)) FROM orders WHERE status = 'draft' (không có đơn nào) trả gì?
- A.
0, vìCOALESCEđã biến mọi giá trị thiếu thành 0. - B. Lỗi, vì tập dòng rỗng không đưa được vào hàm tổng hợp.
- C.
NULL, vì không có dòng nào đểCOALESCExử lý. - D. Một dòng trống, vì kết quả được tính cho mỗi nhóm.
Đáp án và giải thích:
- Câu 1: C. Bài đã chạy
first: Chi hiển thị trống. A là niềm tin sai phổ biến nhất của bài, B và D bịa ra hành vi. - Câu 2: B. Mọi đối số đều
NULLnên kết quảNULL. Bài khuyên thêm một hằng ở cuối danh sách để tránh trường hợp này. - Câu 3: C.
sumtrên tập rỗng trảNULLvà không có dòng nào đểCOALESCEbên trong xử lý. BọcCOALESCEbên ngoàisummới cho 0, xem phần Lời giải và đáp án.
Làm cùng mình
Chú Tư muốn một cột pay là total trừ discount (đơn thiếu discount coi như không giảm), và một cột pay_or_zero hiển thị 0 cho đơn chưa có total.
Bước 1. Vẽ ra giấy
Vẽ bảng sáu đơn với total (250.00, 40.00, 120.00, 90.00, NULL, 10.00) và discount (NULL, 5.00, 0.00, 10.00, NULL, NULL). Dự đoán pay của đơn 1 và đơn 5, và pay_or_zero của đơn 5.
Bước 2. Viết lệnh
Điền hai chỗ trống. Hàm COALESCE đặt lồng nhau được:
SELECT id, total, discount,
-- TODO 1: total trừ discount, discount NULL thì coi như 0
AS pay,
-- TODO 2: pay nhưng nếu kết quả là NULL thì hiển thị 0
AS pay_or_zero
FROM orders
ORDER BY id;
Gợi ý: chỗ trống thứ hai bọc cả biểu thức của chỗ trống thứ nhất trong một COALESCE nữa.
Bước 3. So kết quả
Đối chiếu với mục Lời giải và đáp án. Đơn 5 phải ra NULL ở cột pay và 0 ở cột pay_or_zero.
Thử phá nó đi
Bốn bẫy hay gặp với COALESCE [Đã chạy: PostgreSQL 16.15]. Mỗi lỗi chạy trong một lệnh riêng.
Bẫy 1: đối số khác kiểu. Cột nickname là text, số 0 là integer:
SELECT COALESCE(nickname, 0) FROM customers;
ERROR: COALESCE types text and integer cannot be matched
LINE 1: SELECT COALESCE(nickname, 0) FROM customers;
^
Bẫy 2: hằng chuỗi 'none' chưa có kiểu, nên PostgreSQL thử đổi nó sang numeric của cột total và thất bại:
SELECT COALESCE(total, 'none') FROM orders;
ERROR: invalid input syntax for type numeric: "none"
LINE 1: SELECT COALESCE(total, 'none') FROM orders;
^
Bẫy 3: chuỗi rỗng không phải NULL. Ta đã thấy Chi hiển thị trống. Muốn coi chuỗi rỗng như thiếu, đổi nó thành NULL trước khi đưa vào COALESCE, ở đây bằng một CASE (bài sau sẽ có hàm ngắn hơn cho việc này):
SELECT id, name,
COALESCE(CASE WHEN nickname <> '' THEN nickname END, name) AS shown -- chuỗi rỗng thành NULL trước
FROM customers
ORDER BY id;
id | name | shown
----+------+-------
1 | An | Annie
2 | Binh | Binh
3 | Chi | Chi
4 | Dung | Dung
5 | Eva | Evie
(5 rows)
Bẫy 4: đánh giá lười có ngoại lệ. Ở dòng có discount khác NULL, đối số 1 / 0 lẽ ra không cần tính, nhưng lệnh vẫn lỗi vì 1 / 0 là hằng số bị tính sớm:
SELECT id, COALESCE(discount, 1 / 0) -- 1 / 0 là hằng số nên bị tính sớm
FROM orders
WHERE id = 3;
ERROR: division by zero
Đối số dựa vào cột thì lười thật. Đơn 2 có qty bằng 0 nhưng discount khác NULL, nên phép chia total / qty không bao giờ chạy cho đơn đó:
SELECT id, qty, discount,
COALESCE(discount, total / qty) AS fallback_value
FROM orders
ORDER BY id;
id | qty | discount | fallback_value
----+-----+----------+---------------------
1 | 5 | | 50.0000000000000000
2 | 0 | 5.00 | 5.00
3 | 3 | 0.00 | 0.00
4 | 2 | 10.00 | 10.00
5 | 4 | |
6 | 1 | | 10.0000000000000000
(6 rows)
| Tình huống | Mã lỗi hoặc triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|---|
COALESCE(nickname, 0) | 42804 | text và integer không có kiểu chung | Ép cùng kiểu, ví dụ 0::text hoặc dùng hằng đúng kiểu |
COALESCE(total, 'none') | 22P02 | Hằng chuỗi bị đổi sang kiểu của cột | Dùng giá trị thay thế đúng kiểu số |
Thẻ trống dù có name | Không có lỗi | Chuỗi rỗng được coi là giá trị thật | Đổi chuỗi rỗng thành NULL trước khi gọi hàm |
COALESCE(discount, 1 / 0) | 22012 | Hằng số bị tính sớm khi lập kế hoạch | Đừng dùng hằng số làm đường lui |
Hình dưới đối chiếu hiểu lầm lớn nhất của bài với mô hình đúng.
Đang tải sơ đồ…
Cách đọc hình: hai chuỗi độc lập. Chuỗi đỏ là niềm tin sai: chuỗi rỗng được coi như thiếu. Chuỗi xanh là mô hình đã chạy ở ví dụ đầu: chỉ NULL bị bỏ qua, chuỗi rỗng thắng và thẻ của Chi in trống.
Còn một bẫy tinh tế về nơi đặt COALESCE khi dùng hàm tổng hợp. Đặt bên trong thì đổi giá trị từng dòng. Đặt bên ngoài thì đổi kết quả cuối cùng. Với trung bình, hai cách cho hai con số khác nhau:
SELECT round(avg(total), 2) AS avg_skip_null,
round(avg(COALESCE(total, 0)), 2) AS avg_null_as_zero
FROM orders;
avg_skip_null | avg_null_as_zero
---------------+------------------
102.00 | 85.00
(1 row)
avg(total) bỏ qua đơn NULL và chia cho 5. avg(COALESCE(total, 0)) biến đơn đó thành 0 rồi chia cho 6. Cách nào đúng tùy câu hỏi nghiệp vụ: "trung bình mỗi đơn có tiền" hay "trung bình mỗi đơn kể cả đơn chưa nhập tiền".
Tự tay làm
Dùng chung bảng customers và orders của bài. Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Đoán liên hệ. Đầu vào: COALESCE(nickname, phone, email, 'no contact') trên Binh, Chi và Dung (xem bảng customers).
| Khách | Kết quả |
|---|---|
| Binh | ? |
| Chi | ? |
| Dung | ? |
Tiêu chí đạt: đúng cả ba và chỉ ra kết quả nào đến từ đối số nào. Gợi ý: nickname của Chi là chuỗi rỗng, không phải NULL.
Vận dụng
Bài 2. Tổng và trung bình đúng nghĩa. Đầu vào: bảng orders. Phần a: tổng total của đơn có trạng thái draft (không có đơn nào), hiển thị 0 thay vì trống. Phần b: so sánh sum không bọc COALESCE với phiên bản bọc bên ngoài. Tiêu chí đạt: phần a ra 0, bản không bọc ra trống. Gợi ý: đặt COALESCE bên ngoài sum, không phải bên trong.
Thử thách (tùy chọn)
Bài 3. Điền biệt danh còn thiếu. Đầu vào: bảng customers. Viết một UPDATE đặt nickname bằng name cho khách chưa có biệt danh (NULL hoặc chuỗi rỗng) và giữ nguyên biệt danh đã có. Tiêu chí đạt: Binh, Chi, Dung nhận tên thật; An và Eva giữ Annie và Evie. Gợi ý: COALESCE(nickname, name) một mình sẽ bỏ sót Chi.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài. Mỗi câu ghi mục tiêu được kiểm tra.
- (Mục tiêu 1, nhớ lại)
COALESCE(a, b, c)trả gì khi cả ba đối số làNULL? - (Mục tiêu 2, dự đoán)
COALESCE('', 'x')trả gì, vàCOALESCE(0, 5)trả gì? - (Mục tiêu 3, tìm lỗi)
COALESCE(total, 'none')trên cộtnumericbáo lỗi mã nào và vì sao? - (Mục tiêu 3, giải thích) Vì sao
COALESCE(discount, total / qty)không lỗi ở dòng cóqtybằng 0 vàdiscountkhácNULL, cònCOALESCE(discount, 1 / 0)thì lỗi? - (Mục tiêu 4, tình huống) Có chỉ mục trên
nickname. CâuWHERE COALESCE(nickname, name) = 'x'dùng được chỉ mục đó không, và cách xử lý là gì?
Điều cần nhớ
COALESCEtrả đối số đầu tiên khácNULL; tất cảNULLthì kết quảNULL.- Chỉ
NULLbị bỏ qua: chuỗi rỗng, 0 vàfalselà giá trị thật. - Các đối số hợp nhất về một kiểu chung; khác kiểu là lỗi
42804, hằng chuỗi sai kiểu là lỗi22P02. - Đối số sau không được tính, trừ biểu thức toàn hằng số bị tính sớm.
COALESCEbên trong hàm tổng hợp đổi từng dòng, bên ngoài đổi kết quả cuối; bọc cột trong hàm làm chỉ mục thường không dùng được.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
Giá trị mặc định cho NULL | COALESCE(cột, giá_trị) | Giá trị phải cùng kiểu với cột |
| Danh sách ưu tiên | COALESCE(a, b, c, 'default') | Hằng cuối để không bao giờ NULL |
| Coi chuỗi rỗng là thiếu | COALESCE(CASE WHEN c <> '' THEN c END, d) | Bài sau dùng NULLIF cho gọn |
| Tổng không bao giờ trống | COALESCE(sum(cột), 0) | Tập rỗng cho NULL nếu thiếu bọc ngoài |
Phép tính chịu được NULL | a - COALESCE(b, 0) | Chỉ khi thiếu có nghĩa là 0 |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho ngày mai: chuỗi rỗng trong cột nickname làm hỏng danh sách ưu tiên. Có hàm nào biến thẳng chuỗi rỗng thành NULL mà không cần viết CASE dài không?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Bọc cột trong
COALESCElàm chỉ mục thường không dùng được. Bảngmemberscó 20000 dòng, chỉ mục trênnickname, một phần tư số dòng có biệt danh. So sánh trực tiếp thì dùng chỉ mục [Có thể khác nhau: kế hoạch phụ thuộc thống kê và cấu hình, đây là kết quả trên PostgreSQL 16.15 sauANALYZE]:
EXPLAIN (COSTS OFF)
SELECT * FROM members WHERE nickname = 'nick400';
QUERY PLAN
----------------------------------------------
Index Scan using members_nick_idx on members
Index Cond: (nickname = 'nick400'::text)
(2 rows)
Bọc cột bằng COALESCE thì chỉ mục trên nickname không còn dùng được và PostgreSQL quét toàn bảng:
EXPLAIN (COSTS OFF)
SELECT * FROM members WHERE COALESCE(nickname, name) = 'nick400';
QUERY PLAN
--------------------------------------------------------
Seq Scan on members
Filter: (COALESCE(nickname, name) = 'nick400'::text)
(2 rows)
Cách xử lý là tạo chỉ mục biểu thức đúng với biểu thức trong WHERE:
EXPLAIN (COSTS OFF)
SELECT * FROM members WHERE COALESCE(nickname, name) = 'nick400';
QUERY PLAN
------------------------------------------------------------
Index Scan using members_shown_idx on members
Index Cond: (COALESCE(nickname, name) = 'nick400'::text)
(2 rows)
Kế hoạch dùng chỉ mục biểu thức. Chỉ mục này gắn với đúng biểu thức COALESCE(nickname, name); một biểu thức khác, ví dụ đổi thứ tự hai đối số, sẽ không khớp với nó [Suy luận: bài chưa chạy trường hợp này].
- So sánh với lựa chọn khác. MySQL có
IFNULL(a, b)với hai đối số và Oracle cóNVL(a, b); PostgreSQL không có hai hàm này.COALESCEthuộc chuẩn SQL, chạy trên cả ba hệ và nhận nhiều đối số. Nếu viết lại bằngCASEthì dài gấp nhiều lần:CASE WHEN a IS NOT NULL THEN a ELSE b END. - Hiểu lầm nâng cao. Kiểu của
COALESCE(NULL, NULL)vẫn làtextchứ không phải kiểu nào "không biết". Vì mọi đối số làNULLchưa có kiểu, PostgreSQL chọntext:
SELECT COALESCE(NULL, NULL) AS v, pg_typeof(COALESCE(NULL, NULL)) AS type_name;
v | type_name
---+-----------
| text
(1 row)
- Giới hạn của bài. Bài này chưa nói đến
NULLIF(bài sau), toán tửIS DISTINCT FROM,GREATESTvàLEAST(hai hàm này bỏ quaNULL, khác với phép cộng), hayCOALESCEtrong cột sinh tự động. - Đọc thêm. PostgreSQL 16 Documentation, mục
COALESCE: https://www.postgresql.org/docs/16/functions-conditional.html#FUNCTIONS-COALESCE-NVL-IFNULL và mục chỉ mục biểu thức: https://www.postgresql.org/docs/16/indexes-expressional.html
Bài tiếp theo
Bài sau là hàm NULLIF: chiều ngược lại của COALESCE, biến một giá trị cụ thể thành NULL, và là cách chuẩn để tránh chia cho 0 và để coi chuỗi rỗng là thiếu. Mini-dự án nối tiếp: tính giá mỗi ly của Cà Phê Chú Tư mà không bị lỗi chia cho 0.
Lời giải và đáp án
Làm cùng mình. Hai chỗ trống điền:
total - COALESCE(discount, 0)
COALESCE(total - COALESCE(discount, 0), 0)
SELECT id, total, discount,
total - COALESCE(discount, 0) AS pay,
COALESCE(total - COALESCE(discount, 0), 0) AS pay_or_zero
FROM orders
ORDER BY id;
id | total | discount | pay | pay_or_zero
----+--------+----------+--------+-------------
1 | 250.00 | | 250.00 | 250.00
2 | 40.00 | 5.00 | 35.00 | 35.00
3 | 120.00 | 0.00 | 120.00 | 120.00
4 | 90.00 | 10.00 | 80.00 | 80.00
5 | | | | 0
6 | 10.00 | | 10.00 | 10.00
(6 rows)
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
total - COALESCE(discount, 0) AS pay | Đổi discount NULL thành 0 rồi trừ | Đơn 1 chưa ghi giảm giá nên trả đủ 250.00 |
COALESCE(total - ..., 0) AS pay_or_zero | Bọc kết quả phép trừ, NULL thành 0 | Đơn 5 chưa có total nên phép trừ là NULL, bọc ngoài cho ra 0 |
Đơn 5 ra NULL ở pay và 0 ở pay_or_zero. Hai cấp COALESCE có hai lý do khác nhau: cấp trong là quy ước "không biết giảm là không giảm", cấp ngoài là quy ước hiển thị. Hãy cân nhắc có nên hiển thị đơn chưa có tiền là 0 hay để trống.
Bài 1.
SELECT id, name,
COALESCE(nickname, phone, email, 'no contact') AS contact
FROM customers
WHERE id IN (2, 3, 4)
ORDER BY id;
id | name | contact
----+------+------------
2 | Binh | 0902
3 | Chi |
4 | Dung | no contact
(3 rows)
Binh nhận 0902 (đối số thứ hai), Chi nhận chuỗi rỗng (đối số thứ nhất, không phải NULL), Dung nhận no contact (hằng cuối).
Bài 2. Phần a, bọc bên ngoài:
SELECT COALESCE(sum(total), 0) AS draft_revenue
FROM orders
WHERE status = 'draft';
draft_revenue
---------------
0
(1 row)
Phần b, không bọc:
SELECT sum(total) AS draft_revenue
FROM orders
WHERE status = 'draft';
draft_revenue
---------------
(1 row)
Không có dòng nào để cộng nên sum trả NULL, COALESCE bên ngoài đổi thành 0. Đặt COALESCE bên trong sum không giúp, vì không có dòng nào để hàm bên trong xử lý.
Bài 3.
UPDATE customers
SET nickname = COALESCE(CASE WHEN nickname <> '' THEN nickname END, name)
RETURNING id, nickname;
id | nickname
----+----------
1 | Annie
2 | Binh
3 | Chi
4 | Dung
5 | Evie
(5 rows)
UPDATE 5
Lệnh này biến nickname rỗng hoặc NULL thành tên thật, giữ nguyên Annie và Evie. Cách ngây thơ COALESCE(nickname, name) bỏ sót Chi:
UPDATE customers
SET nickname = COALESCE(nickname, name)
RETURNING id, nickname;
id | nickname
----+----------
1 | Annie
2 | Binh
3 |
4 | Dung
5 | Evie
(5 rows)
UPDATE 5
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
CASE WHEN nickname <> '' THEN nickname END | Trả biệt danh nếu không rỗng, ngược lại NULL | Chuỗi rỗng thành NULL, và NULL <> '' cũng cho NULL nên giữ nguyên là NULL |
COALESCE(..., name) | Nếu vế trên là NULL thì lấy name | Cả NULL lẫn chuỗi rỗng đều rơi xuống tên thật |
RETURNING id, nickname | In các dòng sau khi sửa | Xem kết quả mà không cần SELECT thêm |
Tự kiểm tra.
NULL.COALESCE('', 'x')trả chuỗi rỗng vàCOALESCE(0, 5)trả 0, vì cả hai đối số đầu đều khácNULL.- Lỗi
22P02,invalid input syntax for type numeric: "none": hằng chuỗi chưa có kiểu bị đổi sangnumerictheo cộttotal. - Đối số dựa vào cột chỉ được tính khi đối số trước là
NULL.1 / 0là hằng số nên bị tính sớm khi lập kế hoạch, không còn phụ thuộc dữ liệu. - Không: bọc cột trong hàm làm chỉ mục trên
nicknamekhông dùng được. Tạo chỉ mục biểu thức((COALESCE(nickname, name)))hoặc viết điều kiện bằng chính cột.