PostgreSQL Bài 8: Kiểu dữ liệu nâng cao: UUID, INET, MACADDR và khi nào nên dùng
Ở Bài 6 và 7, chúng ta đã đi qua các kiểu dữ liệu và ràng buộc chuẩn hóa. Tuy nhiên, khi hệ thống mở rộng sang kiến trúc phân tán (Microservices, Event-Driven), hoặc các bài toán an ninh mạng, IoT và viễn thông, các kiểu dữ liệu cổ điển như BIGINT hay VARCHAR bộc lộ nhiều điểm hạn chế.
Thay vì lưu địa chỉ IP hay mã định danh dưới dạng chuỗi VARCHAR, PostgreSQL cung cấp các kiểu dữ liệu chuyên biệt: UUID, INET / CIDR và MACADDR. Bài học này sẽ mổ xẻ cơ chế lưu trữ nội bộ, hiệu năng và các cạm bẫy thực chiến khi sử dụng chúng.
1. Kiểu UUID: Mã định danh toàn cục & Bài toán Phân mảnh Index
UUID (Universally Unique Identifier) là chuỗi số 128-bit được thiết kế để đảm bảo tính duy nhất trên toàn cầu mà không cần một máy chủ điều phối trung tâm.
1.1. Bản chất lưu trữ: UUID vs VARCHAR(36)
Nhiều người có thói quen khai báo VARCHAR(36) để lưu UUID (ví dụ: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11).
| Tiêu chí | Kiểu UUID trong PostgreSQL | Kiểu VARCHAR(36) |
|---|---|---|
| Dung lượng lưu trữ | Đúng 16 bytes (Lưu dưới dạng nhị phân 128-bit) | 37 bytes (36 ký tự ASCII + 1 byte header) |
| Dung lượng Index (B-Tree) | Gọn nhẹ hơn 2.3 lần | Cồng kềnh, nhanh chóng làm tràn RAM |
| So sánh / Sắp xếp | So sánh số nguyên 128-bit cực nhanh | So sánh chuỗi ký tự theo Collation (chậm) |
| Kiểm tra tính hợp lệ | Engine tự động chặn chuỗi sai định dạng | Cho phép nhét chuỗi rác bất kỳ |
Quy tắc: Luôn dùng kiểu dữ liệu gốc
UUID, không bao giờ lưu UUID bằngVARCHARhoặcTEXT.
1.2. Cạm bẫy hiệu năng của UUID v4 (Random) trên Primary Key
Trong ứng dụng backend, UUID v4 (tạo ngẫu nhiên hoàn toàn) rất được ưa chuộng để tránh lộ số lượng bản ghi (tránh ID tự tăng dễ bị cào dữ liệu qua API). Tuy nhiên, nếu dùng UUID v4 làm Clustered / Primary Key trên bảng hàng chục triệu dòng, bạn sẽ gặp thảm họa hiệu năng mang tên: B-Tree Index Fragmentation & Cache Churn.
Cơ chế chèn B-Tree với ID tuần tự (BIGINT / UUID v7):
Dữ liệu mới luôn ghi vào trang lá cuối cùng (Right-most leaf page)
[Page 1] ──► [Page 2] ──► [Page 3 (Ghi liên tục tại đây)] ──► RAM Cache luôn ấm (Hot Page)
Cơ chế chèn B-Tree với UUID v4 (Ngẫu nhiên hoàn toàn):
Ghi ngẫu nhiên vào bất kỳ trang lá nào trong cây B-Tree
┌──────────────┬──────────────┬──────────────┐
│ [Page 1] │ [Page 2] │ [Page 3] │ ──► Gây Page Split (chẻ trang) liên tục
└──────▲───────┴──────▲───────┴──────▲───────┘
│ │ │
UUID_v4 UUID_v4 UUID_v4 ──► Đẩy các trang hữu ích ra khỏi shared_buffers (I/O đĩa tăng vọt)
- Hậu quả: Khi cây Index phình to vượt quá kích thước RAM (
shared_buffers), mỗi thao tácINSERTmột bản ghi mới đều phải đọc một trang index ngẫu nhiên từ ổ đĩa lên RAM để chèn, sau đó lại ghi xuống. Tốc độ ghi sẽ sụt giảm từ hàng chục nghìn dòng/giây xuống còn vài trăm dòng/giây.
1.3. Giải pháp hiện đại: Sử dụng UUID v7 (Time-Ordered UUID)
Khắc phục triệt để nhược điểm của v4, UUID v7 tích hợp một timestamp (độ chính xác mili-giây) ở các bit đầu tiên, phần còn lại là entropy ngẫu nhiên.
- Ưu điểm: Vừa đảm bảo tính duy nhất ngẫu nhiên toàn cục, vừa sắp xếp tuần tự theo thời gian (k-sortable), giúp B-Tree Index ghi tuần tự tương tự như
BIGINT.
Cách triển khai UUID trong PostgreSQL:
SQL
-- Cách 1: Sử dụng UUID v4 truyền thống (Cần extension pgcrypto hoặc uuid-ossp trên PG cũ)
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key_name TEXT NOT NULL
);
-- Cách 2: Tạo hàm sinh UUID v7 thuần trong PostgreSQL
CREATE OR REPLACE FUNCTION generate_uuid_v7()
RETURNS UUID AS $$
DECLARE
unix_time_ms BYTEA;
uuid_bytes BYTEA;
BEGIN
-- Lấy thời gian hiện tại đổi sang mili-giây (48 bits đầu)
unix_time_ms := substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3 for 6);
-- 10 bytes còn lại lấy ngẫu nhiên
uuid_bytes := unix_time_ms || gen_random_bytes(10);
-- Set version = 7 (0111) tại byte thứ 7
uuid_bytes := set_byte(uuid_bytes, 6, (get_byte(uuid_bytes, 6) & 15) | 112);
-- Set variant = RFC 4122 (10xx) tại byte thứ 9
uuid_bytes := set_byte(uuid_bytes, 8, (get_byte(uuid_bytes, 8) & 63) | 128);
RETURN encode(uuid_bytes, 'hex')::uuid;
END;
$$ LANGUAGE plpgsql VOLATILE;
-- Ứng dụng UUID v7 làm Primary Key tối ưu ghi:
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT generate_uuid_v7(),
event_type VARCHAR(50) NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
2. Kiểu dữ liệu Mạng: INET và CIDR
Khi xây dựng các bảng audit_logs, chặn IP (blacklist), hoặc kiểm soát truy cập phân vùng mạng, việc dùng VARCHAR(45) để lưu IP là một sai lầm lớn. PostgreSQL hỗ trợ 2 kiểu dữ liệu mạng nguyên bản: INET và CIDR.
2.1. Phân biệt INET vs CIDR
-
INET(7 hoặc 19 bytes): Dùng để lưu trữ địa chỉ của một host cụ thể, có thể đi kèm subnet mask (ví dụ:192.168.1.100/24hoặc2001:db8::1).- Cho phép các bit host nằm ngoài subnet mask có giá trị khác 0.
-
CIDR(7 hoặc 19 bytes): Dùng để lưu trữ toàn bộ một dải mạng.- Nghiêm ngặt hơn: Nếu bạn khai báo subnet mask là
/24, tất cả các bit phía sau bắt buộc phải bằng 0 (ví dụ192.168.1.0/24). Nếu bạn nhập192.168.1.100/24, PostgreSQL sẽ ném lỗi ngay lập tức.
- Nghiêm ngặt hơn: Nếu bạn khai báo subnet mask là
2.2. Các toán tử chuyên biệt cực mạnh của INET
PostgreSQL cho phép bạn thực hiện các phép toán mạng ngay trong câu lệnh SQL mà không cần xử lý bằng code backend:
SQL
CREATE TABLE security_firewall (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
rule_name TEXT NOT NULL,
blocked_network CIDR NOT NULL
);
INSERT INTO security_firewall (rule_name, blocked_network) VALUES
('Block Local Subnet', '192.168.1.0/24'),
('Block Corporate Range', '10.200.0.0/16');
Truy vấn kiểm tra xem một IP có thuộc dải mạng bị chặn hay không bằng toán tử chứa << (is contained by) hoặc >> (contains):
SQL
-- Kiểm tra xem IP 192.168.1.55 có nằm trong danh sách đen không:
SELECT * FROM security_firewall
WHERE '192.168.1.55'::INET << blocked_network;
-- Lấy netmask, broadcast và địa chỉ IP gốc:
SELECT
ip,
host(ip) AS host_address, -- Lấy dạng chuỗi thuần không kèm subnet mask
netmask(ip) AS subnet_mask, -- Trích xuất Netmask
broadcast(ip) AS broadcast_ip -- Tính địa chỉ Broadcast tự động
FROM (VALUES ('192.168.1.50/24'::INET)) AS t(ip);
Kết quả trả về:
Plaintext
ip | host_address | subnet_mask | broadcast_ip
-----------------+--------------+---------------+-----------------
192.168.1.50/24 | 192.168.1.50 | 255.255.255.0 | 192.168.1.255/24
Hỗ trợ Index vượt trội: Bạn có thể đánh index kiểu GiST hoặc SP-GiST trên cột
INETđể tối ưu các truy vấn kiểm tra dải mạng (<<,>>,&&), đem lại tốc độ tra cứu tức thì trên hàng triệu dòng log.
3. Kiểu địa chỉ phần cứng: MACADDR và MACADDR8
Trong các hệ thống quản lý thiết bị, viễn thông, IoT hoặc theo dõi thiết bị mạng (DHCP server, Network switches):
-
MACADDR(6 bytes): Lưu địa chỉ MAC phần cứng chuẩn EUI-48 (ví dụ:08:00:2b:01:02:03). -
MACADDR8(8 bytes): Lưu địa chỉ MAC chuẩn EUI-64 (dùng cho kiến trúc mạng thế hệ mới, Zigbee, IPv6 interface ID).
Ưu điểm vượt trội so với VARCHAR(17):
-
Tiết kiệm dung lượng: Chỉ tốn đúng 6 bytes thay vì 18 bytes khi dùng
VARCHAR(17). -
Khả năng chuẩn hóa thông minh: PostgreSQL chấp nhận nhiều định dạng nhập vào khác nhau và tự động chuẩn hóa về một định dạng thống nhất:
SQL
CREATE TABLE network_devices (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device_name VARCHAR(100),
mac_address MACADDR NOT NULL UNIQUE
);
-- Cả 4 định dạng sau đều được chấp nhận và lưu cùng một giá trị 6-byte:
INSERT INTO network_devices (device_name, mac_address) VALUES
('Switch Core 01', '08:00:2b:01:02:03'),
('Sensor Temp 02', '08-00-2b-01-02-04'),
('Gateway 03', '08002b:010205'),
('Router 04', '0800.2b01.0206');
SELECT * FROM network_devices;
Kết quả:
Plaintext
id | device_name | mac_address
----+----------------+-------------------
1 | Switch Core 01 | 08:00:2b:01:02:03
2 | Sensor Temp 02 | 08:00:2b:01:02:04
3 | Gateway 03 | 08:00:2b:01:02:05
4 | Router 04 | 08:00:2b:01:02:06
4. Bảng tổng kết kịch bản lựa chọn
| Kiểu dữ liệu | Dung lượng | Trường hợp nên dùng | Tuyệt đối tránh |
|---|---|---|---|
| UUID (v7) | 16 bytes | Primary Key cho Microservices, Distributed Systems, ID public qua API. | Dùng UUID v4 làm Clustered/PK trên bảng ghi dữ liệu tần suất cao. |
| INET | 7 hoặc 19 bytes | Lưu IP Client, User Activity Logs, Kiểm tra IP có nằm trong Subnet hay không. | Dùng VARCHAR(45) (không hỗ trợ toán tử subnet, tốn RAM). |
| CIDR | 7 hoặc 19 bytes | Thiết lập dải mạng, Rule tường lửa, cấu hình phân vùng mạng LAN/VPC. | Lưu IP của một thiết bị đơn lẻ mà không có ý nghĩa dải mạng. |
| MACADDR | 6 bytes | Quản lý thiết bị IoT, Inventory hạ tầng mạng, bảng ARP/DHCP leases. | Dùng VARCHAR(17). |
5. Tóm tắt & Bài tiếp theo
-
Kiểu
UUIDlưu trữ 128-bit nhị phân (16 bytes), nhỏ hơn và so sánh nhanh hơn gấp nhiều lầnVARCHAR(36). -
Tránh dùng
UUID v4cho Primary Key của bảng lớn do gây phân mảnh B-Tree; hãy thay thế bằngUUID v7có tích hợp thời gian. -
INETvàCIDRcung cấp các toán tử mạng nguyên bản (<<,>>) và hỗ trợ index chuyên dụng để lọc IP tức thì. -
MACADDRtự động chuẩn hóa định dạng địa chỉ vật lý và chỉ tiêu tốn 6 bytes lưu trữ.
Bài 9 xem tiếp: Xử lý chuỗi và Văn bản: So sánh
VARCHAR,TEXT,CHARvà cách hoạt động của TOAST — chúng ta sẽ giải mã cơ chế TOAST (The Oversized-Attribute Storage Technique), cách PostgreSQL "lách" giới hạn trang 8KB để lưu trữ các văn bản hoặc tệp nhị phân lên tới 1GB mà không làm chậm việc quét bảng.
All rights reserved