Ràng buộc — NOT NULL, CHECK, UNIQUE, PRIMARY KEY, khóa ngoại — thường được xem thuần túy là công cụ bảo vệ dữ liệu: chặn giá trị sai lọt vào. Đúng, nhưng chưa đủ. Chúng còn là thông tin mà planner của PostgreSQL đọc để sinh kế hoạch tốt hơn. Một ràng buộc nói cho planner một sự thật đảm bảo về dữ liệu, và planner dùng sự thật đó để bỏ qua các bước thừa. Bài này đo thật ba ví dụ: một CHECK cho phép bỏ hẳn quét bảng, một PRIMARY KEY mở khóa phụ thuộc hàm, và một UNIQUE cho ước lượng chính xác.

Ràng buộc là thông tin, không chỉ là hàng rào

Điểm mấu chốt cần nhìn nhận: khi bạn khai CHECK (gia >= 0), bạn không chỉ chặn giá trị âm — bạn cam kết với planner rằng cột gia không bao giờ âm. Planner có thể tin cam kết đó tuyệt đối (khác với thống kê ANALYZE chỉ là ước lượng có thể sai). Từ cam kết đó, nó suy ra được nhiều điều để tối ưu.

Ảnh chụp đoạn mã SQL nền tối minh hoạ ràng buộc giúp planner không chỉ bảo vệ dữ liệu, ràng buộc là thông tin planner dùng để bỏ bước thừa NOT NULL CHECK UNIQUE PRIMARY KEY không chỉ chặn dữ liệu sai chúng còn cho planner biết sự thật về dữ liệu kế hoạch tốt hơn, 1 CHECK loại trừ planner chứng minh không dòng nào khớp CREATE TABLE sp gia int4 CHECK gia lớn hơn bằng 0 SET constraint_exclusion on mặc định partition chỉ áp cho phân vùng SELECT count từ sp WHERE gia nhỏ hơn 0 gia luôn lớn hơn bằng 0 nên WHERE gia nhỏ hơn 0 mâu thuẫn CHECK One-Time Filter false planner bỏ qua quét bảng hoàn toàn, 2 PRIMARY KEY phụ thuộc hàm SELECT cột không gom nhóm id là PK nên mọi cột khác được xác định bởi id SELECT id ten count v FROM t GROUP BY id hợp lệ khi id là PK không PK ERROR column ten must appear in the GROUP BY clause, 3 UNIQUE ước lượng đúng 1 dòng kể cả khi thống kê cũ SELECT sao FROM t WHERE ma bằng 500 ma UNIQUE rows 1 đảm bảo ước lượng đúng dẫn tới chọn Nested Loop index đúng cho JOIN

Hình 1: Ràng buộc cho planner biết sự thật đảm bảo về dữ liệu. CHECK cho phép loại trừ truy vấn mâu thuẫn, PRIMARY KEY cho phụ thuộc hàm, UNIQUE cho ước lượng chính xác.

Đo thật: một CHECK bỏ hẳn quét bảng

Trên bảng sp 5 triệu dòng với cột gia có ràng buộc CHECK (gia >= 0), chạy một truy vấn mâu thuẫn với ràng buộc:

Ảnh chụp bảng kết quả đo thật nền tối ràng buộc giúp planner sp 5 triệu dòng PostgreSQL 16 EXPLAIN ANALYZE shared_buffers 128MB, CHECK gia lớn hơn bằng 0 loại trừ truy vấn mâu thuẫn truy vấn WHERE gia nhỏ hơn 0 có CHECK cộng constraint_exclusion on Result One-Time Filter false 0,025 mili giây không CHECK hoặc mặc định Parallel Seq Scan cả 5 triệu dòng 40 mili giây planner chứng minh gia luôn lớn hơn bằng 0 nên WHERE gia nhỏ hơn 0 không thể khớp bỏ quét hoàn toàn khoảng 1600 lần, PRIMARY KEY phụ thuộc hàm cho phép SELECT cột không gom id là PK SELECT id ten count v FROM t GROUP BY id hợp lệ chạy không PK SELECT id ten count v FROM t GROUP BY id ERROR column ten must appear in the GROUP BY clause or be used in an aggregate function planner biết id xác định ten mỗi id một dòng nên ten không cần gom, UNIQUE ước lượng số dòng chính xác SELECT sao FROM t WHERE ma bằng 500 ma UNIQUE rows 1 đảm bảo ước lượng 1 dòng dẫn planner chọn Nested Loop index đúng khi JOIN thay vì Hash Join thừa đảm bảo này đúng kể cả khi thống kê ANALYZE đã cũ điều ước lượng thường không có, cốt lõi ràng buộc không chỉ chặn dữ liệu sai chúng là thông tin cho planner CHECK cho phép loại trừ truy vấn mâu thuẫn bỏ quét PRIMARY KEY cho phụ thuộc hàm bớt gom nhóm UNIQUE cho ước lượng chính xác chọn join đúng khai ràng buộc đầy đủ vừa an toàn dữ liệu vừa giúp tối ưu

Hình 2: WHERE gia < 0 trên bảng có CHECK (gia >= 0) với constraint_exclusion=on: planner trả Result → One-Time Filter: false trong 0,025 ms (không quét). Không có CHECK (hoặc để mặc định): Parallel Seq Scan cả 5 triệu dòng, 40 ms.

  • Có CHECK + constraint_exclusion=on: kế hoạch là Result → One-Time Filter: false — 0,025 ms. Planner chứng minh rằng vì gia luôn >= 0, điều kiện gia < 0 không thể khớp dòng nào, nên nó bỏ qua việc quét bảng hoàn toàn.
  • Không có CHECK (hoặc để mặc định): Parallel Seq Scan cả 5 triệu dòng — 40 ms.

