Giới thiệu kiến trúc PostgreSQL

PostgreSQL là một hệ quản trị cơ sở dữ liệu quan hệ đối tượng mã nguồn mở đã được phát triển liên tục hơn 30 năm. Đối với lập trình viên backend, việc hiểu PostgreSQL vận hành ra sao bên dưới sẽ giúp bạn viết truy vấn nhanh hơn, thiết kế lược đồ tốt hơn và xử lý hiệu quả các vấn đề về hiệu năng.

Kiến trúc lõi gồm nhiều tầng phối hợp với nhau. Ở tầng trên cùng, các ứng dụng client kết nối qua socket mạng bằng PostgreSQL wire protocol. Kết nối đầu tiên được tiếp nhận bởi tiến trình postmaster, tiến trình này sẽ fork một backend process mới cho mỗi kết nối client. Mô hình một tiến trình cho mỗi kết nối mang lại khả năng cô lập mạnh mẽ, nhưng cũng đồng nghĩa rằng bạn nên sử dụng connection pooling (ví dụ PgBouncer) cho các ứng dụng có độ đồng thời cao.

Khi một truy vấn đến backend process, nó sẽ đi qua query processor: parser kiểm tra cú pháp, analyzer/rewriter áp dụng các quy tắn và xác thực quyền, planner sinh ra execution plan tối ưu, và executor chạy plan đó trên storage layer. Storage layer sử dụng cấu trúc heap file, nơi các dòng dữ liệu được ghi và đọc, cùng với các file index riêng biệt (thường là B-tree) cung cấp khả năng tra cứu nhanh trên các cột được đánh index.

Thiết kế lược đồ quan hệ chuẩn hóa

Thiết kế quan hệ là cách thực hành tổ chức các bảng nhằm giảm thiểu dư thừa dữ liệu và bảo vệ tính toàn vẹn của dữ liệu. Khái niệm nền tảng là chuẩn hóa (normalization) — một tập hợp các quy tắc (dạng chuẩn) hướng dẫn cách tách dữ liệu thành các bảng có quan hệ với nhau.

Các dạng chuẩn thường được áp dụng nhất là Dạng chuẩn 1 (1NF), Dạng chuẩn 2 (2NF) và Dạng chuẩn 3 (3NF). Dạng chuẩn 1 yêu cầu mỗi cột phải chứa giá trị nguyên tử — không chứa mảng hay danh sách phân tách bằng dấu phẩy trong một ô. Dạng chuẩn 2 mở rộng từ 1NF bằng yêu cầu mọi cột không phải khóa phải phụ thuộc vào toàn bộ khóa chính, điều này đặc biệt quan trọng với các khóa tổng hợp. Dạng chuẩn 3 đi xa hơn: không có cột không phải khóa nào được phép phụ thuộc vào một cột không phải khóa khác.

Hãy xét một tình huống thực tế: bạn cần lưu trữ các bài viết blog cùng với tác giả và thẻ (tag) của chúng. Cách tiếp cận không chuẩn hóa sẽ gộp tất cả vào một bảng duy nhất, dẫn đến các bất thường khi cập nhật — nếu tác giả đổi email, bạn phải cập nhật ở mọi dòng mà họ đã viết. Một thiết kế chuẩn hóa sẽ tách dữ liệu thành nhiều bảng riêng biệt với các khóa ngoại.

sql
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
body TEXT,
author_id INTEGER NOT NULL REFERENCES authors(id),
published_at TIMESTAMPTZ
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);

Bảng post_tags là một bảng trung gian (junction table) dùng để giải quyết quan hệ nhiều-nhiều giữa posts và tags. Chú ý cách SERIAL cung cấp số nguyên tự tăng, REFERENCES định nghĩa ràng buộc khóa ngoại, và ON DELETE CASCADE tự động xóa các dòng ở bảng trung gian khi bản ghi cha bị xóa.

Kiểu dữ liệu và ràng buộc

PostgreSQL cung cấp một hệ thống kiểu dữ liệu phong phú vượt xa INTEGER và VARCHAR cơ bản. Đối với lập trình viên backend, một số kiểu đặc biệt hữu ích bao gồm TIMESTAMPTZ cho timestamp có nhận biết múi giờ, JSONB để lưu trữ dữ liệu bán cấu trúc có hỗ trợ indexing, UUID cho định danh duy nhất toàn cục, và BOOLEAN cho cờ true/false.

