Redis Sorted Sets and Client-Side Pagination: ZRANGE vs ZSCAN
0 reputation · 29 Jul 2023, 05:51 UTC
0 reputation · 29 Jul 2023, 05:51 UTC
Implementing pagination for large datasets in Redis often involves a choice between rank-based indexing using ZRANGE and cursor-based iteration via ZSCAN. While ZRANGE allows for precise offset-based bounding, the underlying skip list traversal can lead to increased latency as the offset grows deeper into the set.
Conversely, ZSCAN mitigates server-side blocking and performance degradation associated with high offsets, but it does not guarantee a strict sorted order across multiple cursor calls, which is often a requirement for user-facing paginated interfaces.
Given these architectural trade-offs in Redis 7.0+, what is the recommended approach for maintaining strict sort order while avoiding the O(log(N)+M) performance penalty of deep offsets? Is there a documented method to combine cursor-based stability with rank-based precision?
32275 reputation · 29 Jul 2023, 10:58 UTC
For Redis 7.0+ with a requirement for strict score order in user-facing pages, do not use ZSCAN for pagination. Use ZRANGE with LIMIT for shallow pages and keyset seek pagination with score+member as a cursor for deep pages. There is no documented way to make ZSCAN return score-ordered results, so cursor stability from ZSCAN cannot be combined with rank-based precision.
ZSCAN is intended for full key iteration or maintenance tasks, not for ordered page views. Client-side re-sorting of ZSCAN output defeats the purpose of a sorted set and increases client memory.
For stable, ordered pagination without deep offset cost, use a seek cursor based on last seen score and member:
In practice this is ZRANGEBYSCORE with an exclusive lower bound on score, and a tie-breaker on member within the same score. The pattern avoids offset traversal and remains stable under inserts between requests, at the cost of losing random jump to page N.
# first page
ZRANGE myzset 0 99 WITHSCORES
# seek next page after last_score, last_member
ZRANGEBYSCORE myzset (last_score +inf LIMIT 0 100
If ties on score are possible, use the member as a secondary cursor: score = last_score and member > last_member. This requires application logic to build the min bound.
This recommendation assumes read-mostly access and that users can accept next/prev navigation instead of arbitrary page numbers. Write churn during a session can still cause items to appear on two pages or be skipped if scores change. Redis does not provide a built-in transactional snapshot for pagination.
Verification you can run in a test key:
One missing diagnostic that changes the recommendation: what is the maximum page offset you must support and the write rate to the sorted set during a user session. If you need random access to page 10,000+ with high write churn, a different external index is required.
Use comments to ask for clarification. Post a solution as an answer.
32,275 reputation · 29 Jul 2023, 07:58 UTC
When using the keyset pattern with ZRANGE ... BYSCORE (introduced in Redis 6.2), keep LIMIT offset 0. A non-zero offset forces Redis to traverse the skip list from the beginning of the score range, reintroducing the deep-offset latency you are trying to avoid.
For read-heavy workloads where scores change infrequently, ZRANGESTORE offers a different trade-off: snapshot the ordered set into a list, then paginate with LRANGE. This gives O(1) page access but costs extra memory and write amplification on every update to the source sorted set.
Confirm command syntax and availability with COMMAND INFO on your target Redis version before deploying either pattern.