Ràng buộc CHECK trong PostgreSQL
Xem thửTình huống mở đầu
Cuối ngày, báo cáo doanh thu của Cà Phê Chú Tư báo âm 90.00. Chú Tư lần ngược lại và thấy một dòng trong bảng giá menu_items của database shop: món Latte mang giá -45.00, vì nhân viên gõ nhầm dấu trừ, và hai ly bán trong ngày trừ thẳng vào doanh thu. Chú Tư bảo: "Thêm luật giá phải dương là xong." Nhân viên thêm luật đó. Hôm sau, báo cáo lại thiếu một khoản: món Trà đá chưa nhập giá nhưng vẫn nằm trong bảng, và phép nhân giá với số ly cho kết quả rỗng. Luật giá dương đã có, vậy mà món không có giá vẫn lọt vào.
Hãy đoán trước: luật price > 0 có từ chối giá NULL không, và nếu không thì vì sao một luật so sánh lại bỏ sót đúng giá trị khó chịu nhất?
Ôn nhanh bài trước: xóa một đơn đang có món, khóa ngoại dùng hành động mặc định thì trả mã lỗi nào?
Đáp án: 23503, vì khóa ngoại chặn việc bỏ rơi các món con. Bài này chuyển sang luật không nhìn bảng khác mà chỉ nhìn chính dòng đang ghi.
Mục tiêu bài học
Sau bài này bạn có thể:
- Viết
CHECKở mức cột và mức bảng, đọc được mã lỗi23514và tên ràng buộc trong thông báo. - Giải thích vì sao
NULLđi quaCHECK, và cách chặn nó bằngNOT NULL. - Nêu giới hạn của
CHECK: chỉ nhìn dòng đang ghi, không dùng truy vấn con. - Thêm
CHECKvào bảng đã có dữ liệu bằngNOT VALIDrồiVALIDATE CONSTRAINT.
Bức tranh lớn
Xuyên suốt bài, CHECK là một người soát vé đứng ở cửa chỉ nhìn đúng một tờ vé trên tay: vé hợp lệ thì cho vào, vé sai ràng buộc thì giữ lại. Anh ta không có danh sách khách ở bảng khác và không biết ai đã vào trước. Dự án xuyên suốt vẫn là Cà Phê Chú Tư, database shop. Bài này thêm bảng menu_items với ba món, và quay lại bảng orders để thêm luật vào dữ liệu đã có.
CREATE TABLE menu_items (
id int PRIMARY KEY,
name text NOT NULL,
price numeric(10,2) CHECK (price > 0),
size text CHECK (size IN ('S', 'M', 'L')),
sale_price numeric(10,2),
CONSTRAINT menu_items_sale_le_price CHECK (sale_price <= price)
);
INSERT INTO menu_items VALUES
(1, 'Latte', 45.00, 'M', NULL),
(2, 'Tea', 30.00, 'S', 25.00),
(3, 'Cake', 55.00, 'L', NULL);
Hình dưới trả lời một câu hỏi: PostgreSQL quyết định nhận hay từ chối một dòng dựa trên điều gì?
Đang tải sơ đồ…
Cách đọc hình: đọc từ ô xanh dương xuống ô cam. Chữ trong ô cam là câu hỏi: kết quả biểu thức có phải FALSE không. Nếu có, ô đỏ báo 23514 và dòng bị từ chối. Nếu không, tức là TRUE hoặc NULL, ô xanh lá nhận dòng. Nhánh "no" gộp hai kết quả vào cùng một đường, đó chính là điểm gây bất ngờ của bài.
Chỗ ẩn dụ không còn đúng: người soát vé ngoài đời gặp vé không đọc được thì sẽ giữ lại. CHECK gặp kết quả không xác định thì cho qua.
Khái niệm cốt lõi
Bài này có năm khái niệm, khái niệm thứ hai là điều khiến nhiều người trượt chân.
1. CHECK là biểu thức boolean trên một dòng
CHECK (biểu_thức) được tính mỗi lần một dòng được thêm hoặc sửa. Kết quả FALSE làm câu lệnh bị từ chối với mã 23514 (check_violation). Có hai chỗ viết: ngay sau cột (price numeric CHECK (price > 0)) và ở mức bảng (CHECK (sale_price <= price)). Tên mặc định là <bảng>_<cột>_check khi luật chỉ nhắc một cột, và bạn đặt tên riêng bằng CONSTRAINT tên CHECK (...).
2. NULL đi qua CHECK
Biểu thức có NULL cho kết quả UNKNOWN, và CHECK chỉ từ chối FALSE. Vì vậy CHECK (price > 0) nhận cả price là NULL. Đây là chiều ngược với WHERE, nơi dòng UNKNOWN bị loại. Muốn bắt buộc có giá trị, thêm NOT NULL.
3. Luật mức cột và luật mức bảng
Luật viết ngay sau cột nên chỉ nhắc cột đó. Luật cần so sánh nhiều cột trong một dòng, như sale_price <= price, viết ở mức bảng. Cả hai cùng loại ràng buộc, chỉ khác chỗ viết.
4. CHECK chỉ nhìn dòng đang ghi
Biểu thức không được dùng truy vấn con, không đọc được bảng khác, và không biết các dòng khác trong cùng bảng. Cố viết truy vấn con cho lỗi 0A000 (feature_not_supported). Luật liên quan tới bảng khác thuộc về FOREIGN KEY, trigger hoặc ứng dụng.
5. Thêm CHECK vào bảng đã có dữ liệu
ALTER TABLE ... ADD CONSTRAINT ... CHECK (...) quét mọi dòng cũ và thất bại với 23514 nếu có dòng sai. NOT VALID bỏ qua dòng cũ nhưng áp dụng ngay cho dòng mới, rồi VALIDATE CONSTRAINT kiểm dòng cũ sau khi bạn đã dọn dữ liệu.
Ví dụ đầu tiên
Bảng menu_items của bài có ba luật: giá dương, size chỉ nhận S, M, L, và giá khuyến mãi không vượt giá gốc. Lệnh \d menu_items của psql liệt kê cả ba [Đã chạy: PostgreSQL 16.15]:
\d menu_items
Table "public.menu_items"
Column | Type | Collation | Nullable | Default
------------+---------------+-----------+----------+---------
id | integer | | not null |
name | text | | not null |
price | numeric(10,2) | | |
size | text | | |
sale_price | numeric(10,2) | | |
Indexes:
"menu_items_pkey" PRIMARY KEY, btree (id)
Check constraints:
"menu_items_price_check" CHECK (price > 0::numeric)
"menu_items_sale_le_price" CHECK (sale_price <= price)
"menu_items_size_check" CHECK (size = ANY (ARRAY['S'::text, 'M'::text, 'L'::text]))
Hai luật mức cột mang tên do PostgreSQL tự đặt, menu_items_price_check và menu_items_size_check. Luật mức bảng mang tên bạn đặt, menu_items_sale_le_price. Bây giờ thử ba dòng sai, mỗi dòng vào một lệnh riêng để thông báo hiện đúng chỗ. Giá âm:
INSERT INTO menu_items VALUES (4, 'Mocha', -45.00, 'M', NULL);
ERROR: new row for relation "menu_items" violates check constraint "menu_items_price_check"
DETAIL: Failing row contains (4, Mocha, -45.00, M, null).
Dự đoán trước khi xem lệnh kế: thông báo của size bằng 'XL' sẽ nhắc tới tên ràng buộc nào?
INSERT INTO menu_items VALUES (4, 'Mocha', 50.00, 'XL', NULL);
ERROR: new row for relation "menu_items" violates check constraint "menu_items_size_check"
DETAIL: Failing row contains (4, Mocha, 50.00, XL, null).
Giá khuyến mãi 60 cao hơn giá gốc 50:
INSERT INTO menu_items VALUES (4, 'Mocha', 50.00, 'M', 60.00);
ERROR: new row for relation "menu_items" violates check constraint "menu_items_sale_le_price"
DETAIL: Failing row contains (4, Mocha, 50.00, M, 60.00).
Cả ba lỗi đều nhắc đúng tên ràng buộc vi phạm và in cả dòng bị từ chối. Để xem mã lỗi và thử UPDATE, ta chạy một khối có \echo in mã gần nhất sau mỗi lệnh lỗi [Đã chạy: PostgreSQL 16.15]:
\set VERBOSITY sqlstate
INSERT INTO menu_items VALUES (4, 'Mocha', -45.00, 'M', NULL);
\echo negative price: :LAST_ERROR_SQLSTATE
INSERT INTO menu_items VALUES (4, 'Mocha', 50.00, 'XL', NULL);
\echo size XL: :LAST_ERROR_SQLSTATE
INSERT INTO menu_items VALUES (4, 'Mocha', 50.00, 'M', 60.00);
\echo sale above price: :LAST_ERROR_SQLSTATE
UPDATE menu_items SET price = 0 WHERE id = 1;
\echo update to zero: :LAST_ERROR_SQLSTATE
SELECT id, name, price FROM menu_items ORDER BY id;
negative price: 23514
size XL: 23514
sale above price: 23514
update to zero: 23514
id | name | price
----+-------+-------
1 | Latte | 45.00
2 | Tea | 30.00
3 | Cake | 55.00
(3 rows)
ERROR: 23514
ERROR: 23514
ERROR: 23514
ERROR: 23514
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
VALUES (4, 'Mocha', -45.00, 'M', NULL) | Bị từ chối 23514 | price > 0 cho FALSE với giá âm |
VALUES (4, 'Mocha', 50.00, 'XL', NULL) | Bị từ chối 23514 | size IN ('S', 'M', 'L') cho FALSE với 'XL' |
VALUES (4, 'Mocha', 50.00, 'M', 60.00) | Bị từ chối 23514 | Luật mức bảng so sánh hai cột cùng dòng |
UPDATE menu_items SET price = 0 WHERE id = 1; | Bị từ chối 23514 | CHECK cũng chạy lại khi sửa dòng |
Đến phần gây bất ngờ. Món Trà đá chưa có giá, tức price là NULL. Luật price > 0 vẫn có hiệu lực, nhưng [Đã chạy: PostgreSQL 16.15]:
INSERT INTO menu_items VALUES (4, 'Iced tea', NULL, 'M', NULL);
SELECT id, name, price FROM menu_items ORDER BY id;
INSERT 0 1
id | name | price
----+----------+-------
1 | Latte | 45.00
2 | Tea | 30.00
3 | Cake | 55.00
4 | Iced tea |
(4 rows)
Dòng vào được. Tại sao một luật so sánh lại không nhận ra NULL? Vì NULL > 0 không phải FALSE mà là UNKNOWN. Hãy so sánh cách WHERE và CHECK đối xử với cùng dòng đó:
INSERT INTO menu_items VALUES (4, 'Iced tea', NULL, 'M', NULL);
SELECT count(*) AS all_rows,
count(*) FILTER (WHERE price > 0) AS where_true,
count(*) FILTER (WHERE NOT (price > 0)) AS where_false,
count(*) FILTER (WHERE price IS NULL) AS where_null
FROM menu_items;
INSERT 0 1
all_rows | where_true | where_false | where_null
----------+------------+-------------+------------
4 | 3 | 0 | 1
(1 row)
Với WHERE, dòng NULL không thỏa price > 0 và cũng không thỏa phủ định NOT (price > 0), nên nó bị loại ở cả hai. CHECK thì nhận nó. Cách chặn là thêm NOT NULL vào cột [Đã chạy: PostgreSQL 16.15]:
ALTER TABLE menu_items ALTER COLUMN price SET NOT NULL;
\set VERBOSITY sqlstate
INSERT INTO menu_items VALUES (4, 'Iced tea', NULL, 'M', NULL);
\echo null price after NOT NULL: :LAST_ERROR_SQLSTATE
ALTER TABLE
null price after NOT NULL: 23502
ERROR: 23502
Giờ lỗi mang mã 23502 của NOT NULL, không phải 23514. Hai luật làm hai việc khác nhau và thường đi chung với nhau.
Xem từng bước
Animation dưới đây theo dõi ba món cố vào bảng menu_items qua luật price > 0. 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, các khối codes, null_pass và not_null_fix ở trên]:
| Giá thử | Kết quả price > 0 | Cổng quyết định | Mã lỗi | Dòng có vào bảng không |
|---|---|---|---|---|
| 45.00 | TRUE | CHECK | Không có | Có |
| -45.00 | FALSE | CHECK | 23514 | Không |
NULL, chưa có NOT NULL | UNKNOWN | CHECK, cho qua | Không có | Có |
NULL, sau khi thêm NOT NULL | Không tính | NOT NULL | 23502 | Không |
Bạn vừa làm được: đọc một lỗi CHECK và chỉ ra ràng buộc nào vi phạm, rồi giải thích vì sao NULL lọt qua và cách chặn.
Concept Check
Dùng bảng menu_items có price numeric CHECK (price > 0), chưa có NOT NULL.
Câu 1. INSERT một món có giá -45.00. Mã lỗi là gì?
- A.
23502, vì giá âm coi như thiếu giá trị. - B.
23505, vì giá âm trùng một món đã có. - C.
23514, vìprice > 0choFALSE. - D.
23503, vì giá âm không có trong bảng giá cha.
Câu 2. INSERT một món có price là NULL. Điều gì xảy ra?
- A. Bị từ chối
23514, vìNULLkhông lớn hơn 0. - B. Được nhận, vì
NULL > 0làUNKNOWNvàCHECKchỉ từ chốiFALSE. - C. Bị từ chối
23502, vì mọi cột cóCHECKđều ngầmNOT NULL. - D. Được nhận, và PostgreSQL tự đổi
NULLthành 0.
Câu 3. Cột size có CHECK (size IN ('S', 'M', 'L')) và có thể NULL. Dòng nào bị từ chối?
- A. Dòng
sizelàNULL, vì giá trị này không có trong danh sách. - B. Cả ba giá trị
'S','M','L', vìINđòi khớp mọi phần tử. - C. Cả hai dòng
NULLvà'XL', vì cùng không thuộc danh sách. - D. Dòng
sizelà'XL', vìFALSE, cònNULLthìUNKNOWNnên qua.
Đáp án và giải thích:
- Câu 1: C. Bài đã chạy
bad_pricevàcodes: mã23514kèm tênmenu_items_price_check. - Câu 2: B. Bài đã chạy
null_pass: dòng Trà đá vào bảng. C sai vìCHECKkhông ngầm thêmNOT NULL. - Câu 3: D.
INvớiNULLchoUNKNOWN, nên chỉ'XL'bị chặn. Muốn chặn cảNULL, thêmNOT NULL.
Làm cùng mình
Chú Tư phát phiếu giảm giá và cần bảng coupons với hai luật: phần trăm giảm từ 1 đến 90, và ngày kết thúc không sớm hơn ngày bắt đầu. Bạn viết hai CHECK, một mức cột đặt tên riêng và một mức bảng, rồi thử hai phiếu sai.
Bước 1. Vẽ ra giấy
Viết hai phiếu: OPEN10 giảm 10 phần trăm, BIG95 giảm 95 phần trăm. Dự đoán phiếu nào vào được. Rồi thêm phiếu BACK có ngày kết thúc 01/11 trước ngày bắt đầu 30/11 và dự đoán kết quả.
Bước 2. Viết lệnh
Điền hai chỗ trống bằng hai CHECK, rồi thêm hai phiếu sai:
CREATE TABLE coupons (
code text PRIMARY KEY,
percent int NOT NULL,
starts_on date NOT NULL,
ends_on date NOT NULL,
-- TODO 1: luật tên coupons_percent_range, percent từ 1 đến 90
-- TODO 2: luật mức bảng, ends_on không sớm hơn starts_on
);
INSERT INTO coupons VALUES ('OPEN10', 10, '2026-11-01', '2026-11-30');
-- TODO 3: thêm hai phiếu sai: BIG95 (95 phần trăm) và BACK (ngày kết thúc 2026-11-01, bắt đầu 2026-11-30)
SELECT code, percent FROM coupons;
Gợi ý: BETWEEN 1 AND 90 gọn hơn hai phép so sánh. Hai lệnh INSERT ở chỗ trống thứ ba phải viết thành hai câu riêng.
Bước 3. So kết quả
Đối chiếu với mục Lời giải và đáp án. Nếu cả hai phiếu sai đều vào được, hãy kiểm tra xem hai CHECK có nằm trong dấu ngoặc của CREATE TABLE không.
Thử phá nó đi
Các lỗi người mới hay gặp với CHECK [Đã chạy: PostgreSQL 16.15]. Mỗi lỗi chạy trong một lệnh riêng để thông báo hiện đúng chỗ.
Lỗi 1: tin CHECK chặn được NULL, bạn đã thấy ở null_pass.
Lỗi 2: viết truy vấn con trong CHECK để so với bảng khác.
CREATE TABLE tickets (order_id int CHECK (order_id IN (SELECT id FROM orders)));
\echo code: :LAST_ERROR_SQLSTATE
code: 0A000
ERROR: cannot use subquery in check constraint
LINE 1: ...REATE TABLE tickets (order_id int CHECK (order_id IN (SELECT...
^
Lỗi 3: thêm CHECK vào bảng orders đang có đơn mang qty bằng 0, với luật qty > 0.
ALTER TABLE orders ADD CONSTRAINT orders_qty_check CHECK (qty > 0);
\echo code: :LAST_ERROR_SQLSTATE
code: 23514
ERROR: check constraint "orders_qty_check" of relation "orders" is violated by some row
Bảng giữ nguyên và lệnh thất bại. Chỉnh luật cho khớp dữ liệu (qty >= 0) thì lệnh thành công, và UPDATE làm qty âm cũng bị chặn [Đã chạy: PostgreSQL 16.15]:
ALTER TABLE orders ADD CONSTRAINT orders_qty_check CHECK (qty >= 0);
\set VERBOSITY sqlstate
UPDATE orders SET qty = -1 WHERE id = 1;
\echo update to negative: :LAST_ERROR_SQLSTATE
SELECT id, qty FROM orders WHERE id <= 2 ORDER BY id;
ALTER TABLE
update to negative: 23514
id | qty
----+-----
1 | 5
2 | 0
(2 rows)
ERROR: 23514
Lỗi 4: không muốn ngừng ghi để dọn dữ liệu cũ. NOT VALID cho phép thêm luật ngay, dòng mới bị kiểm, dòng cũ chờ VALIDATE [Đã chạy: PostgreSQL 16.15]:
ALTER TABLE orders ADD CONSTRAINT orders_qty_positive CHECK (qty > 0) NOT VALID;
\set VERBOSITY sqlstate
INSERT INTO orders (id, customer, status, qty) VALUES (7, 'Gia', 'pending', 0);
\echo new row with qty 0: :LAST_ERROR_SQLSTATE
ALTER TABLE orders VALIDATE CONSTRAINT orders_qty_positive;
\echo validate with old bad row: :LAST_ERROR_SQLSTATE
UPDATE orders SET qty = 1 WHERE id = 2;
ALTER TABLE orders VALIDATE CONSTRAINT orders_qty_positive;
SELECT conname, convalidated FROM pg_constraint WHERE conname = 'orders_qty_positive';
ALTER TABLE
new row with qty 0: 23514
validate with old bad row: 23514
UPDATE 1
ALTER TABLE
conname | convalidated
---------------------+--------------
orders_qty_positive | t
(1 row)
ERROR: 23514
ERROR: 23514
| Code lỗi | Mã lỗi | Nguyên nhân | Cách sửa |
|---|---|---|---|
Giá trị làm biểu thức FALSE | 23514 | Luật CHECK bị vi phạm | Sửa giá trị, hoặc sửa luật nếu luật sai |
NULL lọt vào cột có CHECK | Không có lỗi | NULL cho kết quả UNKNOWN | Thêm NOT NULL vào cột |
Truy vấn con trong CHECK | 0A000 | CHECK chỉ nhìn dòng đang ghi | Dùng FOREIGN KEY, trigger hoặc kiểm ở ứng dụng |
ADD CONSTRAINT CHECK trên dữ liệu bẩn | 23514 | Dòng cũ vi phạm luật mới | Dọn dữ liệu trước, hoặc NOT VALID rồi VALIDATE |
VALIDATE khi còn dòng cũ sai | 23514 | Dòng cũ vẫn vi phạm | Sửa dòng cũ rồi chạy lại |
Hình dưới đối chiếu hiểu lầm lớn nhất về phạm vi của CHECK 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: CHECK đối chiếu được với bảng khác nên dẫn tới lỗi 0A000. Chuỗi xanh là mô hình đúng: CHECK chỉ thấy dòng đang ghi, còn luật liên bảng giao cho khóa ngoại, trigger hoặc ứng dụng.
Tự tay làm
Dùng bảng orders, menu_items và các bảng nhỏ 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 trước. Đầu vào: bảng reviews(id, stars) có CHECK (stars BETWEEN 1 AND 5), cột stars có thể NULL, rồi bốn lệnh INSERT với stars lần lượt là 5, NULL, 0 và 6. Tiêu chí đạt: nêu lệnh nào bị từ chối kèm mã lỗi, và bảng cuối có mấy dòng. Gợi ý: hỏi từng giá trị cho TRUE, FALSE hay UNKNOWN.
Vận dụng
Bài 2. Luật cho trạng thái đơn. Đầu vào: bảng orders của bài. Thêm luật để status chỉ nhận pending, paid, cancelled, refunded, closed, rồi thử INSERT một đơn có status là shipped. Tiêu chí đạt: lệnh thêm luật thành công, lệnh INSERT bị 23514, và INSERT đơn pending hợp lệ vào được. Gợi ý: dùng IN, và đặt tên luật bằng CONSTRAINT.
Thử thách (tùy chọn)
Bài 3. Dọn dữ liệu rồi siết luật. Đầu vào: bảng reviews(id, stars) có ba dòng, trong đó một dòng stars bằng 9. Thêm luật stars BETWEEN 1 AND 5 bằng NOT VALID, sửa dòng sai, rồi VALIDATE. Tiêu chí đạt: sau cùng convalidated là t và không dòng nào mất. Gợi ý: sửa dòng sai bằng UPDATE, không xóa.
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) Mã lỗi khi
CHECKbị vi phạm là gì, và tên ràng buộc mặc định của luậtprice > 0trong bảngmenu_items? - (Mục tiêu 2, giải thích) Vì sao
pricelàNULLlọt quaCHECK (price > 0), trong khiWHERE price > 0lại loại dòng đó? - (Mục tiêu 2, vận dụng) Cột cần vừa bắt buộc có giá trị vừa dương. Bạn khai báo gì?
- (Mục tiêu 3, so sánh) Vì sao
CHECK (order_id IN (SELECT id FROM orders))không dùng được, và nên dùng gì thay thế? - (Mục tiêu 4, vận dụng) Bảng 10 triệu dòng, có vài dòng cũ sai. Bạn thêm luật mới bằng cách nào để dòng mới được bảo vệ ngay?
Điều cần nhớ
CHECKtính một biểu thức trên dòng đang ghi và từ chối khi kết quả làFALSE(23514), với cảINSERTlẫnUPDATE.TRUEvàNULLđều được nhận, nênNULLlách quaCHECK; muốn bắt buộc có giá trị phải thêmNOT NULL.- Luật mức cột nhắc một cột, luật mức bảng so sánh được nhiều cột; có thể đặt tên bằng
CONSTRAINT. CHECKchỉ nhìn dòng đang ghi, không dùng truy vấn con (0A000).- Thêm
CHECKvào dữ liệu cũ: thất bại nếu có dòng sai; dùngNOT VALIDrồiVALIDATE CONSTRAINT.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
| Luật mức cột | price numeric CHECK (price > 0) | Tên <bảng>_<cột>_check |
| Đặt tên luật | CONSTRAINT tên CHECK (...) | Tên hiện trong thông báo lỗi |
| Luật nhiều cột | CHECK (sale_price <= price) | Viết ở mức bảng |
| Danh sách giá trị | CHECK (size IN ('S', 'M', 'L')) | NULL vẫn qua |
| Bắt buộc có giá trị | Thêm NOT NULL | Mã lỗi 23502 |
| Thêm vào bảng cũ | ADD CONSTRAINT ... NOT VALID | Rồi VALIDATE CONSTRAINT |
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: hai nhân viên cùng nhập khách hàng với cùng email. CHECK chỉ nhìn một dòng nên không biết dòng kia đã có email đó. Loại ràng buộc nào so sánh các dòng với nhau, và nó đối xử với NULL thế nào?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. PostgreSQL lưu luật dưới dạng biểu thức đã phân tích, nên khi in lại bạn thấy nó viết gọn theo cách của hệ thống (ví dụ
INthành= ANY). Luật mức cột và mức bảng nằm cùng danh sách [Đã chạy: PostgreSQL 16.15]:
CREATE TABLE sizes (
code text CHECK (code IN ('S', 'M', 'L')),
CHECK (code <> 'L' OR code IS NOT NULL)
);
SELECT conname, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'sizes'::regclass
ORDER BY conname;
CREATE TABLE
conname | definition
-------------------+---------------------------------------------------------------
sizes_code_check | CHECK ((code = ANY (ARRAY['S'::text, 'M'::text, 'L'::text])))
sizes_code_check1 | CHECK (((code <> 'L'::text) OR (code IS NOT NULL)))
(2 rows)
Luật mức bảng không đặt tên và chỉ nhắc một cột vẫn mang tên theo cột, ở đây là sizes_code_check1 (thêm số 1 vì sizes_code_check đã bị luật mức cột lấy), còn luật nhắc nhiều cột như coupons_check mang tên <bảng>_check. Điều đáng nhớ là cứ nên đặt tên rõ để đọc thông báo lỗi.
- So sánh với lựa chọn khác.
CHECKlàm được cả hàm tích hợp nhưchar_length. Một cột văn bản vừa không rỗng vừa không quá dài [Đã chạy: PostgreSQL 16.15]:
CREATE TABLE notes (
id int PRIMARY KEY,
body text NOT NULL CHECK (char_length(body) BETWEEN 1 AND 20)
);
INSERT INTO notes VALUES (1, 'short');
INSERT INTO notes VALUES (2, 'this note is much longer than twenty characters');
\echo too long: :LAST_ERROR_SQLSTATE
INSERT INTO notes VALUES (3, '');
\echo empty: :LAST_ERROR_SQLSTATE
SELECT id, body FROM notes ORDER BY id;
CREATE TABLE
INSERT 0 1
too long: 23514
empty: 23514
id | body
----+-------
1 | short
(1 row)
ERROR: new row for relation "notes" violates check constraint "notes_body_check"
DETAIL: Failing row contains (2, this note is much longer than twenty characters).
ERROR: new row for relation "notes" violates check constraint "notes_body_check"
DETAIL: Failing row contains (3, ).
Nếu danh sách giá trị thay đổi thường xuyên (thêm trạng thái mới), CHECK ... IN (...) đòi ALTER TABLE mỗi lần; một bảng tra cứu kết hợp khóa ngoại linh hoạt hơn [Suy luận, bài không chạy so sánh].
- Hiểu lầm nâng cao. "Luật
CHECKluôn được kiểm lại khi khôi phục dữ liệu." Không bảo đảm: PostgreSQL giả định biểu thứcCHECKluôn cho cùng kết quả với cùng dòng, nên các hàm phụ thuộc thời gian hoặc dữ liệu ngoài có thể khiến dòng cũ không còn đúng mà không ai kiểm lại [Suy luận từ tài liệu PostgreSQL, bài không chạy thử]. - Giới hạn của bài. Bài chưa nói đến
CHECKtrên cột kiểu miền (CREATE DOMAIN),NO INHERITvới bảng kế thừa, hay ràng buộc loại trừEXCLUDEcho các khoảng chồng nhau. - Đọc thêm. PostgreSQL 16 Documentation, mục Check Constraints trong Constraints: https://www.postgresql.org/docs/16/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS
Bài tiếp theo
Bài sau là UNIQUE: ràng buộc so sánh các dòng với nhau, nơi NULL cũng gây bất ngờ theo hướng khác. Bạn sẽ thấy lỗi 23505 với cột email, cách cấm trùng theo cặp cột, và UNIQUE từng phần cho dòng chưa bị xóa mềm. Mini-dự án nối tiếp: bảng khách hàng của Cà Phê Chú Tư cấm hai khách cùng email, và thử hai khách đều chưa có email.
Lời giải và đáp án
Làm cùng mình. Hai chỗ trống điền:
CONSTRAINT coupons_percent_range CHECK (percent BETWEEN 1 AND 90),
CHECK (ends_on >= starts_on)
Ở chỗ trống thứ ba viết hai câu INSERT riêng cho BIG95 và BACK. Đoạn đầy đủ thêm \echo để in mã lỗi [Đã chạy: PostgreSQL 16.15]:
CREATE TABLE coupons (
code text PRIMARY KEY,
percent int NOT NULL,
starts_on date NOT NULL,
ends_on date NOT NULL,
CONSTRAINT coupons_percent_range CHECK (percent BETWEEN 1 AND 90),
CHECK (ends_on >= starts_on)
);
INSERT INTO coupons VALUES ('OPEN10', 10, '2026-11-01', '2026-11-30');
INSERT INTO coupons VALUES ('BIG95', 95, '2026-11-01', '2026-11-30');
\echo big percent: :LAST_ERROR_SQLSTATE
INSERT INTO coupons VALUES ('BACK', 20, '2026-11-30', '2026-11-01');
\echo reversed dates: :LAST_ERROR_SQLSTATE
SELECT code, percent FROM coupons;
CREATE TABLE
INSERT 0 1
big percent: 23514
reversed dates: 23514
code | percent
--------+---------
OPEN10 | 10
(1 row)
ERROR: new row for relation "coupons" violates check constraint "coupons_percent_range"
DETAIL: Failing row contains (BIG95, 95, 2026-11-01, 2026-11-30).
ERROR: new row for relation "coupons" violates check constraint "coupons_check"
DETAIL: Failing row contains (BACK, 20, 2026-11-30, 2026-11-01).
Hai phiếu sai đều bị 23514, chỉ OPEN10 còn lại. Tên của hai luật hiện trong thông báo lỗi [Đã chạy: PostgreSQL 16.15]:
CREATE TABLE coupons (
code text PRIMARY KEY,
percent int NOT NULL,
starts_on date NOT NULL,
ends_on date NOT NULL,
CONSTRAINT coupons_percent_range CHECK (percent BETWEEN 1 AND 90),
CHECK (ends_on >= starts_on)
);
INSERT INTO coupons VALUES ('BIG95', 95, '2026-11-01', '2026-11-30');
INSERT INTO coupons VALUES ('BACK', 20, '2026-11-30', '2026-11-01');
CREATE TABLE
ERROR: new row for relation "coupons" violates check constraint "coupons_percent_range"
DETAIL: Failing row contains (BIG95, 95, 2026-11-01, 2026-11-30).
ERROR: new row for relation "coupons" violates check constraint "coupons_check"
DETAIL: Failing row contains (BACK, 20, 2026-11-30, 2026-11-01).
Luật đặt tên riêng hiện đúng coupons_percent_range. Luật mức bảng không đặt tên nhận tên tự sinh coupons_check.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
CONSTRAINT coupons_percent_range CHECK (percent BETWEEN 1 AND 90) | Luật mức bảng nhắc một cột, có tên riêng | Tên rõ nghĩa làm thông báo lỗi dễ đọc |
CHECK (ends_on >= starts_on) | So sánh hai cột trong cùng dòng | Chỉ luật mức bảng so sánh được nhiều cột |
INSERT ... ('BIG95', 95, ...) | Bị từ chối 23514 | 95 BETWEEN 1 AND 90 cho FALSE |
Bài 1.
CREATE TABLE reviews (
id int PRIMARY KEY,
stars int CHECK (stars BETWEEN 1 AND 5)
);
INSERT INTO reviews VALUES (1, 5);
INSERT INTO reviews VALUES (2, NULL);
INSERT INTO reviews VALUES (3, 0);
\echo stars 0: :LAST_ERROR_SQLSTATE
INSERT INTO reviews VALUES (4, 6);
\echo stars 6: :LAST_ERROR_SQLSTATE
SELECT id, stars FROM reviews ORDER BY id;
CREATE TABLE
INSERT 0 1
INSERT 0 1
stars 0: 23514
stars 6: 23514
id | stars
----+-------
1 | 5
2 |
(2 rows)
ERROR: new row for relation "reviews" violates check constraint "reviews_stars_check"
DETAIL: Failing row contains (3, 0).
ERROR: new row for relation "reviews" violates check constraint "reviews_stars_check"
DETAIL: Failing row contains (4, 6).
Giá trị 5 và NULL vào được, 0 và 6 bị 23514. Bảng có hai dòng, trong đó một dòng stars là NULL.
Bài 2.
ALTER TABLE orders ADD CONSTRAINT orders_status_check
CHECK (status IN ('pending', 'paid', 'cancelled', 'refunded', 'closed'));
INSERT INTO orders (id, customer, status, qty) VALUES (7, 'Gia', 'shipped', 1);
\echo shipped: :LAST_ERROR_SQLSTATE
INSERT INTO orders (id, customer, status, qty) VALUES (7, 'Gia', 'pending', 1);
SELECT count(*) AS orders_now FROM orders;
ALTER TABLE
shipped: 23514
INSERT 0 1
orders_now
------------
7
(1 row)
ERROR: new row for relation "orders" violates check constraint "orders_status_check"
DETAIL: Failing row contains (7, Gia, shipped, null, 1, null).
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
ADD CONSTRAINT orders_status_check CHECK (status IN (...)) | Thêm luật, quét sáu đơn cũ | Mọi trạng thái cũ đều nằm trong danh sách nên thành công |
VALUES (7, 'Gia', 'shipped', 1) | Bị từ chối 23514 | shipped không có trong danh sách |
VALUES (7, 'Gia', 'pending', 1) | Thành công | Bảng có bảy đơn |
Bài 3.
CREATE TABLE reviews (id int PRIMARY KEY, stars int);
INSERT INTO reviews VALUES (1, 5), (2, 9), (3, 3);
ALTER TABLE reviews ADD CONSTRAINT reviews_stars_range CHECK (stars BETWEEN 1 AND 5) NOT VALID;
UPDATE reviews SET stars = 5 WHERE stars > 5;
ALTER TABLE reviews VALIDATE CONSTRAINT reviews_stars_range;
SELECT id, stars FROM reviews ORDER BY id;
SELECT convalidated FROM pg_constraint WHERE conname = 'reviews_stars_range';
CREATE TABLE
INSERT 0 3
ALTER TABLE
UPDATE 1
ALTER TABLE
id | stars
----+-------
1 | 5
2 | 5
3 | 3
(3 rows)
convalidated
--------------
t
(1 row)
NOT VALID cho phép thêm luật dù dòng 2 đang sai. Sau khi UPDATE đưa stars về 5, VALIDATE thành công và convalidated là t.
Tự kiểm tra.
23514; tên mặc định làmenu_items_price_check.NULL > 0làUNKNOWN.CHECKchỉ từ chốiFALSEcònWHEREchỉ giữTRUE, nên hai nơi đối xửUNKNOWNngược nhau.price numeric NOT NULL CHECK (price > 0).CHECKchỉ nhìn dòng đang ghi và không cho truy vấn con (0A000). DùngFOREIGN KEYđể éporder_idtồn tại.ADD CONSTRAINT ... CHECK (...) NOT VALIDđể bảo vệ ngay dòng mới, dọn các dòng cũ sai, rồiVALIDATE CONSTRAINT.