0

PostgreSQL Bài 6: Hệ thống kiểu dữ liệu cơ bản: Numeric, String, Boolean, Date/Time và Timezone handling

Việc lựa chọn kiểu dữ liệu (Data Type) trong thiết kế lược đồ không chỉ đơn thuần là để lưu trữ được thông tin, mà còn tác động trực tiếp đến:

  1. Dung lượng đĩa và bộ nhớ cache: Càng tốn ít byte, càng chứa được nhiều dòng trong shared_buffers.

  2. Hiệu năng truy vấn & Index: Các kiểu dữ liệu có kích thước cố định, đơn giản luôn được tính toán và so sánh nhanh hơn.

  3. Tính chính xác của nghiệp vụ: Tránh các lỗi sai số làm tròn tài chính hoặc lệch múi giờ giao dịch xuyên biên giới.

Bài học này đi sâu vào 4 nhóm kiểu dữ liệu nền tảng nhất trong PostgreSQL cùng các quy tắc thực chiến không thể bỏ qua.

1. Nhóm số (Numeric Types): Số nguyên, Số thực và Bài toán Tiền tệ

PostgreSQL cung cấp hai trường phái lưu trữ số: Chính xác tuyệt đối (Exact/Arbitrary Precision) và Gần đúng (Inexact/Floating-point).

1.1. Họ số nguyên (Integers) Trong PostgreSQL / RDBMS Hiện Đại

Kiểu dữ liệu Dung lượng Phạm vi giá trị Ứng dụng thực tế
SMALLINT 2 bytes −32,768-32,768 đến +32,767+32,767 Trạng thái đơn hàng, enum số, mã lỗi, tháng/năm ngắn hạn.
INTEGER (INT) 4 bytes ∼−2.14\sim -2.14 tỷ đến +2.14+2.14 tỷ Khóa ngoại, số lượng tồn kho, lượt xem trang vừa phải.
BIGINT 8 bytes ∼−9.22×1018\sim -9.22 \times 10^{18} đến +9.22×1018+9.22 \times 10^{18} Primary Key chuẩn, bảng lịch sử log, audit trail, transactions.

Khóa tự tăng: Tránh SERIAL, hãy dùng Identity Columns chuẩn SQL: Trước đây PostgreSQL dùng SERIAL / BIGSERIAL (tạo một sequence ngầm). Tuy nhiên, từ PostgreSQL 10+, hãy dùng cú pháp chuẩn ANSI SQL:

SQL

-- Chuẩn khuyến nghị hiện đại:
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY

Ưu điểm: Ngăn chặn việc vô tình INSERT giá trị thủ công đè lên sequence nếu không có mệnh đề OVERRIDING SYSTEM VALUE.

1.2. Số thực và Tiền tệ: Cạm bẫy giữa FLOAT và NUMERIC

SQL

-- Thử nghiệm tính toán dấu phẩy động (Floating point):
SELECT 0.1::FLOAT8 + 0.2::FLOAT8 = 0.3::FLOAT8 AS result;
-- Kết quả: FALSE (Do 0.1 + 0.2 trong chuẩn IEEE 754 là 0.30000000000000004)

  • REAL (4 bytes) / DOUBLE PRECISION (hoặc FLOAT8 - 8 bytes):

    • Lưu trữ theo chuẩn nhị phân IEEE 754.

    • Tốc độ tính toán trên CPU cực nhanh.

    • Nhược điểm: Có sai số làm tròn. Chỉ dùng cho tọa độ địa lý, thông số cảm biến IoT, dữ liệu đồ họa, tính toán khoa học.

  • NUMERIC(precision, scale) (hoặc DECIMAL):

    • Lưu trữ chính xác tuyệt đối từng con số dưới dạng chuỗi số byte nén.

    • precision: Tổng số chữ số (cả trước và sau dấu phẩy).

    • scale: Số chữ số sau dấu thập phân.

    • Ví dụ: NUMERIC(15, 2) lưu được tối đa 999,999,999,999.99.

    • Quy tắc bắt buộc: Luôn dùng NUMERIC cho tiền tệ, giá trị tài chính, lãi suất ngân hàng. Tuyệt đối không dùng FLOAT để lưu tiền.

2. Nhóm chuỗi ký tự (String Types): Sự thật về VARCHAR vs TEXT

