Use ZADD conditions, score precision, and rank semantics correctly before building leaderboards or time indexes.
ZADD Scores, Members, Conditional Updates, and Rank Semantics
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 wants a sales leaderboard where each store appears once, scores can change, ties are deterministic, and a promotion should only increase a score when a newer calculation is actually better. A Redis Sorted Set stores unique binary-safe members associated with double-precision floating-point scores. Ordering is ascending by score; members with equal scores are ordered lexicographically by member bytes.
Distinguish unique members from duplicate scores and explain rank versus score.
Use ZADD NX, XX, GT, LT, CH, and INCR with exact update semantics.
Prove tie ordering with ZRANGE, ZRANK, ZREVRANK, and ZSCORE.
Explain the exact-integer precision boundary of double scores and why scores are not exact money.
Relate cardinality, memory, hot-key concentration, persistence, replication, and Cluster locality to production design.
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. One member, one current score
A member is unique within one sorted-set key. Adding the same member again changes its score instead of creating a duplicate. Different members may share the same score. That distinction matters for rankings: “customer appears once” is enforced by the structure, while ties are normal.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:leaderboarddocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:leaderboard 120 store:berlin 120 store:baku 95 store:romedocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:leaderboard 0 -1 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:leaderboard 140 store:romedocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZCARD atlasmart:ch06:leaderboarddocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZSCORE atlasmart:ch06:leaderboard store:rome
ZCARD remains 3 after updating
store:rome. Equal-score members are ordered by
their member bytes, so tie behavior is deterministic but may be
business-meaningless unless the member naming scheme is
intentionally chosen.
2. Rank is a derived position, not stored business truth
ZRANK reports the zero-based position in ascending
score order; ZREVRANK reports the zero-based
position in descending order. Rank can change when any
neighboring score changes even though the member itself did not
change. Store the score as the business measure and derive rank
when needed.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANK atlasmart:ch06:leaderboard store:rome WITHSCOREdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZREVRANK atlasmart:ch06:leaderboard store:rome WITHSCOREdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:leaderboard 200 store:oslodocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZREVRANK atlasmart:ch06:leaderboard store:rome WITHSCORE
Redis 7.2+ supports the optional WITHSCORE return
on rank commands. Client wrappers may expose this differently,
so the course uses redis-cli as the normative evidence path.
3. Conditional ZADD makes the update rule explicit
NX adds only absent members;
XX updates only existing members.
GT updates an existing member only when the new
score is greater, and LT only when lower.
GT or LT alone still allows a missing
member to be added; combine with XX when “existing
only” is part of the invariant. CH changes the
integer reply from “new members” to “members changed.”
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:conditionaldocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:conditional NX 10 customer:7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:conditional NX 20 customer:7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:conditional XX GT CH 15 customer:7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:conditional XX GT CH 25 customer:7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZSCORE atlasmart:ch06:conditional customer:7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:conditional XX 5 customer:missing
The second NX write does nothing. The GT update to 15 succeeds only if it exceeds the existing score; the later 25 definitely does. The final XX write does not create a missing member.
4. INCR changes the meaning of ZADD
With INCR, ZADD increments one member by the
supplied score and only one score/member pair is allowed. This
is essentially ZINCRBY plus ZADD's conditional
options. The return is the resulting score, or null when a
condition rejects the operation.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:leaderboard INCR 7 store:bakudocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZSCORE atlasmart:ch06:leaderboard store:bakudocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:leaderboard XX GT INCR 5 store:baku
Use this when the score itself is the state you intend to mutate. If multiple external effects must move atomically with the score, a later transaction/function design is required.
5. Score precision is a real data-model boundary
Redis scores are IEEE-754 binary64 doubles. Integers from
-2^53 through +2^53 are exactly
representable; beyond that, adjacent integers can collapse to
the same floating value. That makes scores unsuitable for
arbitrary-precision identifiers or exact currency arithmetic.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:precisiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:precision 9007199254740992 exact:adocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:precision 9007199254740993 exact:bdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:precision 0 -1 WITHSCORES
Do not assume the two supplied decimal integers remain distinct as scores. For money, store integer minor units in a representation whose full range remains exact or keep authoritative monetary arithmetic in a strongly typed system; a leaderboard score is not a ledger.
6. Timestamp scores: milliseconds are practical; precision still needs analysis
Unix epoch milliseconds around 2026 are about 1.8×10^12, well inside the exact integer range of a binary64 score. Epoch microseconds are about 1.8×10^15 and also currently below 2^53; epoch nanoseconds are about 1.8×10^18 and are not exactly representable. Even when a timestamp fits, ties can occur. If deterministic ordering among equal timestamps matters, encode a tie-breaker in the member or use a different structure rather than pretending score precision creates uniqueness.
7. Wrong approach: score design creates unstable business ordering
A common mistake is to use a floating “money score” such as
19.99 and then compare ranks as if those scores
were exact decimal accounting values. Another is to stuff an
ever-growing global leaderboard into one key and assume each
O(log N) update guarantees low tail latency. Both ignore
representation and hot-key concentration.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:money-demodocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:money-demo 1999 order:1 2000 order:2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:money-demo 0 -1 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch06:money-demo
The repaired demo uses integer cents only as a bounded ranking score, not as the system of record. Production design still needs overflow/range analysis, sharding strategy, and authoritative financial state elsewhere.
8. Complexity and hot-key judgment
| Operation | Core cost shape | Operational consequence |
|---|---|---|
ZADD |
O(log N) per member | large/global hot sets still concentrate writes |
ZSCORE |
O(1) | cheap point lookup does not solve leaderboard fan-out |
ZRANK |
O(log N) | rank can move when others update |
ZRANGE |
O(log N + M) | response cardinality M dominates wide reads |
MEMORY USAGE |
observational | measure real encoding/allocator effects; do not hard-code folklore |
9. Reproducible AtlasMart lab
Create a bounded five-store leaderboard, exercise every update mode, and record the before/after score plus rank.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch06:lab:rankdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:lab:rank 51 s:a 40 s:b 51 s:c 75 s:d 12 s:edocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZRANGE atlasmart:ch06:lab:rank 0 -1 WITHSCORESdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:lab:rank XX LT CH 45 s:adocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZADD atlasmart:ch06:lab:rank XX GT CH 60 s:cdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZREVRANK atlasmart:ch06:lab:rank s:c WITHSCOREdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app ZCARD atlasmart:ch06:lab:rankdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch06:lab:rank
Verification: cardinality remains five; s:a becomes
45; s:c becomes 60; ties use lexicographic member
order. Cleanup only this fixture with
DEL atlasmart:ch06:lab:rank.
10. Production judgment
Sorted Sets are appropriate when one current numeric score per unique member and ordered access are native requirements. They do not provide arbitrary-precision scores, immutable history, replay, acknowledgments, schema enforcement, or automatic distribution of a hot global key. Replication and AOF/RDB affect availability/durability but not score semantics. In Cluster, a single sorted-set key belongs to one hash slot and therefore one primary at a time. Monitor cardinality, memory, write rate, range-result size, p50/p95/p99 latency, and retry ambiguity.
11. Summary and next step
Members are unique, scores may tie, ranks are derived, and conditional writes express update policy directly. The next lesson focuses on reading the same structure correctly by rank, score, and lexicographic range.
Check your understanding
- Can two sorted-set members have the same score?
- What does ZADD XX GT mean?
- Why is rank not stable business state?
- What integer score range is exactly representable?
- Does O(log N) ZADD eliminate hot-key risk?
Review the answers
Yes. Member identity is unique; duplicate scores are allowed.
Only update an existing member, and only if the new score is greater.
Any neighboring score change can move the member without changing its own score.
Integers from -2^53 through +2^53 inclusive.
No. A global key can still concentrate CPU, network, replication, persistence, and contention.
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