skip to content

What is a Redis Sorted Set, how is the ordering of its elements defined, and which commands do you use to add an element, read its score, read its rank and read a range?

level: juniorimportance: must knowfreq 74%

answer

  1. member + double score, unique members
  2. order = score asc, then member bytes
  3. ZRANK is 0-based
  4. ZSCORE O(1), ZADD/ZRANK O(log N)
  5. ZRANGE 6.2: REV / BYSCORE / BYLEX / LIMIT

basics

~20 s

A Sorted Set holds unique string members, each with a floating-point score, kept ordered by score ascending and by member bytes when scores tie. ZADD adds or updates, ZSCORE reads a score, ZRANK gives the 0-based position, ZRANGE reads a slice, ZCARD counts.

solid answer

~50 s

A Sorted Set is a set of **unique members**, each carrying a **64-bit floating-point score**. Redis keeps the members permanently ordered by score ascending; when two members share a score, they are ordered by the **lexicographic byte order of the member itself**, which makes ordering fully deterministic. Core commands: - `ZADD key score member [...]` — insert or update a score, O(log N). - `ZSCORE key member` — the member's score, O(1); `ZMSCORE` for several. - `ZINCRBY key delta member` — atomic score increment. - `ZRANK key member` / `ZREVRANK` — **0-based** position ascending / descending, O(log N). - `ZRANGE key start stop [REV] [BYSCORE|BYLEX] [LIMIT o c] [WITHSCORES]` — the general range reader since 6.2. - `ZCARD`, `ZCOUNT min max`, `ZREM`, `ZREMRANGEBYSCORE`. What you get that a plain Set does not: ordering, rank lookup, and range queries by score. What you pay: O(log N) writes instead of O(1) and more memory per member. Removing the last member deletes the key, and a missing key reads as empty.

code

text · 21 lines
text
> ZADD scores 10 alice 30 bob 20 carol
(integer) 3
> ZADD scores 50 alice        # updates, does not duplicate
(integer) 0                   # 0 = added nothing (a score changed)
> ZSCORE scores alice
"50"
> ZRANK scores carol          # 0-based, ascending
(integer) 1
> ZRANGE scores 0 -1 WITHSCORES
1) "alice"   -> wait, order is by score asc: carol(20)? 
> ZRANGE scores 0 -1 WITHSCORES
1) "alice"
2) "10"
> ZRANGE scores 0 1 REV WITHSCORES   # top 2 by score
1) "alice"
2) "50"
3) "bob"
4) "30"
> ZRANGE scores (20 +inf BYSCORE     # strictly above 20
1) "bob"
2) "alice"

go deeper

for a junior

Define the type — unique members plus a numeric score, always sorted — and name ZADD, ZSCORE, ZRANK and ZRANGE with the fact that ranks are 0-based.

for a middle

Add the tiebreak rule, the O(log N) versus O(1) cost table, the ZADD return-value semantics, and the 6.2 unification of range commands under ZRANGE.

for a senior

Emphasise determinism (score then member bytes) as the reason ranks agree across replicas, double precision limits when packing composite scores, and bounding range reads so they do not become O(N) commands.

for a principal

Position the type against alternatives on a cost basis — ordering and rank bought with logarithmic writes and higher per-member memory — and be explicit about where that budget stops paying off at scale.

