Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~16 min· topic 5 of 6

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.

0/6 · 0%

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

  1. 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 (with NX, XX, GT, LT, CH, INCR), ZINCRBY, ZSCORE, ZMSCORE, ZRANK/ZREVRANK (with WITHSCORE in 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. ZREVRANGE and ZRANGEBYSCORE still work but are deprecated in favour of ZRANGE options.

  2. 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 ZSCORE is O(1)). That's why both "top 100" and "my rank" are cheap even with 10M members.

  3. 03

    Complexity: ZADD/ZREM/ZINCRBY/ZRANK are O(log N); ZRANGE is O(log N + M) where M is the number of returned elements. "Top 100" is cheap; "everyone" is O(N).

  4. 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 with ZREMRANGEBYSCORE), "online users in the last 5 minutes", and time-ordered feeds.

  5. 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.

  6. 06

    ZADD ... GT only 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

leaderboard.redisredis
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"
skiplist.mermaiddiagram

A skiplist keeps express lanes over the ordered list; spans on each link let Redis compute rank in O(log N).

Rendering diagram…
around-me.pypython

"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

01

How do you show "top 1%" for players far down the list?

02

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

01

Explain how a sorted set can return both a member's score in O(1) and its rank in O(log N).

02

Give three uses of a sorted set where the score is a timestamp.

Practice

01

Build a "trending products in the last hour" list: record views and return the top 10.

02

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 ZRANGE syntax with BYSCORE, REV and LIMIT.

  • I know at least four timestamp-scored patterns.