limit and offset are methods for limiting the number of query results and implementing pagination. These are essential methods for performance optimization and implementing paging functionality.
Basic Usage
limit
Limits the maximum number of results to return.
const users = await puri.table("users")
.select({ id: "users.id", name: "users.name" })
.limit(10);
// SELECT users.id, users.name FROM users LIMIT 10
offset
Skips a specified number of results.
const users = await puri.table("users")
.select({ id: "users.id", name: "users.name" })
.offset(20)
.limit(10);
// SELECT users.id, users.name FROM users LIMIT 10 OFFSET 20
offset is typically used with limit for pagination.
// Page 1: Items 1-10
const page1 = await puri.table("posts")
.select({ id: "posts.id", title: "posts.title" })
.orderBy("posts.created_at", "desc")
.limit(10)
.offset(0);
// Page 2: Items 11-20
const page2 = await puri.table("posts")
.select({ id: "posts.id", title: "posts.title" })
.orderBy("posts.created_at", "desc")
.limit(10)
.offset(10);
// Page 3: Items 21-30
const page3 = await puri.table("posts")
.select({ id: "posts.id", title: "posts.title" })
.orderBy("posts.created_at", "desc")
.limit(10)
.offset(20);
function paginate(page: number, perPage: number) {
return {
limit: perPage,
offset: (page - 1) * perPage
};
}
// Get page 3 with 20 items per page
const { limit, offset } = paginate(3, 20);
const posts = await puri.table("posts")
.select({ id: "posts.id", title: "posts.title" })
.orderBy("posts.created_at", "desc")
.limit(limit)
.offset(offset);
// Returns items 41-60
Cursor-based pagination is more efficient for large datasets than offset-based pagination.
// First page: Latest 10 posts
const firstPage = await puri.table("posts")
.select({ id: "posts.id", title: "posts.title", created_at: "posts.created_at" })
.orderBy("posts.created_at", "desc")
.limit(10);
// Get last item's cursor
const lastPost = firstPage[firstPage.length - 1];
const cursor = lastPost.created_at;
// Next page: 10 items after cursor
const nextPage = await puri.table("posts")
.select({ id: "posts.id", title: "posts.title", created_at: "posts.created_at" })
.where("posts.created_at", "<", cursor)
.orderBy("posts.created_at", "desc")
.limit(10);
// First page
const firstPage = await puri.table("users")
.select({ id: "users.id", name: "users.name" })
.orderBy("users.id", "asc")
.limit(20);
// Next page cursor
const lastUserId = firstPage[firstPage.length - 1].id;
// Next page
const nextPage = await puri.table("users")
.select({ id: "users.id", name: "users.name" })
.where("users.id", ">", lastUserId)
.orderBy("users.id", "asc")
.limit(20);
Practical Examples
1. offset is Slow for Large Datasets
// ❌ Bad: Offset scans all skipped rows
await puri.table("posts")
.limit(10)
.offset(10000); // Scans 10,000 rows then skips them
// ✅ Good: Use cursor-based pagination
await puri.table("posts")
.where("posts.id", ">", lastId)
.limit(10); // Directly finds target rows
2. Use with orderBy
// ⚠️ Caution: Results may be inconsistent without ordering
await puri.table("users")
.limit(10)
.offset(20);
// ✅ Good: Guarantee consistent ordering
await puri.table("users")
.orderBy("users.id", "asc")
.limit(10)
.offset(20);
3. Optimize COUNT Queries
// ❌ Bad: Separate queries for data and count
const users = await puri.table("users")
.limit(10)
.offset(20);
const { count } = await puri.table("users")
.select({ count: Puri.count() })
.first();
// ✅ Good: Use findMany that combines both
const { rows, total } = await UserModel.findMany("A", {
page: 3,
num: 10
});
// One query returns both data and total
offset vs Cursor
offset Method
Advantages:
- Simple implementation
- Can jump to any page
- Suitable for numbered pages
Disadvantages:
- Slow for large offsets
- Inconsistent results when data changes
- Always scans from beginning
// offset: Suitable for page numbers
<button onClick={() => goToPage(5)}>Page 5</button>
Cursor Method
Advantages:
- Fast regardless of position
- Consistent results
- Efficient memory usage
Disadvantages:
- Cannot jump to arbitrary pages
- Slightly complex implementation
- Cursor management needed
// Cursor: Suitable for infinite scroll
<button onClick={() => loadMore()}>Load More</button>
Cautions
1. Missing orderBy
// ❌ Dangerous: Inconsistent order
await puri.table("users")
.limit(10);
// ✅ Safe: Explicit ordering
await puri.table("users")
.orderBy("users.id", "asc")
.limit(10);
2. Large offset
// ❌ Very slow
await puri.table("posts")
.limit(10)
.offset(1000000);
// ✅ Fast
await puri.table("posts")
.where("posts.id", ">", lastId)
.limit(10);
3. limit Without WHERE
// ⚠️ Caution: Returns ALL data without limit
const allUsers = await puri.table("users").selectAll();
// ✅ Safe: Always use limit
const users = await puri.table("users")
.selectAll()
.limit(1000);
4. Pagination and Data Changes
// Problem: Items may appear twice or be skipped
// when data changes between page loads
// Solution 1: Use cursor pagination
await puri.table("posts")
.where("posts.id", ">", lastId)
.limit(10);
// Solution 2: Use timestamp
await puri.table("posts")
.where("posts.created_at", "<=", snapshotTime)
.limit(10)
.offset(20);
Type Safety
Type safety is maintained when using limit and offset.
const users = await puri.table("users")
.select({ id: "users.id", name: "users.name" })
.limit(10)
.offset(20);
// Type: Array<{ id: number; name: string }>
users[0].id; // number
users[0].name; // string
Next Steps