Nhanh hơn ~1.600 lần — không phải nhờ index, mà nhờ planner suy luận từ ràng buộc. Lưu ý quan trọng: tính năng này gọi là constraint exclusion, và với bảng thường bạn phải bật SET constraint_exclusion = on (mặc định là partition, chỉ áp cho bảng phân vùng và kế thừa). Đây là công cụ mạnh cho các truy vấn sinh động có điều kiện đôi khi mâu thuẫn ràng buộc.

PRIMARY KEY: phụ thuộc hàm

Ràng buộc khóa chính cho planner (và trình phân tích cú pháp) biết một sự thật mạnh: mọi cột khác của bảng được xác định duy nhất bởi khóa chính. Điều này mở khóa "phụ thuộc hàm" trong GROUP BY:

-- id là PRIMARY KEY
SELECT id, ten, count(v) FROM t GROUP BY id;   -- HỢP LỆ (chạy)

Bạn GROUP BY id nhưng lại SELECT ten (không gom nhóm) — thường điều này là lỗi SQL. Nhưng vì id là khóa chính, PostgreSQL biết mỗi id chỉ ứng với đúng một ten, nên ten không cần nằm trong GROUP BY. Nếu id không phải khóa chính, cùng truy vấn báo lỗi:

ERROR: column "t.ten" must appear in the GROUP BY clause or be used in an aggregate function

Đây vừa là tiện lợi cú pháp vừa là dấu hiệu planner hiểu cấu trúc dữ liệu — nó có thể tối ưu gom nhóm dựa trên tính duy nhất của khóa.

UNIQUE: ước lượng chính xác

Ràng buộc UNIQUE (và PRIMARY KEY) đảm bảo mỗi giá trị xuất hiện tối đa một lần. Nên với truy vấn bằng trên cột unique, planner ước lượng chính xác 1 dòng:

SELECT * FROM t WHERE ma = 500;   -- ma UNIQUE → planner biết chắc rows=1

Ước lượng đúng dẫn tới lựa chọn kế hoạch đúng: khi JOIN, biết một phía trả về đúng 1 dòng khiến planner chọn Nested Loop với index thay vì Hash Join tốn kém. Điểm tinh tế: đảm bảo này đúng kể cả khi thống kê ANALYZE đã cũ — ước lượng từ thống kê có thể lệch sau khi dữ liệu đổi nhiều, nhưng ràng buộc UNIQUE là cam kết cứng planner luôn tin được.

Đánh đổi cần cân nhắc

Ràng buộc có chi phí ghi nhưng thường đáng. UNIQUE và PRIMARY KEY cần index (tốn ghi và dung lượng); CHECK/NOT NULL phải kiểm mỗi lần ghi. Nhưng chi phí này nhỏ, và đổi lại bạn được cả đúng đắn dữ liệu lẫn thông tin cho planner. Đừng bỏ ràng buộc để "tiết kiệm" — bạn mất cả hai lợi ích.

Khai ràng buộc đúng ngữ nghĩa, không phải để lừa planner. Ràng buộc phải phản ánh sự thật thật sự về dữ liệu. Khai NOT NULL cho cột thực ra có thể null, hay CHECK sai, sẽ khiến dữ liệu vi phạm bị từ chối (tốt) nhưng cũng có thể khiến planner suy luận sai nếu bạn lách bằng cách nạp dữ liệu vòng qua ràng buộc. Ràng buộc là hợp đồng — giữ đúng nó.

constraint_exclusion mặc định không bật cho bảng thường. Lợi ích loại trừ CHECK ở trên cần SET constraint_exclusion = on. Cân nhắc bật ở cấp truy vấn cho các báo cáo sinh động có điều kiện đôi khi mâu thuẫn ràng buộc; bật toàn cục tốn chút thời gian lập kế hoạch cho mọi truy vấn nên cân theo tải thật.

Ba ý mang về

  1. Ràng buộc là thông tin cho planner, không chỉ hàng rào dữ liệu: một CHECK (gia >= 0) cho phép planner chứng minh WHERE gia < 0 không khớp dòng nào và bỏ hẳn quét bảng — đo thật 0,025 ms so với 40 ms (~1.600×), với constraint_exclusion=on.
  2. PRIMARY KEY mở khóa phụ thuộc hàm: planner biết mọi cột được xác định bởi khóa chính, nên SELECT ten ... GROUP BY id hợp lệ khi id là PK (không PK thì báo lỗi phải đưa ten vào GROUP BY).
  3. UNIQUE cho ước lượng chính xác đảm bảo: truy vấn bằng trên cột unique ước lượng đúng 1 dòng, dẫn tới lựa chọn join đúng — và cam kết này đúng kể cả khi thống kê ANALYZE đã cũ, điều ước lượng thông thường không có.

Phần sau ta đi vào cơ chế nền tảng giải thích nhiều hành vi lưu trữ và VACUUM: Phần sau mổ xẻ MVCC — vì sao mỗi UPDATE không sửa tại chỗ mà tạo một phiên bản dòng mới, để lại dòng cũ chờ dọn, và điều đó ảnh hưởng hiệu năng ra sao.