Mô phỏng Enum bằng SMALLINT trong PostgreSQL
So sánh các cách mô phỏng enum trong PostgreSQL: SMALLINT với CHECK, khóa ngoại, ENUM gốc — ưu nhược điểm và khuyến nghị.
Khi mô hình hóa một tập nhỏ các giá trị định sẵn trong PostgreSQL, bạn có vài lựa chọn:
- Kiểu
ENUMgốc của PostgreSQL - Cột
SMALLINTvới ràng buộcCHECK - Cột
SMALLINTvới khóa ngoại tham chiếu bảng tra cứu - Cột
SMALLINTthuần chỉ được kiểm tra bởi ứng dụng
Mỗi cách có đánh đổi khác nhau về type safety, linh hoạt, hiệu năng và bảo trì.
Bài viết này so sánh chúng và giải thích khi nào nên dùng cách nào.
Vấn đề#
Giả sử đơn hàng có một trong ba trạng thái:
0 = Đang chờ
1 = Đã thanh toán
2 = Đã hủytextTa muốn lưu trạng thái hiệu quả đồng thời ngăn các giá trị không hợp lệ như:
99
-1
1234textCâu hỏi là: nên mô hình hóa điều này trong PostgreSQL như thế nào?
Cách 1: SMALLINT với ràng buộc CHECK#
Cách đơn giản nhất là lưu giá trị enum dưới dạng SMALLINT và giới hạn giá trị cho phép bằng ràng buộc CHECK.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status SMALLINT NOT NULL
CHECK (status IN (0, 1, 2))
);sqlỨng dụng định nghĩa ý nghĩa:
0 = Đang chờ
1 = Đã thanh toán
2 = Đã hủytextGiá trị không hợp lệ bị PostgreSQL từ chối:
INSERT INTO orders (status)
VALUES (99);sqlERROR: new row violates check constrainttextƯu điểm#
- Đơn giản
- Hiệu quả
- Được cơ sở dữ liệu đảm bảo hoàn toàn
- Không cần bảng phụ
- Dễ truy vấn
SELECT *
FROM orders
WHERE status = 1;sqlNhược điểm#
Ý nghĩa của giá trị không hiện ra từ chính cột:
status = 1sql1 có nghĩa là gì?
Mã ứng dụng hoặc tài liệu phải định nghĩa điều đó.
Một vấn đề khác là thay đổi giá trị cho phép yêu cầu sửa ràng buộc bảng:
ALTER TABLE orders
DROP CONSTRAINT orders_status_check;
ALTER TABLE orders
ADD CONSTRAINT orders_status_check
CHECK (status IN (0, 1, 2, 3));sqlTuy nhiên, với một tập giá trị nhỏ và ổn định, đây thường là giải pháp tuyệt vời.
Cách 2: SMALLINT không có ràng buộc CHECK#
Bạn cũng có thể định nghĩa đơn giản:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status SMALLINT NOT NULL
);sqlỨng dụng định nghĩa các giá trị:
0 = Đang chờ
1 = Đã thanh toán
2 = Đã hủytextĐây là schema cơ sở dữ liệu đơn giản nhất.
Ưu điểm#
- Schema tối thiểu
- Không cần bảo trì ràng buộc
- Rất linh hoạt
- Kích thước lưu trữ rất nhỏ
Nhược điểm#
Cơ sở dữ liệu chấp nhận mọi giá trị SMALLINT hợp lệ:
INSERT INTO orders (status)
VALUES (32767);sqlCâu lệnh này thành công.
Cơ sở dữ liệu không biết liệu 32767 có phải là trạng thái kinh doanh hợp lệ hay không.
Do đó, cách này hoàn toàn dựa vào kiểm tra ở tầng ứng dụng.
Điều đó có thể chấp nhận được khi:
- Chỉ có một ứng dụng ghi vào cơ sở dữ liệu
- Cơ sở dữ liệu được xem như chi tiết triển khai nội bộ
- Việc kiểm tra được tập trung hóa
- Bạn cố tình muốn schema cơ sở dữ liệu duy trì tính dễ dãi
Tuy nhiên, nó có thể trở nên nguy hiểm khi nhiều dịch vụ, script hoặc quản trị viên ghi trực tiếp vào cơ sở dữ liệu.
Cách 3: SMALLINT với khóa ngoại#
Một lựa chọn mạnh mẽ là dùng bảng tra cứu.
CREATE TABLE order_status (
id SMALLINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);sqlThêm các trạng thái hợp lệ:
INSERT INTO order_status (id, name)
VALUES
(0, 'pending'),
(1, 'paid'),
(2, 'cancelled');sqlSau đó tham chiếu bảng:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status SMALLINT NOT NULL
REFERENCES order_status(id)
);sqlPostgreSQL ngăn các giá trị không hợp lệ:
INSERT INTO orders (status)
VALUES (99);sqlCâu lệnh thất bại vì 99 không tồn tại trong order_status.
Ưu điểm#
- Không cần ràng buộc
CHECK - Kiểm tra ở cấp cơ sở dữ liệu
- Ý nghĩa từng giá trị được lưu trong cơ sở dữ liệu
- Dễ thêm siêu dữ liệu
- Dễ mở rộng
Ví dụ:
CREATE TABLE order_status (
id SMALLINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort_order SMALLINT NOT NULL
);sqlBảng trạng thái giờ có thể chứa siêu dữ liệu nghiệp vụ.
Nhược điểm#
- Cần thêm một bảng phụ
- Cần một khóa ngoại
- Schema phức tạp hơn một chút
- Cần quản lý dữ liệu tham chiếu
Đây thường là lựa chọn tốt nhất khi bạn muốn biểu diễn SMALLINT nhưng vẫn muốn kiểm tra mạnh ở cấp cơ sở dữ liệu mà không dùng ràng buộc CHECK.
Cách 4: ENUM gốc của PostgreSQL#
PostgreSQL cũng cung cấp kiểu enum gốc:
CREATE TYPE order_status AS ENUM (
'pending',
'paid',
'cancelled'
);sqlSau đó:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status order_status NOT NULL
);sqlCơ sở dữ liệu hiểu trực tiếp các giá trị hợp lệ:
INSERT INTO orders (status)
VALUES ('paid');sqlGiá trị không hợp lệ bị từ chối:
INSERT INTO orders (status)
VALUES ('unknown');sqlƯu điểm#
- Type safety mạnh
- Rất dễ đọc
- Kiểm tra bởi cơ sở dữ liệu
- Không cần ánh xạ số
Câu này dễ hiểu:
status = 'paid'sqlso với:
status = 1sqlNhược điểm#
Enum của PostgreSQL kém linh hoạt hơn khi tập giá trị thay đổi.
Thêm giá trị thì được:
ALTER TYPE order_status
ADD VALUE 'refunded';sqlNhưng xóa hoặc đổi tên giá trị enum thường phức tạp hơn so với sửa dòng trong bảng tra cứu.
Điều này khiến enum gốc phù hợp cho các giá trị thực sự là một phần của mô hình miền ổn định của cơ sở dữ liệu.
So sánh#
| Cách tiếp cận | Kiểm tra DB | Dùng SMALLINT | Dễ thay đổi | Lưu ý nghĩa trong DB | Độ phức tạp |
|---|---|---|---|---|---|
SMALLINT thuần | Không | Có | Rất dễ | Không | Thấp |
SMALLINT + CHECK | Có | Có | Trung bình | Một phần | Thấp |
SMALLINT + khóa ngoại | Có | Có | Dễ | Có | Trung bình |
PostgreSQL ENUM | Có | Không | Trung bình | Có | Thấp |
Nên chọn cách nào?#
Dùng SMALLINT + CHECK cho tập nhỏ, ổn định#
status SMALLINT NOT NULL
CHECK (status IN (0, 1, 2))sqlĐây là lựa chọn mặc định tốt khi:
- Giá trị ổn định
- Bạn muốn lưu trữ nhỏ gọn
- Bạn muốn kiểm tra ở cơ sở dữ liệu
- Bạn không cần thêm siêu dữ liệu
Dùng SMALLINT + khóa ngoại khi cần mở rộng#
status SMALLINT NOT NULL
REFERENCES order_status(id)sqlChọn cách này khi:
- Trạng thái có thể thay đổi
- Bạn cần mô tả hoặc siêu dữ liệu
- Bạn muốn bật/tắt trạng thái
- Bạn muốn tránh ràng buộc
CHECK - Giá trị được quản lý như dữ liệu tham chiếu
Đây thường là thiết kế linh hoạt nhất.
Dùng PostgreSQL ENUM cho kiểu miền ổn định#
CREATE TYPE order_status AS ENUM (
'pending',
'paid',
'cancelled'
);sqlĐây là lựa chọn tốt khi:
- Tập giá trị nhỏ
- Giá trị là nền tảng cho miền
- Bạn muốn SQL dễ đọc
- Bạn không mong đợi thay đổi thường xuyên
Dùng SMALLINT thuần chỉ khi kiểm tra ứng dụng là chủ ý#
status SMALLINT NOT NULLsqlCách này hoàn toàn có thể chấp nhận được, nhưng cần hiểu rõ sự đánh đổi:
Cơ sở dữ liệu sẽ không biết giá trị nào là hợp lệ.
Cách này hoạt động tốt nhất khi kiểm tra được tập trung hóa trong mã ứng dụng và việc ghi trực tiếp vào cơ sở dữ liệu được kiểm soát chặt chẽ.
Khuyến nghị thực tế#
Với hầu hết ứng dụng, tôi chọn một trong hai thiết kế sau:
Giá trị ổn định#
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status SMALLINT NOT NULL
CHECK (status IN (0, 1, 2))
);sqlGiá trị động hoặc giàu siêu dữ liệu#
CREATE TABLE order_status (
id SMALLINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status SMALLINT NOT NULL
REFERENCES order_status(id)
);sqlĐiểm khác biệt chính rất đơn giản:
Dùng ràng buộc
CHECKkhi giá trị hợp lệ là bất biến của bảng. Dùng bảng tra cứu khi giá trị hợp lệ là dữ liệu có thể tiến hóa. DùngENUMcủa PostgreSQL khi giá trị là kiểu cơ sở dữ liệu ổn định.
Và nếu bạn chọn SMALLINT thuần không có ràng buộc cơ sở dữ liệu, hãy làm điều đó một cách có chủ đích — bởi vì bạn đang chuyển trách nhiệm về tính toàn vẹn dữ liệu từ PostgreSQL sang ứng dụng.