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.
Offset-based: simple, supports random page access:
typescript
const [data, total] = await this.repo.findAndCount({ take: limit, skip: (page-1)*limit });Downside: unstable when data changes, slow with large skip.
Cursor-based: stable for real-time feeds, scales better:
typescript
const data = await this.repo.find({
where: { createdAt: LessThan(new Date(decodedCursor)) },
take: limit + 1,
});Use offset for: admin dashboards, search.
Use cursor for: social feeds, infinite scroll.