Read sorted sets by rank, score, or lexicographic range without mixing their boundary and pagination semantics.
ZRANGE by Rank/Score/Lex, Reverse Ordering, Pagination, and WITHSCORES
Choose hashes, Redis JSON, or many keys from structure, update/query patterns, expiration granularity, memory/cardinality, indexing, ACL boundaries, and migration evidence.
Learning outcomes
AtlasMart has one sorted set but several query meanings: “top
20,” “orders due between two timestamps,” and “members whose
byte strings fall in a prefix range.” Those are different range
models. ZRANGE unifies rank, score, and
lexicographic access, but its arguments change meaning with
BYSCORE, BYLEX, and REV.
Use ZRANGE correctly by rank, score, and lexicographic interval.
Explain inclusive/exclusive score bounds, infinities, negative rank indexes, and reverse ordering.
Use WITHSCORES and LIMIT without treating large offsets as free pagination.
Explain why BYLEX is only well-defined for equal-score members.
Design bounded pagination that tolerates concurrent score changes instead of assuming a stable snapshot.
All Chapter 06 mandatory labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from Docker
Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default ACL user disabled,
named ACL users atlasmart-app and
academy-admin, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data volume, and no explicit Redis
maxmemory limit or eviction policy. The primary
interface is the redis-cli shipped in the same
pinned image. Mandatory examples use only bounded synthetic
keys under atlasmart:ch06:*.
1. Rank ranges use zero-based inclusive indexes
Without BYSCORE or BYLEX, start and
stop are ranks. They are inclusive. Negative indexes count from
the end, so 0 -1 means the whole set. That form is
convenient for tiny fixtures but unsafe as a default API for
unbounded production sets.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:rangedocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:range 10 a 20 b 30 c 40 d 50 edocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 0 2 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range -2 -1 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 0 1 REV WITHSCORES
With REV, rank zero means the highest score. Do not
emulate top-N by reading the entire set and sorting in the
client.
2. Score ranges change the meaning of start and stop
With BYSCORE, the two range arguments are score
bounds. Bounds are inclusive by default; prefix a numeric bound
with ( for exclusive. -inf and
+inf express open-ended numeric ranges.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 20 40 BYSCORE WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range "(20" 40 BYSCORE WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range -inf 30 BYSCORE WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZCOUNT atlasmart:ch06:range "(20" 40
The ZCOUNT result should match the number of
members in the same score interval without transferring them.
Quote exclusive-bound tokens in host shells so
( reaches redis-cli literally.
3. Reverse BYSCORE reverses bound order too
When REV is combined with BYSCORE, the
first bound is the high bound and the second is the low bound. A
common bug is to keep ascending bound order and receive an empty
result.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 40 20 BYSCORE REV WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 20 40 BYSCORE REV WITHSCORES
The first form is the intended descending interval. Treat direction as part of the query contract, not a cosmetic output sort.
4. BYLEX is a different index idea
BYLEX compares member bytes lexicographically. Its
defined use assumes all members in the sorted set have the same
score; with different scores, lex-range results are unspecified.
Lex bounds use [ for inclusive and
( for exclusive, plus -/+
for unbounded ends.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:lexdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:lex 0 customer:100 0 customer:120 0 customer:200 0 order:100docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:lex "[customer:100" "[customer:199" BYLEXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:lex "[customer:" "(customer;" BYLEXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZLEXCOUNT atlasmart:ch06:lex "[customer:" "(customer;"
This works because every member uses score 0. The prefix-end trick depends on byte ordering and chosen alphabet; validate it with fixtures rather than treating it as locale-aware text search.
5. Wrong approach: lex queries over mixed scores
If you reuse a leaderboard with different scores and issue
BYLEX, the result is not a valid secondary text
index. Redis documents the result as unspecified in that
situation.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:badlexdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:badlex 1 alpha 2 beta 3 gammadocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:badlex "[a" "[z" BYLEXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:badlexdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:badlex 0 alpha 0 beta 0 gammadocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:badlex "[a" "[z" BYLEX
The first output must not be used as evidence of a lexicographic contract. Repair by using equal scores for a lex index, or choose Redis Search/another index when you need richer text/query semantics.
6. LIMIT bounds reply size, but deep offsets still cost traversal
For BYSCORE/BYLEX, LIMIT offset count bounds the
reply count. However, a large offset can require Redis to
traverse many matching elements before returning the page.
Offset pagination also shifts under concurrent score updates.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range -inf +inf BYSCORE LIMIT 0 2 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range -inf +inf BYSCORE LIMIT 2 2 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZCARD atlasmart:ch06:range
For large or mutable datasets, prefer cursor-like application continuation based on the last observed score/member where the business contract permits it, and document duplicate/skip handling at ties. Sorted-set range reads are not snapshot transactions.
7. Same-score ties need a tie-break rule
Score ordering alone does not distinguish members that tie. Redis orders equal-score members lexicographically. If you page by score only and multiple members share the boundary score, a naïve “next page score > last score” rule can skip peers. Carry both score and member as continuation state, or use a composite member/score design whose ordering contract is explicit.
8. WITHSCORES is evidence and payload
WITHSCORES is useful when clients need both member
and score or when you are verifying range boundaries. It also
roughly doubles the logical items returned and increases
wire/client parsing work.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 0 4docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:range 0 4 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch06:range
Measure response bytes in your client/load-test environment rather than inferring network cost from element count alone.
9. Range selection matrix
| Question | ZRANGE form | Important boundary |
|---|---|---|
| Top N | 0 N-1 REV WITHSCORES |
rank changes when scores change |
| Score interval | min max BYSCORE |
inclusive by default; ( excludes |
| Descending score interval | max min BYSCORE REV |
reverse bound order |
| Lex interval | min max BYLEX |
defined for equal-score set |
| Page | ... LIMIT offset count |
deep offset + concurrent mutation cost |
10. Reproducible query lab
Build separate rank/score and lex fixtures so their contracts cannot be accidentally mixed.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:lab:scores atlasmart:ch06:lab:lexdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:lab:scores 100 p:a 100 p:b 150 p:c 220 p:ddocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:lab:scores 0 2 REV WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:lab:scores "(100" 220 BYSCORE WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:lab:lex 0 sku:aa 0 sku:ab 0 sku:badocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:lab:lex "[sku:a" "(sku:b" BYLEX
Verification: top-three ranking includes the 220 and 150 scores
plus one 100-score member according to tie ordering;
score-exclusive range excludes both 100s; lex range returns the
sku:a* members.
11. Production judgment
Choose range mode from the invariant, not convenience. Keep result counts bounded; measure deep-offset cost; handle ties explicitly; do not promise snapshot consistency across multiple pages; and do not use BYLEX on mixed scores. In Cluster, one sorted-set key is local to one slot, but cross-key derived reads later in the chapter require compatible slot locality. Search/index workloads should use Redis Search when full-text/structured predicates are the real problem rather than forcing lexicographic member encodings.
12. Summary and next step
Rank, score, and lex ranges are three different query contracts exposed through one command family. Next, we mutate scores over time to build leaderboards, windows, delayed work, and time-ordered indexes.
Check your understanding
- Are ZRANGE rank stop indexes inclusive?
- How do you express score > 20?
- With BYSCORE REV, which bound comes first?
- When is BYLEX well-defined?
- Why can deep LIMIT offsets be expensive?
Review the answers
Yes.
Use an exclusive lower bound such as "(20".
The higher score bound comes first.
When the members being lexically indexed use the same score.
Redis may need to traverse the skipped matching elements before returning the page.
Authoritative references
- Redis sorted sets — data model, score/rank behavior, and common patterns
- ZADD — conditional updates, score precision, and return behavior
- ZRANGE — rank, BYSCORE, BYLEX, REV, LIMIT, and WITHSCORES
- ZINCRBY — relative score updates
- ZUNION — weighted unions and aggregation
- ZINTER — weighted intersections and complexity
- ZDIFF — sorted-set difference semantics
- Redis Cluster specification — slot locality for multi-key commands
- Redis Open Source 8.10 release notes — pinned course release family
- ZRANGEBYLEX — equal-score lexicographic ordering requirement
- ZCOUNT — score-range cardinality