Ràng buộc (constraints) thực thi tính toàn vẹn dữ liệu ở cấp độ cơ sở dữ liệu thay vì chỉ dựa vào codeứng dụng. Các ràng buộc phổ biến bao gồm NOT NULL, UNIQUE, CHECK, FOREIGN KEY và PRIMARY KEY. Sử dụng ràng buộc đồng nghĩa với việc dữ liệu của bạn luôn hợp lệ ngay cả khi mộtứng dụng bị lỗi cố gắng chèn các giá trị không hợp lệ.

sql
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
price NUMERIC(10,2) CHECK (price >= 0),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
metadata JSONB DEFAULT '{}'
);

NUMERIC(10,2) lưu trữ giá trị thập phân chính xác, hoàn hảo cho các khoản tiền tệ nơi mà lỗi dấu phẩy động là không thể chấp nhận được. Ràng buộc CHECK đảm bảo giá và số lượng tồn kho không bao giờ âm. Mệnh đề DEFAULT trên metadata nghĩa là các hàng mới sẽ tự động nhận một đối tượng JSON rỗng thay vì NULL.

sơ đồ luồng (flowchart) hiển thị pipeline xử lý truy vấn từ kết nối client thông qua parser, analyzer, planner, executor, đến storage layer

Những sai lầm thiết kế phổ biến

Một sai lầm thường gặp là sử dụng VARCHAR mà không giới hạn độ dài, điều này có thể dẫn đến các hàng có kích thước lớn bất ngờ. Một sai lầm khác là chọn TEXT ở khắp mọi nơi mà không cân nhắc liệu cột có thực sự cần độ dài không giới hạn hay không — việc chỉ rõ ràng các ràng buộc sẽ phát hiện lỗi sớm hơn. Các lập trình viên cũng thường quên tạo index trên các cột foreign key, điều này gây ra JOIN chậm và CASCADE delete chậm trên các bảng lớn.

Over-normalization là cái bẫy ngược lại: chia dữ liệu thành quá nhiều bảng nhỏ có thể khiến truy vấn phải JOIN quá nhiều. Đối với các workload đọc nhiều, denormalization có chiến lược hoặc materialized view có thể là sự đánh đổi phù hợp. Mục tiêu là tìm được sự cân bằng phù hợp với các access pattern cụ thể của bạn.

Tóm tắt

Kiến trúc đa tiến trình của PostgreSQL với pipeline parser-planner-executor mang lại cho bạn đặc tính hiệu năng có thể dự đoán được. Thiết kế quan hệ đã chuẩn hóa thông qua 1NF, 2NF và 3NF giảm thiểu dư thừa và ngăn ngừa các bất thường khi cập nhật, với foreign key và junction table xử lý các quan hệ phức tạp. Hệ thống kiểu dữ liệu phong phú và hỗ trợ ràng buộc của PostgreSQL cho phép bạn thực thi tính toàn vẹn dữ liệu ở cấp độ schema, phát hiện lỗi trước khi chúng đến tầng code ứng dụng. Trong bài học tiếp theo, chúng ta sẽ chuyển từ thiết kế schema sang viết các truy vấn hiệu quả trên các cấu trúc này.

Bài kiểm tra cuối bài

1. Tiến trình nào của PostgreSQL chịu trách nhiệm tiếp nhận các kết nối client mới và tạo một backend process cho mỗi kết nối?

2. Dạng chuẩn 3 (3NF) yêu cầu gì ngoài 1NF và 2NF?

3. Trong ví dụ schema blog, tại sao lại dùng bảng trung gian post_tags thay vì cột tags trong bảng posts?

4. Kiểu dữ liệu PostgreSQL nào phù hợp nhất để lưu trữ các giá trị tiền tệ mà yêu cầu độ chính xác tuyệt đối?

5. Điều gì xảy ra khi ON DELETE CASCADE được định nghĩa trên ràng buộc khóa ngoại?

6. Một truy vấn SQL đi qua những giai đoạn nào trong bộ xử lý truy vấn của PostgreSQL?

7. Một sai lầm phổ biến trong thiết kế schema mà bài học cảnh báo là gì?