Bài 4: Phân trang hiệu quả – Bí quyết xử lý dữ liệu lớn không dùng OFFSET

Dưới đây là nội dung chi tiết cho Bài 4: Phân trang hiệu quả – Phân tích sâu cơ chế và triển khai Keyset Pagination:


Hiểu cơ chế đọc dữ liệu của database để tăng tốc phân trang gấp 100 lần


1. Vấn đề: Tại sao OFFSET chậm?

1.1. Cách OFFSET hoạt động

Giả sử query:

SELECT * FROM orders  
ORDER BY id  
LIMIT 10 OFFSET 100000;

Database phải làm gì?

  1. Quét toàn bộ bảng, sắp xếp theo id (nếu chưa có index).

  2. Đếm từng hàng cho đến khi đạt OFFSET + LIMIT (100,000 + 10).

  3. Bỏ qua 100,000 hàng đầu, trả về 10 hàng tiếp theo.

Chi phí:

  • Tăng tuyến tính với OFFSET: OFFSET càng lớn, query càng chậm.

  • Tốn I/O và CPU để đếm và bỏ qua các hàng.

Execution Plan (PostgreSQL):

EXPLAIN ANALYZE  
SELECT * FROM orders  
ORDER BY id  
LIMIT 10 OFFSET 100000;
  • Kết quả:

      Planning Time: 0.2 ms  
      Execution Time: 1200.5 ms  -- Rất chậm với OFFSET lớn!
    

2. Giải pháp: Keyset Pagination (Cursor-Based Pagination)

2.1. Nguyên lý hoạt động

  • Không đếm số hàng đã bỏ qua, thay vào đó:

    • Dựa vào giá trị cột cuối cùng của trang trước (ví dụ: id).

    • Dùng WHERE để lấy hàng sau giá trị đó.

Ví dụ:

  • Trang 1:

      SELECT * FROM orders  
      ORDER BY id  
      LIMIT 10;
    

    → Giá trị id cuối cùng: 100.

  • Trang 2:

      SELECT * FROM orders  
      WHERE id > 100  -- "Nhảy" thẳng đến vị trí này  
      ORDER BY id  
      LIMIT 10;
    

2.2. Tại sao Keyset nhanh hơn?

  • Database sử dụng index (nếu cột sắp xếp được index) để:

    • Tìm vị trí bắt đầu (WHERE id > 100) trực tiếp thông qua B-tree.

    • Quét tuần tự 10 hàng tiếp theo từ vị trí đó.
      Độ phức tạp: O(log N + LIMIT), không phụ thuộc vào OFFSET.

Execution Plan (PostgreSQL):

EXPLAIN ANALYZE  
SELECT * FROM orders  
WHERE id > 100000  
ORDER BY id  
LIMIT 10;
  • Kết quả:

      Planning Time: 0.1 ms  
      Execution Time: 0.5 ms  -- Nhanh hơn 2400 lần!
    

3. Phân tích sâu cơ chế Index và B-tree

3.1. Cấu trúc B-tree của Index

  • Index trên cột id lưu trữ giá trị theo thứ tự, phân chia thành các node.

  • Khi query WHERE id > 100000:

    1. Tìm kiếm binary search trên B-tree để xác định node chứa id = 100000.

    2. Quét tuần tự các leaf node tiếp theo để lấy 10 giá trị.

B-tree Index Scan

3.2. Điều kiện để Keyset hoạt động hiệu quả

  • Cột sắp xếp phải có index (ví dụ: PRIMARY KEY, UNIQUE INDEX).

  • Thứ tự sắp xếp phải cố định (ASC/DESC) giữa các trang.

  • Dữ liệu không thay đổi giữa các lần pagination (hoặc xử lý được việc thay đổi).


4. Case Study: Triển khai Keyset Pagination trong thực tế

4.1. Triển khai cho API

  • Yêu cầu: API phân trang danh sách bài viết, sắp xếp theo created_at DESC.

  • Tham số API:

      GET /posts?limit=10&cursor=2023-10-05T12:00:00Z
    
  • Query SQL:

      SELECT * FROM posts  
      WHERE created_at < '2023-10-05T12:00:00Z'  -- Lấy bài viết cũ hơn cursor  
      ORDER BY created_at DESC  
      LIMIT 10;
    

4.2. Xử lý dữ liệu động (New Data Inserted)

  • Vấn đề: Bài viết mới được thêm giữa các lần pagination → Thứ tự thay đổi.

  • Giải pháp:

    • Dùng cột immutable làm cursor (e.g., id thay vì created_at).

    • Hoặc thêm id vào điều kiện để đảm bảo thứ tự nhất quán:

        WHERE (created_at, id) < ('2023-10-05T12:00:00Z', 100)  
        ORDER BY created_at DESC, id DESC;
      

5. So sánh Keyset vs OFFSET

Tiêu chíOFFSETKeyset
Hiệu năngChậm khi OFFSET lớnNhanh, không phụ thuộc vào vị trí
Tài nguyênTốn CPU, I/O, MemoryTiết kiệm
Nhất quán dữ liệuDễ bị missing/duplicate khi data thay đổiNhất quán nếu dùng cursor cố định
Ứng dụngPhân trang đơn giản, dữ liệu nhỏDữ liệu lớn, real-time

6. Bài tập thực hành

Triển khai Keyset Pagination cho bảng products:

  • Yêu cầu:

    • Sắp xếp theo price DESC, id ASC.

    • Lấy trang tiếp theo dựa trên priceid của sản phẩm cuối cùng.

  • Giả sử sản phẩm cuối cùng của trang hiện tại có price = 50, id = 200.

Gợi ý đáp án:

SELECT * FROM products  
WHERE (price, id) < (50, 200)  -- So sánh composite  
ORDER BY price DESC, id ASC  
LIMIT 10;

Giải thích:

  • Điều kiện (price, id) < (50, 200) đảm bảo lấy các sản phẩm có giá nhỏ hơn 50 hoặc bằng 50 nhưng id lớn hơn 200.

  • Index cần thiết: Composite index trên (price DESC, id ASC).


7. Tổng kết

  • OFFSET phù hợp cho dữ liệu nhỏ, không yêu cầu tốc độ cao.

  • Keyset Pagination là lựa chọn tối ưu cho:

    • Ứng dụng real-time.

    • Dataset lớn (hàng triệu đến tỷ bản ghi).

    • API cần trải nghiệm người dùng mượt mà.

  • Luôn đảm bảo:

    • Index phù hợp cho cột cursor.

    • Xử lý nhất quán thứ tự và thay đổi dữ liệu.

Hiểu sâu cơ chế index và cách database quét dữ liệu giúp bạn thiết kế phân trang "đẳng cấp"!


Preview bài tiếp theo:
Bài 5: Phân tích Execution Plan – Bí kíp đọc hiểu và tối ưu hóa SQL từ gốc.