Một mã tham chiếu được phép để trống nhưng phải duy nhất khi có giá trị. Bạn thêm UNIQUE rồi ngạc nhiên vì hai dòng có NULL vẫn được lưu. Với PostgreSQL mặc định, điều đó có thể đúng với thiết kế của constraint; UNIQUE và NOT NULL đang giải quyết hai yêu cầu khác nhau.
Bài đối chiếu PostgreSQL 17 ngày 30/10/2026. Fixture UNIQUE mặc định được chạy trên SQLite CLI 3.54.0 để quan sát các ca cơ bản. Cú pháp NULLS NOT DISTINCT được rà từ tài liệu PostgreSQL 17, chưa chạy trên PostgreSQL server; không dùng SQLite để khẳng định đã kiểm thử tính năng riêng này.
Viết yêu cầu bằng ba câu riêng
Với mã liên kết tới hệ thống ngoài, có thể có ba hợp đồng: chưa có mã thì cho phép nhiều dòng NULL; chưa có mã thì chỉ cho phép tối đa một dòng NULL; hoặc mọi dòng đều bắt buộc có mã. Cả ba đều hợp lý trong những mô hình khác nhau, nhưng không dùng chung một constraint.
Constraints của PostgreSQL 17 ghi rằng UNIQUE mặc định không coi hai NULL là trùng trong phép kiểm tra tính duy nhất. Đừng suy diễn từ đó rằng biểu thức SQL NULL = NULL trả true; ngữ nghĩa so sánh và quy tắc constraint cần được đọc đúng chỗ.
Fixture: cho phép nhiều dòng chưa có mã
Chạy phần tạo bảng trong database thử nghiệm riêng:
CREATE TABLE external_links_demo (
id INTEGER PRIMARY KEY,
external_ref TEXT UNIQUE
);
INSERT INTO external_links_demo VALUES
(1, NULL),
(2, NULL),
(3, 'REF-A');
SELECT id, COALESCE(external_ref, '(missing)') AS display_ref
FROM external_links_demo
ORDER BY id;
Kết quả đã quan sát trên SQLite là 1|(missing), 2|(missing), 3|REF-A. COALESCE ở đây chỉ giúp terminal hiển thị NULL rõ hơn, không thay đổi dữ liệu được lưu hay cách constraint hoạt động.
Sau đó thử riêng câu lệnh sau, vì đây là ca cố ý gây lỗi:
INSERT INTO external_links_demo VALUES (4, 'REF-A');
SQLite báo lỗi UNIQUE và không thêm dòng đó. Hai NULL được phép không có nghĩa mọi giá trị trùng đều được phép. Tài liệu CREATE TABLE của SQLite xác nhận quy tắc NULL cho UNIQUE của engine này; các engine khác cần kiểm tra tài liệu riêng.
Khi NULL cũng chỉ được xuất hiện một lần
Với PostgreSQL 17, có thể khai báo chính sách rõ ràng trên một bảng khác:
CREATE TABLE single_missing_demo (
id INTEGER PRIMARY KEY,
external_ref TEXT,
UNIQUE NULLS NOT DISTINCT (external_ref)
);
Theo hợp đồng của constraint, NULL đầu tiên có thể được lưu, NULL thứ hai vi phạm tính duy nhất. Hai giá trị non-NULL giống nhau cũng vi phạm. Đây là kết quả kỳ vọng theo tài liệu PostgreSQL, không phải log thực thi PostgreSQL trong bài.
Trước khi chọn chính sách này cho mã tham chiếu tùy chọn, hỏi lại mô hình dữ liệu: vì sao toàn bảng chỉ được có một đối tượng chưa nhận mã? Nếu nhiều bản ghi đang chờ đồng bộ cùng được phép thiếu mã, default UNIQUE thường diễn đạt yêu cầu đó trực tiếp hơn.
Khi mã là bắt buộc
CREATE TABLE required_ref_demo (
id INTEGER PRIMARY KEY,
external_ref TEXT NOT NULL UNIQUE
);
NOT NULL loại trường hợp thiếu mã; UNIQUE ngăn mã trùng. Bài đã thử thêm NULL vào schema này trên SQLite và nhận lỗi NOT NULL. Chỉ đổi sang NULLS NOT DISTINCT không thay thế được yêu cầu bắt buộc có giá trị, vì chính sách ấy vẫn cho phép một NULL.
Chuỗi rỗng '' cũng không phải NULL. Cột text NOT NULL vẫn có thể nhận chuỗi rỗng nếu không có quy tắc khác. Quyết định trim, chuẩn hóa hoặc từ chối mã rỗng cần thống nhất với API; đừng biến NULL thành chuỗi rỗng trong client chỉ để vượt một bước validation.
Checklist trước migration
Đếm bản ghi NULL, tìm nhóm non-NULL trùng và kiểm tra dữ liệu được chuẩn hóa ra sao. Nếu dùng constraint nhiều cột, thử cả hai dòng có cùng tenant nhưng một phần khóa là NULL; không lấy kết quả của một cột để suy đoán mọi khóa ghép.
Khi thêm ràng buộc vào bảng thật, chuẩn bị cách xử lý dữ liệu đang vi phạm và đánh giá tác động khóa theo kế hoạch migration của dự án. CREATE INDEX của PostgreSQL còn mô tả tùy chọn NULLS DISTINCT/NOT DISTINCT ở mức index. Bài không đưa một lệnh ALTER chạy thẳng trên production hay đo thời gian xây index.
Giữ constraint trong database làm nguồn bảo vệ cuối cùng. Một lượt “SELECT xem có mã chưa” trong API không ngăn hai request đồng thời cùng vượt qua kiểm tra. Khi database từ chối, API cần chuyển thành phản hồi nghiệp vụ thích hợp mà không bỏ qua lỗi.
Lỗi thường gặp và giới hạn
- Nghĩ UNIQUE đồng nghĩa trường bắt buộc nhập.
- Dùng một NULL làm sentinel cho mọi giá trị chưa có mà không xem chính sách duy nhất.
- Kiểm thử trên SQLite rồi áp dụng nguyên cú pháp riêng PostgreSQL.
- Đồng nhất NULL, chuỗi rỗng và chuỗi chỉ có khoảng trắng.
Chốt điều kiện thiếu giá trị trước, rồi mới chọn constraint. Một schema tốt phải thể hiện đúng những dòng nào được phép cùng tồn tại, kể cả khi dữ liệu chưa hoàn chỉnh.

