PostgreSQL Indexing: Đừng chỉ dùng B-Tree nếu muốn xử lý hàng triệu dòng dữ liệu

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Bối cảnh: Khi Index không còn là “phép màu” mặc định

Đi làm dự án thực tế, từ E-commerce chạy MySQL đến các hệ thống Big Data trên PostgreSQL, mình rút ra một bài học xương máu: Index chính là linh hồn của hiệu suất.

Thử tưởng tượng bảng 100 triệu dòng mà thiếu Index. Nó chẳng khác nào việc bạn mò kim đáy bể trong một thư viện khổng lồ không có mục lục. Kết quả? CPU nhảy vọt lên 100%, Disk I/O nghẽn cổ chai và ứng dụng quay vòng vòng đến timeout.

Nhưng sai lầm lớn nhất của nhiều dev là chỉ biết mỗi CREATE INDEX mặc định. PostgreSQL cung cấp cả một “kho vũ khí” chuyên dụng như GIN, GiST hay BRIN. Chọn sai loại Index không chỉ làm chậm lệnh ghi (INSERT/UPDATE) mà còn ngốn hàng chục GB ổ cứng một cách lãng phí.

Triển khai: Quy tắc sống còn trên Production

Tạo Index trong Postgres rất dễ về cú pháp, nhưng cực kỳ rủi ro nếu bạn làm trên một bảng đang hoạt động mạnh.

-- Cách tạo Index "phá hủy" hệ thống (Lock bảng)
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- Cách tạo Index an toàn (Khuyên dùng cho Production)
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);

Lưu ý từ thực tế: Luôn dùng từ khóa CONCURRENTLY. Nó giúp tạo Index mà không ngăn cản user đặt hàng hay thanh toán. Tuy thời gian tạo sẽ lâu hơn một chút, nhưng ít nhất bạn sẽ không bị sếp gọi vì làm treo app giữa giờ cao điểm.

Phân loại và Ứng dụng: Chọn đúng vũ khí

Mỗi loại Index sinh ra để giải quyết một bài toán riêng biệt. Đừng dùng dao mổ trâu để giết gà.

1. B-Tree Index: “Con dao pha lê” đa năng

Đây là loại mặc định. Nó cực giỏi trong việc tìm kiếm khoảng (range) và so sánh trực tiếp như <, =, >.

Dùng khi nào: 90% các trường hợp tìm ID, email, ngày tháng hoặc dùng ORDER BY. Nếu dữ liệu của bạn là dạng phẳng và đơn giản, B-Tree là lựa chọn số 1.

2. GIN (Generalized Inverted Index): Cứu cánh cho JSONB

Nếu bạn đang lưu thông tin linh hoạt trong cột JSONB hoặc mảng (Array), B-Tree sẽ hoàn toàn vô dụng. GIN đánh chỉ mục sâu vào từng key/value bên trong dữ liệu phức tạp đó.

Con số thực tế: Trong một dự án quản lý 500.000 sản phẩm có thuộc tính động, việc dùng GIN đã giúp truy vấn lọc theo màu sắc và kích cỡ giảm từ 2.8 giây xuống còn đúng 12ms.

-- Tối ưu cực mạnh cho cột JSONB
CREATE INDEX idx_products_metadata ON products USING GIN (metadata jsonb_path_ops);

3. BRIN (Block Range Index): Tuyệt chiêu cho Big Data

BRIN là “vũ khí hạng nặng” cho những bảng log hàng tỷ dòng. Thay vì đánh chỉ mục từng dòng, nó chỉ lưu giá trị min/max của từng khối dữ liệu (block).

Ví dụ cụ thể: Một bảng log 500GB nếu dùng B-Tree thì file Index có thể chiếm tới 40-60GB. Chuyển sang BRIN, dung lượng Index chỉ còn khoảng 150MB mà tốc độ search theo thời gian vẫn cực nhanh.

CREATE INDEX idx_huge_logs_ts ON big_logs USING BRIN (created_at);

4. Hash Index: Chỉ dành cho so sánh bằng

Hash Index chỉ hỗ trợ phép =. Từ bản Postgres 10 trở đi, nó đã ổn định và nhanh hơn B-Tree một chút cho các cột có giá trị duy nhất (như UUID) nhưng không phổ biến bằng B-Tree.

Kiểm tra & Tối ưu: Đừng tin vào cảm giác

Tạo Index xong không có nghĩa là xong việc. Đôi khi Postgres sẽ phớt lờ Index của bạn nếu nó thấy quét toàn bộ bảng (Seq Scan) còn nhanh hơn.

Đọc hiểu câu lệnh với EXPLAIN ANALYZE

Hãy luôn soi kỹ cách SQL vận hành bằng lệnh sau:

EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';

Nếu thấy dòng Index Scan, bạn đã đi đúng hướng. Nếu thấy Seq Scan trên bảng hàng triệu dòng, hãy kiểm tra lại kiểu dữ liệu hoặc xem Index đã được tạo thành công chưa.

Dọn dẹp Index “rác”

Index là con dao hai lưỡi. Quá nhiều Index sẽ khiến lệnh INSERT chậm thảm hại vì database phải cập nhật tất cả chỉ mục cùng lúc. Hãy chạy script này để tìm các Index chưa bao giờ được dùng và mạnh dạn xóa chúng:

SELECT relname, indexrelname, idx_scan 
FROM pg_stat_user_indexes 
WHERE idx_scan = 0 AND idx_unique = false;

Tóm lại, tối ưu PostgreSQL không phải là phép màu, mà là sự thấu hiểu dữ liệu. Nếu bảng nhỏ, B-Tree là đủ. Nếu dữ liệu khổng lồ, hãy cân nhắc BRIN. Nếu chơi với JSONB, GIN là bắt buộc. Chúc các bạn làm chủ được database của mình!

Share: