Offset-based (skip/take): đơn giản, hỗ trợ random page access:
typescript
async findAll(page: number, limit: number) {
const [data, total] = await this.repo.findAndCount({
take: limit, skip: (page - 1) * limit,
order: { createdAt: 'DESC' },
});
return { data, total, page, lastPage: Math.ceil(total / limit) };
}Nhược điểm: không ổn định khi data thay đổi real-time, chậm khi skip lớn (DB phải scan).
Cursor-based (keyset pagination): ổn định hơn cho real-time feeds, hiệu quả hơn khi scale:
typescript
async findAfterCursor(cursor: string, limit: number) {
const decodedCursor = Buffer.from(cursor, 'base64').toString();
// cursor = ISO timestamp của item cuối cùng
const data = await this.repo.find({
where: { createdAt: LessThan(new Date(decodedCursor)) },
take: limit + 1, // +1 để biết có page tiếp không
order: { createdAt: 'DESC' },
});
const hasMore = data.length > limit;
const items = hasMore ? data.slice(0, -1) : data;
const nextCursor = hasMore ? Buffer.from(items.at(-1)!.createdAt.toISOString()).toString('base64') : null;
return { items, nextCursor, hasMore };
}Dùng offset cho: admin dashboards, search results.
Dùng cursor cho: social feeds, infinite scroll.