Trong các hệ quản trị như MySQL hay Oracle, bạn thường phải cân nhắc kỹ việc đặt VARCHAR(50), VARCHAR(255) vì nó ảnh hưởng đến việc phân bổ bộ nhớ đệm hay cấu trúc row. Nhưng trong PostgreSQL thì khác.

2.1. Bản chất lưu trữ trong PostgreSQL

Kiểu dữ liệu Mô tả
CHAR(n) Chuỗi có độ dài cố định. Nếu chuỗi ngắn hơn n, PostgreSQL sẽ tự động đệm thêm khoảng trắng (space-padded). Hầu như không nên dùng vì tốn dung lượng vô ích và dễ sinh lỗi so sánh chuỗi.
VARCHAR(n) Chuỗi có độ dài tối đa là n. Ném lỗi nếu độ dài vượt quá giới hạn.
VARCHAR Không giới hạn độ dài khai báo (hoạt động giống TEXT).
TEXT Chuỗi có độ dài thay đổi, tối đa 1GB cho mỗi giá trị.

Sự thật hiệu năng: Trong PostgreSQL engine, VARCHAR(n), VARCHAR và TEXT sử dụng cùng một cấu trúc lưu trữ nội bộ (internal storage structure: varlena). Việc truy vấn hay đánh index trên cột TEXT không hề chậm hơn VARCHAR(50). Giới hạn (n) trong VARCHAR(n) chỉ đóng vai trò là một lớp kiểm tra ràng buộc độ dài (Constraint check) trước khi ghi.

Quy tắc thiết kế:

  • Nếu nghiệp vụ có giới hạn cứng logic rõ ràng (ví dụ: Mã bưu chính 10 ký tự, Mã định danh CCCD 12 số): Dùng VARCHAR(n).

  • Các trường nội dung mô tả, ghi chú, địa chỉ, bài viết: Dùng thẳng TEXT.

3. Nhóm Logic (Boolean Type)

PostgreSQL hỗ trợ kiểu BOOLEAN (hoặc BOOL) với dung lượng lưu trữ 1 byte.

  • Trạng thái 3 giá trị (Three-valued logic): Một cột boolean có thể nhận 3 giá trị: TRUE, FALSE, hoặc NULL (chưa xác định / Unknown).

  • Hỗ trợ chuỗi linh hoạt khi nạp dữ liệu:

    • True: 'true', 't', 'yes', 'y', '1'.

    • False: 'false', 'f', 'no', 'n', '0'.

SQL

CREATE TABLE account_flags (
    user_id BIGINT PRIMARY KEY,
    is_active BOOLEAN NOT NULL DEFAULT TRUE, -- Khuyến nghị thêm NOT NULL để tránh logic 3 giá trị phức tạp
    is_verified BOOLEAN DEFAULT NULL
);

4. Nhóm Ngày tháng & Thời gian: Xử lý Timezone chuẩn xác

Đây là khu vực phát sinh nhiều lỗi nhất trong các hệ thống backend khi có người dùng truy cập từ nhiều múi giờ khác nhau.

4.1. Phân biệt TIMESTAMP vs TIMESTAMPTZ

PostgreSQL cung cấp 2 kiểu thời gian kèm ngày:

  • TIMESTAMP WITHOUT TIME ZONE (hay viết tắt là TIMESTAMP):

    • Chiếm 8 bytes.

    • Lưu nguyên vẹn chuỗi thời gian được đưa vào mà không quan tâm múi giờ của client hay server.

    • Hiểm họa: Nếu server ở UTC lưu 2026-10-08 15:00:00, và một user ở Việt Nam (UTC+7) cũng đọc chuỗi đó thì hệ thống không biết 15:00 đó là giờ của ai.

  • TIMESTAMP WITH TIME ZONE (hay viết tắt là TIMESTAMPTZ):

    • Chiếm 8 bytes.

    • Cơ chế lưu trữ: PostgreSQL luôn luôn chuyển đổi thời gian đầu vào về chuẩn UTC để lưu vật lý xuống ổ đĩa.

    • Cơ chế hiển thị: Khi client truy vấn, PostgreSQL lấy mốc giờ UTC từ đĩa và tự động chuyển đổi sang múi giờ hiện tại của phiên kết nối (TimeZone session setting).

