Topic 1.5
Sorted Sets: Rankings, Time Windows, and Priority
In one line
A sorted set keeps unique members ordered by a floating-point score, with O(log N) inserts, rank queries and range queries. It powers leaderboards, sliding-window rate limiters, delayed-job schedulers, priority queues and "latest N" timelines.
Think of it like this
A race-results board that re-sorts itself instantly whenever a runner's time changes. You can ask "who's in the top 10?", "what place is runner 57?", or "who finished between 2 and 3 hours?", and every answer comes back without re-sorting the whole list.
Key ideas
- 01
The model is member → score. Members are unique strings; scores are 64-bit floats (integers are exact up to 2^53). Ties are broken by lexicographic order of the member. Commands:
ZADD(withNX,XX,GT,LT,CH,INCR),ZINCRBY,ZSCORE,ZMSCORE,ZRANK/ZREVRANK(withWITHSCOREin 7.2),ZRANGE(unified since 6.2:BYSCORE,BYLEX,REV,LIMIT),ZCOUNT,ZREM,ZREMRANGEBYSCORE/BYRANK,ZPOPMIN/ZPOPMAX,BZPOPMIN,ZMPOP(7.0),ZRANGESTORE,ZUNIONSTORE/ZINTERSTORE,ZSCAN.ZREVRANGEandZRANGEBYSCOREstill work but are deprecated in favour ofZRANGEoptions. - 02
Internals: small sorted sets (≤ 128 members, values ≤ 64 bytes) are listpacks. Larger ones use a skiplist (ordered by score, with span counters so rank is O(log N)) plus a hash table (member → score, so
ZSCOREis O(1)). That's why both "top 100" and "my rank" are cheap even with 10M members. - 03
Complexity:
ZADD/ZREM/ZINCRBY/ZRANKare O(log N);ZRANGEis O(log N + M) where M is the number of returned elements. "Top 100" is cheap; "everyone" is O(N). - 04
Scores as timestamps unlock time-based patterns: a delayed queue (score = run-at time, poll with
ZRANGE ... BYSCORE -inf now LIMIT 0 100), a sliding-window rate limiter (score = request time, remove old ones withZREMRANGEBYSCORE), "online users in the last 5 minutes", and time-ordered feeds. - 05
Tie-breaking with composite scores: to rank equal points by who got there first, encode both in the score, for example
points * 10^10 + (MAX_TS - ts), which stays exact while it fits in 53 bits. Or keep points in the score and order ties by a member prefix, or resolve ties in the app. - 06
ZADD ... GTonly updates when the new score is greater, perfect for "best score" leaderboards where a worse run must not lower the record, in one atomic command.
Code & diagrams
127.0.0.1:6379> ZADD lb:season:7 1200 john 900 amit 750 rahul
(integer) 3
127.0.0.1:6379> ZINCRBY lb:season:7 50 rahul
"800"
127.0.0.1:6379> ZADD lb:season:7 GT 850 amit # lower than 900: ignored
(integer) 0
127.0.0.1:6379> ZRANGE lb:season:7 0 2 REV WITHSCORES # top 3
1) "john"
2) "1200"
3) "amit"
4) "900"
5) "rahul"
6) "800"
127.0.0.1:6379> ZREVRANK lb:season:7 rahul WITHSCORE # 0-based rank
1) (integer) 2
2) "800"
127.0.0.1:6379> ZCOUNT lb:season:7 (900 +inf # strictly above 900
(integer) 1
127.0.0.1:6379> ZRANGE lb:season:7 800 1000 BYSCORE WITHSCORES
1) "rahul"
2) "800"
3) "amit"
4) "900"A skiplist keeps express lanes over the ordered list; spans on each link let Redis compute rank in O(log N).
"Show me and the 5 players above and below me": two O(log N) calls.
import redis
r = redis.Redis(decode_responses=True)
def around_me(board: str, player: str, span: int = 5):
rank = r.zrevrank(board, player) # 0 = top
if rank is None:
return []
start = max(0, rank - span)
return r.zrange(board, start, rank + span, desc=True, withscores=True)
print(around_me("lb:season:7", "rahul"))
# [('john', 1200.0), ('amit', 900.0), ('rahul', 800.0)]Interview problem
The problem
Gaming leaderboard for 10M players
Design a real-time leaderboard: 10M players, scores update constantly, show the top 100, show any player's rank, handle ties, survive one player updating 1M times/sec, and shard it if needed.
You're given
- 10M players
- Top 100 on the home screen
- My rank on the profile
- Ties by earliest achievement
- Global plus per-country boards
The interviewer follows up
How do you show "top 1%" for players far down the list?
How do you run a weekly leaderboard that resets every Monday?
When it breaks
Using a sorted set with millisecond timestamps × large multipliers as scores
What you see
Scores exceed 2^53, lose precision as floats, and tie-breaking silently breaks.
Fix & prevent
Keep composite scores under 2^53; use seconds or an offset from an epoch, and test with the maximum values.
ZRANGE board 0 -1 in an API endpoint
What you see
Returns all 10M members: seconds of blocking and gigabytes of reply buffers.
Fix & prevent
Always paginate with bounded ranges or LIMIT; use ZSCAN for background iteration.
Explain it without notes
Explain how a sorted set can return both a member's score in O(1) and its rank in O(log N).
Give three uses of a sorted set where the score is a timestamp.
Practice
Build a "trending products in the last hour" list: record views and return the top 10.
Show the 3 players above and below rahul using only two commands.
Trade-offs
- ↔
One big sorted set: exact ranks, simple, but a big hot key on one shard. Sharded boards: scale, but global rank becomes approximate or needs multi-step queries.
- ↔
Composite scores give correct ties in one structure but are harder to read and limited to 53 bits of precision.
Done when you can
I can design a leaderboard with ties, top-N, rank and periodic resets.
I can explain skiplist plus hash table internals and each operation's complexity.
I can use unified
ZRANGEsyntax withBYSCORE,REVandLIMIT.I know at least four timestamp-scored patterns.