## The data model A Sorted Set (`ZSET`) combines two ideas: **set semantics** on the members, and a **total order** derived from a numeric score attached to each member. - **Members are unique.** `ZADD lb 10 alice` followed by `ZADD lb 20 alice` does not create a second entry; it updates alice's score to 20. The first call returns 1 (one new member added), the second returns 0 (nothing added — a score was updated). That return value trips people up constantly; see `CH` below. - **Scores are IEEE-754 doubles.** They can be negative, fractional, and the special values `+inf` and `-inf`. Integers are represented exactly up to 2^53; beyond that you silently lose precision, which matters when people pack composite values into a score. - **Ordering is score ascending, then member lexicographically.** The tiebreak is not a detail — it is what makes rank a deterministic function of the data, identical on the primary and every replica. Internally Redis maintains both a map from member to score (so `ZSCORE` is O(1)) and an ordered structure (so ranges and ranks are O(log N)). You do not need to reason about that representation to use the type, but it explains the cost table. ## Writing `ZADD key score member` adds a member or updates its score. It accepts many score/member pairs in one call, which is the right way to bulk-load. It also takes flags — `NX`, `XX`, `GT`, `LT`, `CH`, `INCR` — that turn it into a conditional update; those are worth learning separately because they eliminate whole classes of read-modify-write races. `ZINCRBY key delta member` adds `delta` to the member's score atomically, creating it at `delta` if absent. This is the standard way to accumulate — vote counts, points, event tallies — without a read-then-write race. `ZREM key member [...]` removes members. `ZREMRANGEBYSCORE key min max` removes everything in a score window — the standard pruning tool for time-series-shaped data where the score is a timestamp. When the last member goes, the key is deleted. ## Reading `ZSCORE key member` returns the score as a bulk string (or nil if absent) in O(1). `ZMSCORE` (6.2) does several members in one round trip. `ZRANK key member` returns the **0-based** index of the member in ascending order; `ZREVRANK` does the same descending. Both are O(log N) and return nil for an absent member. Zero-based is the single most common off-by-one in leaderboard code — display rank is `ZREVRANK + 1`. `ZRANGE key start stop` returns members by index. Indexes are 0-based and may be negative (`-1` is the last element), so `ZRANGE key 0 -1` is the whole set in ascending order and `ZRANGE key 0 9 REV` is the top ten descending. Add `WITHSCORES` to get score/member pairs. Since **Redis 6.2**, `ZRANGE` absorbed the older family: `BYSCORE` makes `start`/`stop` a score interval, `BYLEX` makes them a lexicographic interval, `REV` reverses direction, and `LIMIT offset count` paginates (only valid with `BYSCORE`/`BYLEX`). The older `ZRANGEBYSCORE`, `ZREVRANGE`, `ZREVRANGEBYSCORE` and `ZRANGEBYLEX` still work but are deprecated in favour of the unified command. Score intervals are inclusive by default and take `(` for exclusive, plus `-inf`/`+inf`: `ZRANGE key (100 +inf BYSCORE` means "strictly above 100". `ZCARD key` is O(1). `ZCOUNT key min max` counts a score window in O(log N) — it uses the ordered structure rather than scanning. ## Cost table worth memorising | Operation | Cost | |---|---| | `ZADD`, `ZINCRBY`, `ZREM` | O(log N) | | `ZSCORE`, `ZCARD` | O(1) | | `ZRANK`, `ZREVRANK`, `ZCOUNT` | O(log N) | | `ZRANGE` returning M elements | O(log N + M) | | `ZREMRANGEBYSCORE` removing M | O(log N + M) | The important reading is that **rank and range-start are cheap regardless of size**, but anything proportional to the number of returned elements is under your control only if you bound it. `ZRANGE key 0 -1` on a ten-million-member set is the same mistake as `SMEMBERS` on a huge Set. ## When to choose it Reach for a Sorted Set when you need any of: a ranking ("what position is this user in"), a range query over a numeric attribute ("events between these two timestamps"), a priority ordering ("lowest score first"), or a set with a natural sort. If you only need membership, a Set is cheaper; if you need a value per key, a Hash is cheaper; if you need insertion order and push/pop at the ends, a List is cheaper.

  • Two members have the same score. Which one comes first, and why does it matter?
    The one whose member string sorts first by raw byte comparison. It matters because it makes ordering and therefore rank fully deterministic — the primary and every replica produce identical results, and repeated reads are stable rather than arbitrary. It also means ties are broken by an attribute that usually has no business meaning, so if tie order is user-visible you should encode a real tiebreaker such as an earlier timestamp into the score itself.
  • Why does ZADD return 0 when you update an existing member's score, and how do you detect a change?
    By default ZADD's reply counts only members that were newly added, so an update reports 0 even though the data changed. Passing the CH flag changes the reply to count members added *or* changed, which is what you want when the caller needs to know whether anything happened. If you also need the resulting value, the INCR flag makes ZADD behave like ZINCRBY and return the new score.
  • When would you use a Hash or a plain Set instead of a Sorted Set?
    Use a Set when you only ever ask membership questions — it is O(1) for add and test and uses less memory per element. Use a Hash when each element needs a payload of fields rather than a single numeric ordering key. Choose a Sorted Set only when you genuinely need rank, range-by-score, or a maintained ordering, because you pay O(log N) writes and extra per-member memory for that capability.

A race result board: each runner appears once with a time, the board is always sorted by time, and runners with identical times are listed alphabetically so the order never wobbles.

saying these in an interview costs you the question

  • Treating ZRANK as 1-based and reporting off-by-one ranks to users
  • Assuming ZADD on an existing member creates a duplicate entry or fails
  • Thinking scores must be integers, or that arbitrarily large integers survive as scores (doubles are exact only to 2^53)
  • Believing ties are ordered by insertion time rather than by member byte order
  • Reading a whole large Sorted Set with ZRANGE key 0 -1 because 'it is sorted anyway'

context