SQL

-- Kiểm tra sự khác biệt thực tế:
SET timezone TO 'Asia/Ho_Chi_Minh'; -- UTC+7

CREATE TABLE event_logs (
    ts_without TIMESTAMP,
    ts_with TIMESTAMPTZ
);

INSERT INTO event_logs VALUES 
('2026-10-08 12:00:00+00', '2026-10-08 12:00:00+00');

SELECT * FROM event_logs;

Kết quả trả về:

Plaintext

        ts_without        |        ts_with         
--------------------------+------------------------
 2026-10-08 12:00:00      | 2026-10-08 19:00:00+07

ts_with đã được chuyển sang giờ Việt Nam (+7 tiếng) một cách chuẩn xác, trong khi ts_without vứt bỏ thông tin múi giờ.

Quy tắc vàng cho Backend: Luôn luôn sử dụng TIMESTAMPTZ cho các mốc thời gian diễn ra sự kiện (created_at, updated_at, order_date, login_time). Chỉ dùng TIMESTAMP WITHOUT TIME ZONE khi đại diện cho một khái niệm thời gian trừu tượng không phụ thuộc địa lý (ví dụ: giờ mở cửa cố định của chuỗi cửa hàng: "8:00 AM mỗi ngày").

4.2. Kiểu khoảng thời gian: INTERVAL

Dùng để thực hiện các phép cộng trừ thời gian cực kỳ mạnh mẽ mà không cần viết logic phức tạp ở code ứng dụng:

SQL

-- Lấy tất cả user không hoạt động trong 30 ngày qua
SELECT * FROM users 
WHERE last_login < CURRENT_TIMESTAMP - INTERVAL '30 days';

-- Cộng thêm 2 giờ 15 phút
SELECT CURRENT_TIMESTAMP + INTERVAL '2 hours 15 minutes';

5. Thực hành: Bảng đơn hàng hoàn chỉnh tối ưu kiểu dữ liệu

Áp dụng toàn bộ kiến thức vào một bảng thực tế:

SQL

CREATE TABLE orders.purchase_orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 8 bytes, an toàn, không lo tràn số
    order_code VARCHAR(32) NOT NULL UNIQUE,             -- Giới hạn độ dài nghiệp vụ
    buyer_id BIGINT NOT NULL,                           -- Foreign key chuẩn
    subtotal NUMERIC(14, 2) NOT NULL,                   -- Không bao giờ dùng FLOAT cho tiền tệ
    discount_percentage NUMERIC(5, 2) DEFAULT 0.00,
    is_paid BOOLEAN NOT NULL DEFAULT FALSE,             -- 1 byte
    placed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,    -- Luôn lưu mốc thời gian kèm múi giờ
    estimated_delivery DATE NOT NULL,                   -- Chỉ cần ngày (4 bytes)
    notes TEXT                                          -- Độ dài động, không giới hạn giả tạo
);

6. Tóm tắt các quy tắc cốt lõi

  1. Số nguyên: Dùng BIGINT GENERATED ALWAYS AS IDENTITY cho Primary Key. Dùng SMALLINT cho mã trạng thái/enum.

  2. Tiền tệ & Tài chính: Tuyệt đối dùng NUMERIC(p, s). Bỏ qua FLOAT / DOUBLE PRECISION để tránh sai số làm tròn.

  3. Chuỗi: TEXT và VARCHAR(n) có hiệu năng tương đương nhau bên trong Postgres; chỉ dùng VARCHAR(n) khi cần kiểm tra giới hạn độ dài nghiệp vụ. Tránh dùng CHAR(n).

  4. Thời gian: Mặc định chọn TIMESTAMPTZ cho toàn bộ các trường thời gian sự kiện (created_at, updated_at) để dữ liệu luôn được quy về UTC chuẩn xác.

Bài 7 xem tiếp: Thiết kế bảng chuẩn hóa, Ràng buộc dữ liệu (Constraints) và Cascade Rules — bước sang Phần 2, chúng ta sẽ tìm hiểu cách thiết kế lược đồ quan hệ vững chắc với CHECK, UNIQUE, FOREIGN KEY, cơ chế ON DELETE CASCADE vs RESTRICT, và kỹ thuật kiểm soát toàn vẹn dữ liệu ở cấp độ database engine.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí