Document + Relational: PostgreSQL mang lại điều gì?

Chắc hẳn nhiều anh em khi bắt đầu làm việc với dữ liệu linh hoạt, đã từng rất ưu ái các giải pháp NoSQL, điển hình là MongoDB. Mình cũng từng dành nhiều thời gian nghiên cứu và áp dụng mô hình document vì khả năng scale và sự linh hoạt của nó.

Tuy nhiên, khi mình có cơ hội làm việc sâu hơn với PostgreSQL (Pg) trong các dự án gần đây, mình đã thấy một sự thay đổi lớn. PostgreSQL đã phát triển mạnh mẽ và tích hợp những tính năng document vượt trội, đồng thời giữ vững ưu điểm về reliabilitydata integrity truyền thống.

Bài chia sẻ hôm nay không nhằm mục đích so sánh hơn thua, mà để cùng anh em khám phá những cải tiến của Pg, đặc biệt là khả năng xử lý dữ liệu JSON mà có thể thay đổi quan điểm về việc lựa chọn cơ sở dữ liệu cho nhiều use case hiện tại.

JSONB: Sự linh hoạt của document trong PostgreSQL

Khi nhắc đến MongoDB, điểm mạnh lớn nhất chính là mô hình document, cho phép lưu trữ dữ liệu dưới dạng JSON linh hoạt, giúp anh em dễ dàng thay đổi cấu trúc dữ liệu mà không cần quá nhiều bước schema migrations.

Điều ít người ngờ tới là PostgreSQL đã bổ sung và hoàn thiện tính năng JSONB (Binary JSON). Việc lưu trữ JSON dưới dạng nhị phân này mang lại khả năng truy vấn dữ liệu nhanh hơn và tối ưu hơn.

Với JSONB, anh em có thể tạo bảng và lưu trữ dữ liệu phi cấu trúc (unstructured data) một cách dễ dàng:

-- Tạo bảng đơn giản chứa Document (tương đương Collection trong MongoDB)
CREATE TABLE user_profiles (
    user_id UUID PRIMARY KEY,
    document JSONB NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Chèn một Document (User Profile)
INSERT INTO user_profiles (user_id, document) VALUES 
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 
 '{"username": "devops_hanoi", "age": 30, "city": "Hanoi", "skills": ["Docker", "Kubernetes", "Terraform"], "is_active": true}');

1. Tùy chọn Index đa dạng

PostgreSQL cung cấp nhiều lựa chọn index mạnh mẽ cho JSONB như B-tree, GIN, GiST, cho phép anh em tối ưu hóa truy vấn JSONB một cách rất chi tiết. Index GIN (Generalized Inverted Index) là công cụ chính giúp tăng tốc truy vấn:

-- GIN index cho phép tìm kiếm bất kỳ trường nào bên trong document
CREATE INDEX idx_user_document_gin ON user_profiles USING GIN (document);

-- GIN index chuyên biệt cho phép toán Containment (@>) trên trường 'skills'
-- Phù hợp để tìm người dùng có chứa một tập hợp skills cụ thể
CREATE INDEX idx_user_skills ON user_profiles USING GIN ((document->'skills') jsonb_ops);

2. Khả năng truy vấn mạnh mẽ

PostgreSQL cung cấp các hàm và toán tử JSON rất mạnh mẽ và dễ đọc để truy vấn dữ liệu lồng nhau (nested data):

-- Query 1: Tìm người dùng có skill là 'Kubernetes'
SELECT user_id, document->>'username' AS username
FROM user_profiles
WHERE document->'skills' ? 'Kubernetes';

-- Query 2: Tìm người dùng ở Hanoi và 'is_active': true (dùng Containment @>)
SELECT user_id, document->>'username' AS username
FROM user_profiles
WHERE document @> '{"city": "Hanoi", "is_active": true}';

Toán tử @> kết hợp với index GIN mang lại tốc độ truy vấn hiệu quả hơn.

3. Biểu Thức JSON Path

JSON Path Expressions hỗ trợ mạnh mẽ cho việc truy vấn dữ liệu JSON lồng nhau. PostgreSQL hỗ trợ tính năng này, giúp việc truy vấn trở nên linh hoạt và dễ đọc hơn, đồng thời tối ưu hóa tốc độ:

-- Sử dụng JSON Path để tìm và trích xuất tuổi (age) của người dùng trên 25
SELECT jsonb_path_query_first(document, '$.age ? (@ > 25)') AS age_value
FROM user_profiles
WHERE jsonb_path_exists(document, '$.age ? (@ > 25)');

