jonnify·redis /redis-patterns/time-delay-priority/leaderboards/zadd-and-zrank

R4.05.1 · Leaderboards — dive 1

ZADD and ZREVRANK.

A player's score goes in with ZADD; the rank comes out with ZREVRANK. The score is the ranking key — the set orders by it, and the rank is a position in that order, computed on read.

Grounding: echo/apps/codemojex/lib/codemojex/board.ex · Codemojex.Board.record/3 — the best-of ZADD to cm:<game>:board via the real EchoWire.Cmd builders.

One game's board, eight players by score. Select a player to light their row; the readout names their ZREVRANK (0 = leader) and its mirror ZRANK.
0—— 1—— 2—— 3—— 4—— 5—— 6—— 7——
readout · Select a player. ZREVRANK counts from the top (0 = leader); ZRANK is the mirror from the bottom, and the two add up to N−1.

The leader is vega at 540 (ZREVRANK 0); the trailing player is nori at 180 (ZREVRANK 7). No row stores a rank.

§1

ZADD — the score is the write

leaderboards.md.txt · Core Operations

A sorted set holds members, each with a numeric score, and keeps them ordered by score. The whole write is ZADD: a score and a member. Run ZADD on a member already on the board and its score is overwritten, and the member is re-placed in the order. The score is the entire index — there is no separate ranking column. A player's own score is read with ZSCORE, which is O(1).

ZADD leaderboard 540 "vega"     # write a member and a score
ZADD leaderboard 510 "mango"    # re-running ZADD overwrites and re-places
ZSCORE leaderboard "vega"       # => 540  (O(1))

After three writes, reading the set high-to-low gives the leader first; reading it low-to-high gives the trailing player. The set maintains the order — nothing was sorted by hand.

There is no insert versus update: one verb does both. The score is the write, and the order is maintained for free.

§2

ZREVRANK — the rank is a position

leaderboards.md.txt · Rank vs Reverse Rank

A rank is a player's position in the order, counted from one end. ZREVRANK counts from the top, so the leader is rank 0. The rank is 0-based: rank 0 is first place, rank 1 is second. The set computes it by walking the order to the member, which is O(log N). The rank is not stored anywhere — it is derived from the score order on each read, so it is always exactly the current order; there is no snapshot to fall out of date.

Write a score, then read the rank. Pick a new score for sora and watch the board re-place her — the ZREVRANK falls out of the new order.
readout · Pick a score to ZADD for sora; the board re-orders and her ZREVRANK is read from the new position.

Each write is one ZADD; the rank is never written. A higher score moves sora up, and every player she passes drops a place — none of them is rewritten, only their computed rank differs on the next read.

§3

ZRANK — the mirror from the bottom

leaderboards.md.txt · Rank vs Reverse Rank

ZRANK is the same position counted from the other end — low-to-high, so the lowest score is rank 0. For a board where higher is better, ZREVRANK is the rank a player wants to see; ZRANK answers the opposite question. The two are mirrors of one position in the same order: if the board holds N players, a member's ZRANK and ZREVRANK add up to N−1. Reading either end leaves the set unchanged.

ZREVRANK leaderboard "vega"     # => 0   (top of the order)
ZRANK    leaderboard "vega"     # => 7   (the same position, from the bottom)
§4

In codemojex — ZADD in Board.record/3

echo/apps/codemojex · board.ex · scoring.ex

codemojex is a Telegram emoji-guessing game on the same stack; its leaderboard is exactly this. The competitive state is in Valkey, the board is one sorted set per game keyed cm:<game>:board, and the score is the linear total from Codemojex.Scoring — points = 100 - 20 * d per emoji at distance d, summed over six positions, out of 600. When a guess is scored, Codemojex.Board.record/3 writes the player's best linear total to the board.

echo/apps/codemojex/lib/codemojex/board.ex · record/3, the best-of ZADDverbatim
old = hget_int(conn, k(game, "base"), player)
new_base = max(old, base)                          ← fold to the best-so-far
Cmd.hset(k(game, "base"), player, to_string(new_base)) |> Wire.run(conn)
Cmd.zadd(k(game, "board")) |> Cmd.score(new_base, player) |> Wire.run(conn)   ← ZADD cm:<game>:board score player

Notes on ValkeyZADD key score member adds or updates one member in O(log N) and keeps the set ordered; ZREVRANK reads a 0-based rank, also O(log N) — valkey.io/commands/zadd.

Cmd.zadd/1 and Cmd.score/3 are the real EchoWire.Cmd builders; k(game, "board") is cm:<game>:board. The write is a best-of overwrite: ZADD re-places the player at their highest base, never lowering it — the raw linear best is the sole rank, no tier ladder, no first-mover bonus. The rank is read back later from the order, not stored.

/echomq/queue · the queue's score-ordered work

Where EchoMQ ranks for real

A leaderboard is a generic sorted-set ranking, not the queue protocol. Where EchoMQ orders by a score is the Queue pillar — work ranked by a packed schedule and priority score in the emq:{q}: keyspace. Open the Queue pillar for the queue ranking built on the same score order.

the pattern

ZADD writes a score and ZREVRANK reads a 0-based rank from the score order, computed on read.

its codemojex application

Codemojex.Board.record/3 writes each player's best linear total with Cmd.zadd("cm:<game>:board") |> Cmd.score(new_base, player); the rank is the player's position in that order on read.

The score is the write and the rank is the read; nothing stores a rank. codemojex's Board.record/3 is that ZADD, with a best-of fold so a player's rank only ever climbs.

§5

References

sources · related

Sources

  • Redis — ZADD — add or update a member's score; the single write that places a player in the order.
  • Redis — ZREVRANK — a member's rank, 0-based from the top, computed on read.
  • Redis — Sorted sets — the data type behind the board: members ordered by a numeric score.

Related in this course