# Sets

> Unordered collections of unique strings with fast membership tests and set algebra.

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


A set is an unordered collection of unique strings. Adding a member that
is already there changes nothing, and checking whether a member is present
takes constant time however large the set is. Sets are a natural fit for
tags on an object, the unique visitors of a page, who follows whom, and
any question of the form "which items are in both of these groups?".

If you need the members in a particular order or with a score, use a
[sorted set](https://docs.hotkv.com/data-types/sorted-sets/index.md). If you only need an approximate
count of unique items and want it in a few kilobytes, use a
[HyperLogLog](https://docs.hotkv.com/data-types/hyperloglog/index.md).

## Create, read, update, delete

Adding to a missing key creates the set, and removing its last member
deletes it. [SMEMBERS](https://docs.hotkv.com/commands/smembers/index.md) returns members in no
particular order.

```
127.0.0.1:6379> SADD article:7:tags "redis" "databases" "caching"
(integer) 3
127.0.0.1:6379> SADD article:7:tags "caching"
(integer) 0
127.0.0.1:6379> SISMEMBER article:7:tags "redis"
(integer) 1
127.0.0.1:6379> SMISMEMBER article:7:tags "redis" "python"
1) (integer) 1
2) (integer) 0
127.0.0.1:6379> SCARD article:7:tags
(integer) 3
127.0.0.1:6379> SREM article:7:tags "databases"
(integer) 1
127.0.0.1:6379> SMEMBERS article:7:tags
1) "redis"
2) "caching"
```

Commands that return members (`SMEMBERS`, `SINTER`, `SUNION`, ...) return
them in no defined order, so yours may come back differently.

Other ways to read and change a set:

- [SRANDMEMBER](https://docs.hotkv.com/commands/srandmember/index.md) returns random members without
  removing them; [SPOP](https://docs.hotkv.com/commands/spop/index.md) removes and returns them.
- [SMOVE](https://docs.hotkv.com/commands/smove/index.md) moves a member from one set to another
  atomically.
- [SSCAN](https://docs.hotkv.com/commands/sscan/index.md) iterates a large set in pages, so you never
  pull millions of members in one reply.

## Set algebra

Combine sets on the server instead of downloading them:

```
127.0.0.1:6379> SADD user:1:follows "ann" "bob" "cid"
(integer) 3
127.0.0.1:6379> SADD user:2:follows "bob" "cid" "dee"
(integer) 3
127.0.0.1:6379> SINTER user:1:follows user:2:follows
1) "bob"
2) "cid"
127.0.0.1:6379> SDIFF user:1:follows user:2:follows
1) "ann"
127.0.0.1:6379> SINTERCARD 2 user:1:follows user:2:follows
(integer) 2
127.0.0.1:6379> SUNIONSTORE everyone user:1:follows user:2:follows
(integer) 4
```

[SUNION](https://docs.hotkv.com/commands/sunion/index.md), [SINTER](https://docs.hotkv.com/commands/sinter/index.md) and
[SDIFF](https://docs.hotkv.com/commands/sdiff/index.md) return the result; the `...STORE` forms write it
to a destination key and return its size. [SINTERCARD](https://docs.hotkv.com/commands/sintercard/index.md)
returns only the size of the intersection, and its `LIMIT` option stops
counting early when you only need to know "at least N".

In a [cluster](https://docs.hotkv.com/clustering/index.md), multi-key commands need all their keys in the
same hash slot. Give related sets a common hash tag, for example
`{user:1}:follows` and `{user:1}:blocked`.

## Common patterns

**Unique visitors.** `SADD visitors:2026-10-02 <user-id>` once per visit;
`SCARD` gives the exact count of distinct visitors that day.

**Mutual friends and recommendations.** `SINTER` of two follow sets gives
the people both follow; `SDIFF` gives the ones only one of them follows.

**Deduplicating work.** `SADD seen:urls <url>` replies `1` the first time
and `0` after that, so a crawler can skip URLs it has already queued.

**Random picks.** `SRANDMEMBER` with a count draws samples; `SPOP` draws
winners that should not be drawn again.

## Expiry

A time to live applies to the whole set. Single members cannot expire. For
members that should age out on their own, use a sorted set scored by time
and trim it with `ZREMRANGEBYSCORE`, or a [hash](https://docs.hotkv.com/data-types/hashes/index.md) with
field TTLs.

## Limits and performance

Each member is a string up to `proto-max-bulk-len` (512 MB by default).
`SADD`, `SREM` and `SISMEMBER` take constant time per member. `SMEMBERS` and
the set algebra commands touch every member involved, so on large sets
prefer `SSCAN`, `SINTERCARD` with a `LIMIT`, or the `...STORE` forms that
keep the result on the server.

## Commands

| Command | What it does |
| --- | --- |
| [SADD](https://docs.hotkv.com/commands/sadd/index.md) | Add members |
| [SREM](https://docs.hotkv.com/commands/srem/index.md) | Remove members |
| [SISMEMBER](https://docs.hotkv.com/commands/sismember/index.md) / [SMISMEMBER](https://docs.hotkv.com/commands/smismember/index.md) | Test membership |
| [SCARD](https://docs.hotkv.com/commands/scard/index.md) | Number of members |
| [SMEMBERS](https://docs.hotkv.com/commands/smembers/index.md) | All members |
| [SSCAN](https://docs.hotkv.com/commands/sscan/index.md) | Iterate members in pages |
| [SRANDMEMBER](https://docs.hotkv.com/commands/srandmember/index.md) | Random members |
| [SPOP](https://docs.hotkv.com/commands/spop/index.md) | Remove and return random members |
| [SMOVE](https://docs.hotkv.com/commands/smove/index.md) | Move a member to another set |
| [SUNION](https://docs.hotkv.com/commands/sunion/index.md) / [SUNIONSTORE](https://docs.hotkv.com/commands/sunionstore/index.md) | Union |
| [SINTER](https://docs.hotkv.com/commands/sinter/index.md) / [SINTERSTORE](https://docs.hotkv.com/commands/sinterstore/index.md) | Intersection |
| [SINTERCARD](https://docs.hotkv.com/commands/sintercard/index.md) | Size of the intersection |
| [SDIFF](https://docs.hotkv.com/commands/sdiff/index.md) / [SDIFFSTORE](https://docs.hotkv.com/commands/sdiffstore/index.md) | Difference |

## From your application

**Python**

```python
import hotkv

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

client.sadd("user:1:follows", "ann", "bob", "cid")
client.sadd("user:2:follows", "bob", "cid", "dee")
print(client.sismember("user:1:follows", "ann"))         # True
print(client.sinter("user:1:follows", "user:2:follows"))  # {'bob', 'cid'}
print(client.scard("user:1:follows"))                     # 3
client.close()
```

**Node.js**

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

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

await client.sAdd("user:1:follows", "ann", "bob", "cid");
await client.sAdd("user:2:follows", "bob", "cid", "dee");
console.log(await client.sIsMember("user:1:follows", "ann"));
console.log(await client.sInter("user:1:follows", "user:2:follows"));
console.log(await client.sCard("user:1:follows"));
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.SAdd(ctx, "user:1:follows", "ann", "bob", "cid")
client.SAdd(ctx, "user:2:follows", "bob", "cid", "dee")
ok, _ := client.SIsMember(ctx, "user:1:follows", "ann").Result()
both, _ := client.SInter(ctx, "user:1:follows", "user:2:follows").Result()
fmt.Println(ok, both)
```

**Java**

```java
import com.hotkv.HotKV;

try (HotKV client = HotKV.create("hotkv://localhost:6379")) {
    client.sadd("user:1:follows", "ann", "bob", "cid");
    client.sadd("user:2:follows", "bob", "cid", "dee");
    System.out.println(client.sisMember("user:1:follows", "ann"));
    System.out.println(client.sinter("user:1:follows", "user:2:follows"));
    System.out.println(client.scard("user:1:follows"));
}
```
