Menu

LIMIT và OFFSET trong SQLite: phân trang kết quả truy vấn

Cách LIMIT và OFFSET hoạt động trong SQLite: giới hạn số hàng, bỏ qua hàng, phân trang an toàn và cái bẫy hiệu năng cần tránh với bảng lớn.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

LIMIT giới hạn số hàng

LIMIT là nút điều chỉnh đơn giản nhất trong SQL: nó bảo SQLite "cho tôi tối đa chừng này hàng". Đặt nó ở cuối câu SELECT và bạn sẽ nhận về tối đa số kết quả đó: không hơn, có thể ít hơn nếu bảng không đủ hàng.

Bạn nhận về ba hàng đầu tiên. Nhưng chính xác là ba hàng nào? Đó là điểm mấu chốt: không có ORDER BY, SQLite chọn thứ tự nào tiện cho nó. Hôm nay có thể là thứ tự chèn; ngày mai, sau một lần cập nhật hay thay đổi chỉ mục, có thể không còn như vậy. Chỉ dùng LIMIT thì ổn khi bạn cần "xem thử vài hàng", nhưng ngay khi thứ tự quan trọng, bạn phải chỉ định rõ.

OFFSET bỏ qua các hàng ở đầu

Kết hợp LIMIT với OFFSET để lấy một đoạn ở giữa tập kết quả. OFFSET k bỏ đi k hàng đầu tiên; sau đó LIMIT n trả về tối đa n hàng trong phần còn lại.

Câu này nghĩa là "bỏ qua hai hàng, trả về hai hàng tiếp theo", tức hàng thứ 3 và thứ 4 của kết quả đã sắp xếp. Cách hình dung: WHERE lọc, ORDER BY sắp xếp, OFFSET bỏ qua, LIMIT giới hạn. Chúng chạy theo đúng thứ tự đó, và mệnh đề nào cũng quan trọng.

Phân trang luôn cần ORDER BY

Cách dùng LIMIT và OFFSET phổ biến nhất là phân trang: chia một danh sách dài thành các trang, chẳng hạn mỗi trang 20 hàng. Trang 1 là LIMIT 20 OFFSET 0, trang 2 là LIMIT 20 OFFSET 20, và cứ thế tiếp tục.

Có hai điều cần chú ý. Thứ nhất, ORDER BY là bắt buộc: không có nó, "trang 2" không có ý nghĩa xác định và các hàng có thể bị xáo trộn giữa những lần tải trang. Thứ hai, khóa sắp xếp có thêm id để phân định khi bằng nhau. Nếu hai bài viết có cùng created_at, bạn cần một cột duy nhất để cho chúng một thứ tự xác định, nếu không vị trí của chúng có thể đổi chỗ và một hàng có thể lọt sang trang khác.

Quy tắc chung: sắp xếp theo một cột duy nhất, hoặc theo cột sắp xếp của bạn cộng thêm một cột duy nhất để phân định.

Cách viết tắt: LIMIT n, m

SQLite hỗ trợ cú pháp dấu phẩy cũ để tương thích với MySQL: LIMIT offset, count. Nó có nghĩa giống LIMIT count OFFSET offset, nhưng thứ tự bị đảo ngược và dễ đọc nhầm.

-- These two are equivalent:
SELECT * FROM books LIMIT 10 OFFSET 20;
SELECT * FROM books LIMIT 20, 10;     -- offset first, then count

Dạng thứ hai ngắn gọn, nhưng hay làm khó những ai nghĩ số đầu tiên là số lượng. Hãy dùng LIMIT n OFFSET k: rõ ràng và đọc từ trái sang phải.

OFFSET không có LIMIT: mẹo LIMIT -1

OFFSET không hợp lệ khi đứng một mình: ngữ pháp của SQLite yêu cầu nó phải đi sau LIMIT. Vậy làm sao để nói "bỏ qua 10 hàng đầu và trả về toàn bộ phần sau"? Quy ước là LIMIT -1, mà SQLite hiểu là "không giới hạn trên".