Mô Hình Hybrid Schema và Data Integrity

Một vấn đề thường gặp với mô hình schemaless là sự khó khăn trong việc đảm bảo data integrity ở database. PostgreSQL giải quyết vấn đề này bằng cách cho phép kết hợp schema chặt chẽ và linh hoạt.

Sự kết hợp giữa document và relational model

Anh em có thể enforce schema cho các trường quan trọng (order_id, user_id, order_status) bằng ràng buộc, đồng thời cho phép trường line_items thay đổi mà không cần migration.

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    user_id UUID REFERENCES users(user_id),  -- Trường quan hệ (Relational)
    order_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    order_status VARCHAR(50) NOT NULL,
    line_items JSONB  -- Document chứa các sản phẩm, chi tiết có thể thay đổi
);

Việc này giúp anh em có được:

  • Đảm bảo data integrity cho các trường cần thiết.
  • Linh hoạt cho dữ liệu ít quan trọng hơn mà có thể thay đổi cấu trúc mà không cần migration

Schema Evolution

Postgres cho phép anh em thêm các validation phức tạp dần dần, cân bằng giữa flexibilitydata integrity.

-- CHECK CONSTRAINT để đảm bảo trường 'order_status' phải là một trong các giá trị hợp lệ
ALTER TABLE orders
ADD CONSTRAINT chk_order_status CHECK (order_status IN ('PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CANCELLED'));

Đáng Tin Cậy Và Hiệu Suất Ổn Định

ACID Transactions

PostgreSQL luôn được biết đến với khả năng ACID compliance. Điều này đảm bảo rằng các transaction của anh em luôn đáng tin cậy, ngay cả khi cập nhật nhiều bảng hoặc nhiều document cùng lúc (multi-table transaction).

-- Đặt hàng - Cập nhật số lượng sản phẩm và tạo order mới
BEGIN;
    -- 1. Giả định giảm số lượng tồn kho (tại bảng relational/JSONB)
    UPDATE products 
    SET stock_quantity = stock_quantity - 1 
    WHERE product_id = 42 AND stock_quantity >= 1;

    -- 2. Insert đơn hàng mới (tại bảng orders)
    INSERT INTO orders (user_id, order_status, line_items)
    VALUES ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'PROCESSING', 
    '[{"product_id": 42, "name": "Laptop", "quantity": 1, "price": 1200.00}]'::JSONB);

    -- 3. Ghi log/Event nếu cần
    INSERT INTO events_log (event_type, payload) VALUES ('ORDER_CREATED', ...);

COMMIT;
-- Nếu bất kỳ bước nào thất bại, tất cả sẽ được ROLLBACK

Tối ưu Caching bằng UNLOGGED TABLE

Để đạt được hiệu năng gần với Redis trong việc lưu trữ dữ liệu không cần đảm bảo tính durability (như session, cache đơn giản), anh em có thể dùng UNLOGGED TABLE.

-- UNLOGGED TABLE không ghi vào WAL, cho hiệu suất I/O cao hơn
CREATE UNLOGGED TABLE session_cache (
    session_key VARCHAR(255) PRIMARY KEY,
    session_data JSONB,
    expires_at TIMESTAMP
);

-- Vẫn có thể dùng Index
CREATE INDEX idx_session_expiry ON session_cache (expires_at);

Hiệu Suất Tối Ưu Với Query Planner Thông Minh

Postgre được biết đến với Query Planner thông minh. Nó phân tích truy vấn và đưa ra plan thực thi tối ưu, thường vượt trội hơn so với các hệ thống có bộ tối ưu hóa đơn giản hơn, đặc biệt là với các query đa điều kiện và phức tạp.

Việc sử dụng lệnh EXPLAIN ANALYZE cho phép anh em kiểm tra chi tiết plan truy vấn của Pg. Trong ví dụ này, mình kiểm tra cách Pg tìm kiếm người dùng có tuổi (age) trong khoảng nhất định và đang hoạt động (is_active: true):

EXPLAIN ANALYZE
SELECT user_id, document->>'username'
FROM user_profiles
WHERE document @> '{"is_active": true}'
AND (document->>'age')::numeric < 30;

Anh em sẽ thấy được cách Pg tận dụng các index GIN và kết hợp chúng để xử lý query một cách hiệu quả.

Các tính năng khác giúp giảm phụ thuộc hệ thống bên ngoài

Bên cạnh các tính năng cốt lõi, Pg còn cung cấp các công cụ tích hợp sẵn rất mạnh mẽ mà các Document database thường cần công cụ bên ngoài.

Full-Text Search

PostgreSQL hỗ trợ stemming (cắt gốc từ), ranking, và biểu thức boolean phức tạp. Để minh họa, mình giả định có một bảng product_catalog (thay vì user_profiles) dùng để lưu trữ các sản phẩm, và mình sẽ thực hiện tìm kiếm trên trường tên sản phẩm và mô tả, được lưu trong cột document.

-- Giả định có bảng product_catalog (Cột 'document' chứa {'name': '...', 'description': '...'})

-- Tạo GIN Index cho Full-Text Search trên trường 'name' và 'description' trong document
CREATE INDEX idx_product_fts ON product_catalog
USING GIN (to_tsvector('english', 
    document->>'name' || ' ' || document->>'description'
));

-- Truy vấn tìm kiếm sản phẩm: tìm từ khóa "mobile" VÀ "new"
SELECT document->>'name' AS product_name,
       ts_rank(
           to_tsvector('english', document->>'name' || ' ' || document->>'description'), 
           plainto_tsquery('mobile & new') -- Sử dụng toán tử '&' (AND)
       ) AS rank_score
FROM product_catalog
WHERE to_tsvector('english', document->>'name' || ' ' || document->>'description') @@ plainto_tsquery('mobile & new')
ORDER BY rank_score DESC;

Kỹ thuật phân rã mảng

PostgreSQL có thể xử lý arrays trong JSONB một cách chi tiết, cho phép anh em thực hiện các phép toán tập hợp, thống kê trên các phần tử mảng. Trong ví dụ này, mình giả định trong bảng product_catalog, trường document có chứa một mảng tags (ví dụ: {"name": "...", "tags": ["sale", "new_arrival", "featured"]}) và mình muốn thống kê các tag phổ biến.

-- Giả định bảng product_catalog có trường 'document' chứa {'tags': ['tag1', 'tag2', ...]}

-- Đếm số lần xuất hiện của từng tag trên tất cả sản phẩm
SELECT tag, COUNT(*) AS tag_count
FROM product_catalog, 
     jsonb_array_elements_text(document->'tags') AS tag -- Tách từng phần tử mảng 'tags' thành các hàng riêng biệt
GROUP BY tag
ORDER BY tag_count DESC;

Cú pháp jsonb_array_elements_text() này cho phép chúng ta unnest mảng JSON thành các hàng dữ liệu riêng lẻ, từ đó dễ dàng thực hiện các hàm tổng hợp (COUNT, GROUP BY), điều mà NoSQL thường cần đến Aggregation Pipeline phức tạp hơn.

Phân Tích Dữ Liệu

Pg có sẵn các hàm phân tích như window functions, tính toán percentile (phân vị), giúp anh em thực hiện các thống kê phức tạp ngay trong database mà không cần chuyển dữ liệu sang hệ thống data processing khác.

-- Ví dụ Phân tích: Tính giá trung vị (median) và phân vị 90 (P90) của giá sản phẩm theo từng Category
SELECT 
    document->>'category' AS product_category,

    -- Tính giá trung vị (Median - P50)
    percentile_cont(0.5) WITHIN GROUP (ORDER BY (document->>'price')::numeric) AS median_price,

    -- Tính phân vị 90 (P90)
    percentile_cont(0.9) WITHIN GROUP (ORDER BY (document->>'price')::numeric) AS p90_price

FROM product_catalog
WHERE document->>'price' IS NOT NULL -- Loại bỏ các document không có trường 'price'
GROUP BY product_category;

Kết Luận

Việc mình chuyển từ ưu tiên NoSQL sang sử dụng PostgreSQL không phải là từ bỏ Document model, mà là tìm thấy một cách triển khai Document model tốt hơn.

PostgreSQL với JSONB mang lại sự kết hợp hoàn hảo giữa linh hoạt của documentsức mạnh của relational database: reliability ACID, khả năng truy vấn nâng cao và nền tảng ổn định.

Nếu anh em đang bắt đầu một dự án mới, cần cả sự linh hoạt trong phát triển lẫn sự ổn định về data integritytransaction, PostgreSQL là một lựa chọn rất đáng cân nhắc.

Chia sẻ bài viết:
Theo dõi
Thông báo của
0 Góp ý
Được bỏ phiếu nhiều nhất
Mới nhất Cũ nhất
Phản hồi nội tuyến
Xem tất cả bình luận