# Sorted sets

> Unique members ordered by a numeric score, for leaderboards, priority queues, and time-ordered indexes.

URL: https://docs.hotkv.com/data-types/sorted-sets/


A sorted set is a set of unique strings where every member carries a
floating-point score, and members are kept ordered by that score (members
with equal scores are ordered by their bytes). You can fetch members by
rank, by score range, or by lexicographic range, and find any member's rank
quickly. Sorted sets power leaderboards, priority queues, scheduling
("everything due before now"), sliding-window rate limits, and secondary
indexes by number or date.

## Create, read, update, delete

[ZADD](https://docs.hotkv.com/commands/zadd/index.md) adds members with scores, or updates the score of
members that are already there. It replies with the number of new members.

```
127.0.0.1:6379> ZADD leaderboard 1200 "ann" 950 "bob" 1430 "cid"
(integer) 3
127.0.0.1:6379> ZSCORE leaderboard "bob"
"950"
127.0.0.1:6379> ZINCRBY leaderboard 300 "bob"
"1250"
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES
1) "ann"
2) "1200"
3) "bob"
4) "1250"
5) "cid"
6) "1430"
127.0.0.1:6379> ZRANGE leaderboard 0 1 REV
1) "cid"
2) "bob"
127.0.0.1:6379> ZREVRANK leaderboard "ann"
(integer) 2
127.0.0.1:6379> ZCARD leaderboard
(integer) 3
127.0.0.1:6379> ZREM leaderboard "ann"
(integer) 1
```

Ranks start at 0. [ZRANK](https://docs.hotkv.com/commands/zrank/index.md) counts from the lowest score
and [ZREVRANK](https://docs.hotkv.com/commands/zrevrank/index.md) from the highest; both accept
`WITHSCORE` to return the score too.

### ZADD options

| Option | Effect |
| --- | --- |
| `NX` | Only add new members, never update existing ones |
| `XX` | Only update existing members, never add |
| `GT` / `LT` | Only update when the new score is greater / less than the current one |
| `CH` | Reply with the number of members added or changed, not only added |
| `INCR` | Add the score to the current one, like `ZINCRBY`, and reply with the new score |

`GT` is the natural choice for "keep each player's best score".

### Range queries

[ZRANGE](https://docs.hotkv.com/commands/zrange/index.md) is the one command for all three kinds of
range. By rank (the default), by score with `BYSCORE`, or by member name
with `BYLEX`; `REV` reverses the order and `LIMIT offset count` pages
through the result.

```
127.0.0.1:6379> ZADD jobs 1767225600 "job:a" 1767229200 "job:b" 1767232800 "job:c"
(integer) 3
127.0.0.1:6379> ZRANGE jobs -inf 1767229200 BYSCORE
1) "job:a"
2) "job:b"
127.0.0.1:6379> ZRANGE jobs (1767225600 +inf BYSCORE LIMIT 0 1
1) "job:b"
127.0.0.1:6379> ZCOUNT jobs 1767229200 +inf
(integer) 2
```

A `(` before a score makes that end exclusive; `-inf` and `+inf` are open
ends. For `BYLEX`, ranges are written `[a` (inclusive), `(a` (exclusive),
`-` and `+`, and are meant for sets where every member has the same score.
The older commands ([ZRANGEBYSCORE](https://docs.hotkv.com/commands/zrangebyscore/index.md),
[ZREVRANGE](https://docs.hotkv.com/commands/zrevrange/index.md), [ZRANGEBYLEX](https://docs.hotkv.com/commands/zrangebylex/index.md),
...) still work.

## Common patterns

**Leaderboard.** `ZADD board GT <score> <player>` records a score only when
it beats the player's best, `ZRANGE board 0 9 REV WITHSCORES` shows the top
ten, and `ZREVRANK board <player>` gives a player's position.

**Priority queue.** Score by priority and pop the most urgent item with
[ZPOPMIN](https://docs.hotkv.com/commands/zpopmin/index.md) (or [ZPOPMAX](https://docs.hotkv.com/commands/zpopmax/index.md)).
[BZPOPMIN](https://docs.hotkv.com/commands/bzpopmin/index.md) and [BZMPOP](https://docs.hotkv.com/commands/bzmpop/index.md) block until
an item arrives.

**Delayed jobs.** Score each job by the Unix time it is due. A worker runs
`ZRANGE jobs -inf <now> BYSCORE LIMIT 0 10` and removes what it takes with
`ZREM`, inside a [transaction](https://docs.hotkv.com/develop/transactions/index.md) or Lua script so two
workers never take the same job.

**Sliding-window rate limit.** For each request, `ZADD` a unique member
scored by the current time in milliseconds, `ZREMRANGEBYSCORE` everything
older than the window, and `ZCARD` to count what is left. See
[Rate limiting](https://docs.hotkv.com/develop/rate-limiting/index.md) for the full recipe and HotKV's
built-in alternatives.

**Index by number or date.** Keep `ZADD users:by-signup <timestamp>
<user-id>` alongside your hashes to answer "users who signed up last week".

## Combining sorted sets

[ZUNION](https://docs.hotkv.com/commands/zunion/index.md), [ZINTER](https://docs.hotkv.com/commands/zinter/index.md) and
[ZDIFF](https://docs.hotkv.com/commands/zdiff/index.md) combine sorted sets, adding up scores by default.
`WEIGHTS` multiplies each input's scores and `AGGREGATE MIN | MAX | SUM`
picks how scores are merged. The `...STORE` forms write the result to a key,
and [ZINTERCARD](https://docs.hotkv.com/commands/zintercard/index.md) counts the intersection only. In a
[cluster](https://docs.hotkv.com/clustering/index.md), all the keys must share a hash slot.

## Expiry

A time to live applies to the whole sorted set; members cannot expire one by
one. The usual way to age out members is to score them by time and remove
old ones with [ZREMRANGEBYSCORE](https://docs.hotkv.com/commands/zremrangebyscore/index.md).

## Limits and performance

Scores are 64-bit floating-point numbers, so integers are exact up to
2^53. Members are strings up to `proto-max-bulk-len` (512 MB by default).
Adding, removing, scoring and ranking a member take logarithmic time;
range queries cost that plus the number of members returned.

## Commands

| Command | What it does |
| --- | --- |
| [ZADD](https://docs.hotkv.com/commands/zadd/index.md) | Add members or update scores |
| [ZINCRBY](https://docs.hotkv.com/commands/zincrby/index.md) | Add to a member's score |
| [ZREM](https://docs.hotkv.com/commands/zrem/index.md) | Remove members |
| [ZSCORE](https://docs.hotkv.com/commands/zscore/index.md) / [ZMSCORE](https://docs.hotkv.com/commands/zmscore/index.md) | Scores of members |
| [ZRANK](https://docs.hotkv.com/commands/zrank/index.md) / [ZREVRANK](https://docs.hotkv.com/commands/zrevrank/index.md) | Rank of a member |
| [ZCARD](https://docs.hotkv.com/commands/zcard/index.md) | Number of members |
| [ZCOUNT](https://docs.hotkv.com/commands/zcount/index.md) / [ZLEXCOUNT](https://docs.hotkv.com/commands/zlexcount/index.md) | Members in a score or lex range |
| [ZRANGE](https://docs.hotkv.com/commands/zrange/index.md) | Members by rank, score, or lex range |
| [ZRANGESTORE](https://docs.hotkv.com/commands/zrangestore/index.md) | Store a range in another key |
| [ZRANGEBYSCORE](https://docs.hotkv.com/commands/zrangebyscore/index.md) / [ZREVRANGEBYSCORE](https://docs.hotkv.com/commands/zrevrangebyscore/index.md) | Older score-range forms |
| [ZRANGEBYLEX](https://docs.hotkv.com/commands/zrangebylex/index.md) / [ZREVRANGEBYLEX](https://docs.hotkv.com/commands/zrevrangebylex/index.md) | Older lex-range forms |
| [ZREVRANGE](https://docs.hotkv.com/commands/zrevrange/index.md) | Older reverse rank-range form |
| [ZPOPMIN](https://docs.hotkv.com/commands/zpopmin/index.md) / [ZPOPMAX](https://docs.hotkv.com/commands/zpopmax/index.md) | Remove and return the lowest or highest |
| [ZMPOP](https://docs.hotkv.com/commands/zmpop/index.md) | Pop from the first non-empty sorted set |
| [BZPOPMIN](https://docs.hotkv.com/commands/bzpopmin/index.md) / [BZPOPMAX](https://docs.hotkv.com/commands/bzpopmax/index.md) / [BZMPOP](https://docs.hotkv.com/commands/bzmpop/index.md) | Blocking pops |
| [ZRANDMEMBER](https://docs.hotkv.com/commands/zrandmember/index.md) | Random members |
| [ZSCAN](https://docs.hotkv.com/commands/zscan/index.md) | Iterate members in pages |
| [ZREMRANGEBYSCORE](https://docs.hotkv.com/commands/zremrangebyscore/index.md) / [ZREMRANGEBYRANK](https://docs.hotkv.com/commands/zremrangebyrank/index.md) / [ZREMRANGEBYLEX](https://docs.hotkv.com/commands/zremrangebylex/index.md) | Remove a range |
| [ZUNION](https://docs.hotkv.com/commands/zunion/index.md) / [ZUNIONSTORE](https://docs.hotkv.com/commands/zunionstore/index.md) | Union |
| [ZINTER](https://docs.hotkv.com/commands/zinter/index.md) / [ZINTERSTORE](https://docs.hotkv.com/commands/zinterstore/index.md) | Intersection |
| [ZINTERCARD](https://docs.hotkv.com/commands/zintercard/index.md) | Size of the intersection |
| [ZDIFF](https://docs.hotkv.com/commands/zdiff/index.md) / [ZDIFFSTORE](https://docs.hotkv.com/commands/zdiffstore/index.md) | Difference |

## From your application

**Python**

```python
import hotkv

client = hotkv.HotKV(host="localhost", port=6379, decode_responses=True)

client.zadd("leaderboard", {"ann": 1200, "bob": 950, "cid": 1430})
client.zincrby("leaderboard", 300, "bob")
top = client.zrange("leaderboard", 0, 2, rev=True, withscores=True)
print(top)
print(client.zrevrank("leaderboard", "ann"))   # 2
client.close()
```

**Node.js**

```ts
import { HotKV } from "@hotkv/client";

const client = new HotKV({ url: "hotkv://127.0.0.1:6379" });

await client.zAdd("leaderboard", [["ann", "1200"], ["bob", "950"], ["cid", "1430"]]);
await client.zIncrBy("leaderboard", 300, "bob");
const top = await client.zRange("leaderboard", "0", "2", { rev: true, withScores: true });
console.log(top);
await client.close();
```

**Go**

```go
client, err := hotkv.NewClient(&hotkv.Options{Addr: "localhost:6379"})
if err != nil {
    log.Fatal(err)
}
defer client.Close()
ctx := context.Background()

client.ZAdd(ctx, "leaderboard",
    hotkv.Z{Score: 1200, Member: "ann"},
    hotkv.Z{Score: 950, Member: "bob"},
    hotkv.Z{Score: 1430, Member: "cid"})
client.ZIncrBy(ctx, "leaderboard", 300, "bob")
top, _ := client.ZRevRange(ctx, "leaderboard", 0, 2).Result()
fmt.Println(top)
```

**Java**

```java
import com.hotkv.HotKV;
import java.util.Map;

try (HotKV client = HotKV.create("hotkv://localhost:6379")) {
    client.zadd("leaderboard", Map.of("ann", 1200.0, "bob", 950.0, "cid", 1430.0));
    client.zincrBy("leaderboard", 300, "bob");
    System.out.println(client.zrevRangeWithScores("leaderboard", 0, 2));
    System.out.println(client.zrank("leaderboard", "ann"));
}
```