Mọi giá trị LIMIT âm đều có tác dụng như vậy, nhưng -1 là cách viết quen thuộc. Bạn sẽ thường gặp nó trong các script duyệt qua kết quả theo trang và cần một truy vấn "lấy hết phần còn lại" cho lô cuối cùng.

Cái bẫy hiệu năng của OFFSET

Đây là điều không ai nhắc tới cho đến khi bạn gặp phải: OFFSET không giúp SQLite bỏ qua công việc, nó chỉ bỏ qua đầu ra. Để trả về các hàng từ 10.001 đến 10.020, engine vẫn phải duyệt qua mười nghìn hàng đầu tiên bên trong trước khi bắt đầu trả kết quả. Offset nhỏ thì không tốn gì; offset lên tới hàng chục hay hàng trăm nghìn sẽ chậm thấy rõ.

Với phân trang sâu, cách khắc phục chuẩn là keyset pagination: thay vì "bỏ qua N hàng", hãy nhớ khóa sắp xếp của hàng cuối cùng và yêu cầu "các hàng sau hàng này".

Mỗi trang là một lần tra cứu qua chỉ mục thay vì quét qua mọi thứ đứng trước. Đổi lại, bạn không thể nhảy thẳng tới "trang 47", chỉ có thể đi tiếp về phía trước. Với feed cuộn vô hạn và cursor của API, đó chính xác là điều bạn muốn.

Phân trang bằng OFFSET phù hợp với bảng quản trị và tập kết quả nhỏ. Với mọi thứ tăng trưởng không giới hạn, hãy dùng keyset pagination.

Một ví dụ hoàn chỉnh

Ghép tất cả lại: một truy vấn phân trang có lọc, sắp xếp và cột phân định để thứ tự luôn xác định:

Lọc còn các sản phẩm văn phòng, sắp xếp theo giá tăng dần với tên làm cột phân định, lấy hai hàng đầu. Đổi OFFSET 0 thành OFFSET 2 để lấy trang 2. Truy vấn ngắn, nhưng mệnh đề nào cũng có vai trò riêng.

Tiếp theo: DISTINCT

LIMIT kiểm soát số hàng trả về; DISTINCT kiểm soát việc có trả về hàng trùng hay không. Đó là mệnh đề tiếp theo trong bộ công cụ của SELECT, và rất dễ dùng sai: đó là nội dung của trang tiếp theo.

Câu hỏi thường gặp

LIMIT trong SQLite dùng để làm gì?

LIMIT n giới hạn số hàng mà SELECT trả về tối đa là n. Nó chạy sau WHERE, GROUP BY và ORDER BY, nên bạn đang giới hạn tập kết quả cuối cùng, không phải số hàng mà truy vấn quét qua. SELECT * FROM users LIMIT 10 trả về tối đa mười hàng.

OFFSET hoạt động cùng LIMIT trong SQLite như thế nào?

OFFSET k bỏ qua k hàng đầu tiên của kết quả trước khi LIMIT bắt đầu đếm. Vậy LIMIT 10 OFFSET 20 trả về các hàng từ 21 đến 30. Bên trong, SQLite vẫn phải duyệt qua những hàng bị bỏ qua đó, vì vậy offset lớn sẽ chạy chậm.

Có thể dùng OFFSET mà không có LIMIT trong SQLite không?

Không trực tiếp: OFFSET chỉ hợp lệ khi là một phần của mệnh đề LIMIT. Cách xử lý là LIMIT -1 OFFSET k, trong đó -1 nghĩa là 'không giới hạn trên', nên SQLite bỏ qua k hàng và trả về toàn bộ phần còn lại. Đây là một điểm đặc biệt đáng nhớ.

Vì sao truy vấn phân trang cần ORDER BY?

Không có ORDER BY, SQLite được tự do trả về các hàng theo bất kỳ thứ tự nào, và thứ tự đó có thể thay đổi giữa các lần truy vấn. Khi đó phân trang sẽ hỏng: cùng một hàng có thể xuất hiện ở trang 1 và trang 3, hoặc biến mất hẳn. Luôn kết hợp LIMIT/OFFSET với ORDER BY trên một cột ổn định và duy nhất.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu