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.

Intermediate140–170 minutesRank/score/lex range labRedis Open Source 8.10.1Free/local-firstLast reviewed: September 6, 2026

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.

01

Use ZRANGE correctly by rank, score, and lexicographic interval.

02

Explain inclusive/exclusive score bounds, infinities, negative rank indexes, and reverse ordering.

03

Use WITHSCORES and LIMIT without treating large offsets as free pagination.

04

Explain why BYLEX is only well-defined for equal-score members.

05

Design bounded pagination that tolerates concurrent score changes instead of assuming a stable snapshot.

Exact lab baseline

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.

redis-cli · rank ranges
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.

redis-cli · score range boundaries
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.

redis-cli · reverse score query
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.

redis-cli · defined lexicographic range
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.

redis-cli · controlled misuse and repair
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.

redis-cli · bounded page
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.

redis-cli · observe reply shape and memory
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.

redis-cli · two-index lab
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

  1. Are ZRANGE rank stop indexes inclusive?
  2. How do you express score > 20?
  3. With BYSCORE REV, which bound comes first?
  4. When is BYLEX well-defined?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.