+1

Smart Pagination: Khi Materialized View Thay Đổi Cuộc Chơi

Những gì còn sót lại

Hi anh em dev 👋, hôm trước tớ có làm buổi techtalk trong team về optimize với Postgres, cuối buổi có một câu hỏi mở của anh Leader làm mình trăn trở, làm sao tối ưu được Pagination của 1 trang portal của công ty 🥹️️️️️️

Hiện tại trang portal bên mình dùng offset pagination, nên khi đến 1 trang bất kỳ ví dụ trang 100.000 thì nó load rất là lâu luôn! Vậy là hành trình tìm giải pháp của tớ bắt đầu

Các bạn theo dõi nhen, vì tớ có một giải pháp thú vị đấy 🤪

Performance Comparison

Vấn đề với phương pháp phân trang truyền thống

Khi làm việc với PostgreSQL, chúng ta thường sử dụng hai phương pháp phân trang chính:

1. Offset Pagination

SELECT * FROM big_table
ORDER BY id
LIMIT 10 OFFSET 10000;

Vấn đề: PostgreSQL vẫn phải quét và bỏ qua 10.000 rows đầu tiên! Khi OFFSET tăng lên, thời gian thực thi cũng tăng theo, do phải duyệt qua rất nhiều phần tử

2. Cursor-based Pagination

SELECT * FROM big_table
WHERE id > 10000
ORDER BY id
LIMIT 10;

Tốt hơn nhưng vẫn có những hạn chế:

  • Yêu cầu client lưu trữ giá trị cursor
  • Khó khăn khi muốn nhảy đến một trang cụ thể

Ý tưởng chợt đến

Sau một thời gian nghĩ giải pháp, muốn thoát khỏi 2 cách phổ biến này 🫠 tớ muốn cách nào đó lạ hơn xíu 😆 bỗng..

"Khoan, sao không dùng materialized view để lưu trữ sẵn thông tin phân trang?"

Đi sâu vào ý tưởng:

  1. Tạo một materialized view lưu trữ ID của mỗi record và số trang tương ứng, mình tạo mặc định mỗi trang 10 phần tử
  2. Khi cần phân trang, join với materialized view này để lấy dữ liệu một cách nhanh chóng. Nếu dữ liệu table chính có dữ liệu mới mà materialized view chưa được refresh thì mình lấy ở table chính

Lưu ý cái này không áp dụng được cho filter column nào đó nha 😂

Mình thấy nó có lý và có khả năng thực hiện được, bắt đầu triển thử nhé ^^

Triển khai: Smart Pagination với Materialized View

Bước 1: Chuẩn bị dữ liệu

Tạo dữ liệu mẫu để test luôn há

CREATE TABLE big_table (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    value INTEGER,
    created_at TIMESTAMP DEFAULT NOW()
);

INSERT INTO big_table (name, value)
SELECT
    'Item ' || i,
    (random() * 1000)::integer
FROM generate_series(1, 10000000) i;

Bước 2: Tạo Materialized View

CREATE MATERIALIZED VIEW big_table_views AS
SELECT
    id,
    (ROW_NUMBER() OVER (ORDER BY id) - 1) / 10 + 1 AS page_number
FROM
    big_table
ORDER BY
    id;

Materialized view này sẽ:

  • Gán mỗi record vào một trang (10 records/trang)
  • Lưu trữ ID và số trang của mỗi record

Bước 3: Tạo Index để tối ưu truy vấn

CREATE INDEX idx_big_table_views_page_number ON big_table_views(page_number);
CREATE INDEX idx_big_table_views_id ON big_table_views(id);

Bước 4: Viết hàm lấy dữ liệu phân trang thông minh

Đây là phần thú vị - cách tớ kết hợp materialized view với bảng gốc để có pagination cực kỳ hiệu quả

Flow như sau, còn code các bạn theo dõi trong Repository này nhé dongitran/Smart-Pagination

Flow

Lợi ích của phương pháp này

Phương pháp này mang lại nhiều lợi ích đáng kể:

  1. Hiệu suất cao: Thời gian truy vấn không phụ thuộc vào offset, đặc biệt quan trọng với trang có số thứ tự lớn
  2. Xử lý dữ liệu mới: Kết hợp giữa materialized view và bảng gốc để đảm bảo dữ liệu mới nhất luôn được hiển thị
  3. Linh hoạt: Có thể chuyển đến bất kỳ trang nào mà không cần lưu trữ cursor

Cần lưu ý

Có một số điểm cần lưu ý khi sử dụng phương pháp này:

  1. Cập nhật materialized view: Khi có nhiều dữ liệu mới, bạn cần refresh materialized view
  2. Dung lượng lưu trữ: Materialized view sẽ chiếm thêm không gian

Như vậy Smart Pagination với Materialized View là một kỹ thuật khá thú vị mà tớ tình cờ khám phá ra. Nó kết hợp những ưu điểm của cả offset pagination và cursor-based pagination, đồng thời khắc phục những hạn chế của chúng, bạn có nghĩ vậy hong ^^

Bạn có gặp vấn đề tương tự không? Hoặc có cách tối ưu nào khác cho phân trang trong PostgreSQL?

Hãy comment bên dưới để chia sẻ nhé! 🤓


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í