Set Operations: SINTER, SUNION, SDIFF

Redis sets store unordered collections of unique strings, and one of their most powerful features is server-side set algebra: intersection, union, and difference computed entirely inside Redis without pulling every element back to your application first. SINTER, SUNION, and SDIFF let you answer questions like "which users belong to both groups?" or "what is the combined membership of these three lists?" in a single round trip. Because Redis is single-threaded, each of these operations runs atomically — no other command can interleave mid-computation and change a set out from under you. This lesson covers how the operations work internally, their persisted *STORE variants, the newer SINTERCARD command, and the mistakes that trip people up in production.

Overview / How It Works

A Redis set (SADD, SREM, SMEMBERS, …) is backed internally by one of two encodings depending on size and content: a compact intset when every member is an integer and the set is small, or a hash table (via listpack for small sets, then a full hash table once it grows) for general string members. Membership tests ("is X in this set?") are O(1) average case against a hash table, and that single fact is what makes multi-set operations efficient.

SINTER computes the intersection of two or more sets. Internally, Redis sorts the input sets by cardinality (smallest first), then iterates only the smallest set, checking each of its members against every other set with an O(1) lookup. This keeps the cost close to the size of the smallest input rather than the sum of all inputs, and it means order of your key arguments doesn’t matter for performance — Redis reorders internally.

SUNION and SDIFF can’t take that shortcut: a union or difference must inspect every element of every input set at least once, so their cost is proportional to the total number of elements across all sets involved. SDIFF computes members present in the first set that are absent from all the others — argument order changes the result, unlike SINTER and SUNION, which are order-independent in what they return (though not necessarily in the order elements are printed, since sets are unordered).

Each of these three commands has a persisted twin — SINTERSTORE, SUNIONSTORE, and SDIFFSTORE — which write the computed result into a destination key as a brand-new set instead of returning it to the client. This is useful for caching an expensive multi-set computation so repeated reads don’t recompute it. Two behaviors to know cold: the destination key is always overwritten, regardless of what type of value it held before, and if the computed result is empty, Redis deletes the destination key rather than leaving behind an empty set.

Redis 7.0 added SINTERCARD, which computes the same intersection as SINTER but returns only the count of matching members instead of transferring every member back to the client — much cheaper on the network when you only need a number, and it supports an optional LIMIT to stop early once a threshold is reached.

Syntax

SINTER key [key ...]
SUNION key [key ...]
SDIFF key [key ...]
SINTERSTORE destination key [key ...]
SUNIONSTORE destination key [key ...]
SDIFFSTORE destination key [key ...]
SINTERCARD numkeys key [key ...] [LIMIT limit]
Command Time Complexity What it does
SINTER O(N*M) N = size of the smallest set, M = number of sets. Returns intersection members.
SUNION O(N) N = total elements across all given sets. Returns union members.
SDIFF O(N) N = total elements across all given sets. Returns members in the first set absent from the rest.
SINTERSTORE O(N*M) Same as SINTER, plus writes the result to destination.
SUNIONSTORE / SDIFFSTORE O(N) Same as the non-store command, plus writes the result to destination.
SINTERCARD O(N*M) Same cost as computing the intersection, but returns only a count (can stop early with LIMIT).
  • key — one or more existing set keys. A missing key is treated as an empty set, not an error.
  • destination — the key that will receive the resulting set (for the *STORE variants). Always overwritten.
  • numkeys — for SINTERCARD, the count of key arguments that follow (required so Redis knows where keys end and options like LIMIT begin).
  • LIMIT limit — optional for SINTERCARD; stop counting once this many matches are found (0 means no limit).

Examples

Example 1: Basic intersection, union, and difference

SADD fruits:basket1 apple banana cherry
SADD fruits:basket2 banana cherry date
SINTER fruits:basket1 fruits:basket2
SUNION fruits:basket1 fruits:basket2
SDIFF fruits:basket1 fruits:basket2
SDIFF fruits:basket2 fruits:basket1

Output:

(integer) 3
(integer) 3
1) "banana"
2) "cherry"
1) "apple"
2) "banana"
3) "cherry"
4) "date"
1) "apple"
1) "date"

The two SADD calls report how many new members were added (3 each). SINTER returns the two fruits present in both baskets. SUNION returns all four distinct fruits across both. Note the two SDIFF calls with swapped argument order return different results: apple is unique to basket1, date is unique to basket2 — this is the order-sensitivity of SDIFF. (Sets are unordered, so the exact printed order of members can vary between Redis versions and encodings even though the membership is identical.)

Example 2: Finding shared and unique interests

SADD user:1001:interests reading hiking cooking
SADD user:1002:interests hiking gaming cooking
SADD user:1003:interests reading gaming
SINTER user:1001:interests user:1002:interests
SUNION user:1001:interests user:1002:interests user:1003:interests
SDIFF user:1001:interests user:1003:interests

Output:

(integer) 3
(integer) 3
(integer) 2
1) "hiking"
2) "cooking"
1) "reading"
2) "hiking"
3) "cooking"
4) "gaming"
1) "hiking"
2) "cooking"

This is a realistic "people you may know" or "shared interests" pattern. SINTER shows user 1001 and 1002 both like hiking and cooking. SUNION across all three users gives the full distinct interest pool for building a combined recommendation list. SDIFF shows what 1001 is interested in that 1003 is not — useful for "things this person might introduce you to."

Example 3: Persisting a result with SINTERSTORE and counting with SINTERCARD

SADD group:admins:online alice bob carol
SADD group:moderators:online bob carol dave
SINTERSTORE group:staff:online:common group:admins:online group:moderators:online
SMEMBERS group:staff:online:common
EXPIRE group:staff:online:common 60
TTL group:staff:online:common
SINTERCARD 2 group:admins:online group:moderators:online

Output:

(integer) 3
(integer) 3
(integer) 2
1) "bob"
2) "carol"
(integer) 1
(integer) 60
(integer) 2

SINTERSTORE computes the same intersection as SINTER but writes it into group:staff:online:common as a real set and returns the count of elements stored (2). We then attach a 60-second TTL so this cached "currently-online staff" snapshot naturally expires and gets recomputed rather than going stale forever. Finally, SINTERCARD 2 ... recomputes the same intersection but returns only the count (2) — no members transferred — which is the cheaper choice when a UI just needs to show "2 people online".

How It Works Step by Step

For SINTER key1 key2 key3, Redis: (1) looks up the cardinality of each set without reading its contents; (2) picks the smallest one as the "driver" set; (3) iterates that driver set’s members one at a time; (4) for each member, checks whether it exists in every other input set using an O(1) hash lookup, short-circuiting as soon as one set doesn’t contain it; (5) collects the members that survived every check and returns them (or writes them to destination for SINTERSTORE). This is why intersecting a 10-element set against a 10-million-element set is cheap — Redis never scans the huge set. SUNION and SDIFF instead build an internal hash table by walking every element of every input set once (for union, adding each; for diff, adding the first set’s elements then removing anything found in the others), so their cost scales with total input size, not the smallest set.

Common Mistakes

1. Running a set command against a key that isn’t a set. Every Redis key has exactly one type, and mixing types raises an error:

SET config:site:theme "dark"
SADD config:site:features darkmode notifications
SINTER config:site:theme config:site:features

Output:

OK
(integer) 2
(error) WRONGTYPE Operation against a key holding the wrong kind of value

config:site:theme holds a plain string, not a set, so SINTER refuses to run. Fix it by using a genuinely set-typed key, e.g. SADD config:site:theme:tags dark instead of SET.

2. Assuming SDIFF is symmetric. Because SDIFF subtracts every other set from the first argument, swapping the order changes the answer entirely:

SADD team:frontend alice bob carol
SADD team:backend bob carol dave
SDIFF team:frontend team:backend
SDIFF team:backend team:frontend

Output:

(integer) 3
(integer) 3
1) "alice"
1) "dave"

If you meant "who is only on backend," you need the second form, not the first — always put the "base" set you’re subtracting from first.

3. Forgetting that *STORE overwrites the destination key unconditionally.

SET report:daily:cache "cached report - do not overwrite"
SADD users:active alice bob
SADD users:premium bob carol
SINTERSTORE report:daily:cache users:active users:premium
GET report:daily:cache

Output:

OK
(integer) 2
(integer) 2
(integer) 1
(error) WRONGTYPE Operation against a key holding the wrong kind of value

SINTERSTORE silently destroyed the cached string and replaced it with a one-member set, so the following GET now errors. Always pick a destination key namespace that nothing else writes to, e.g. cache:intersect:....

4. Not knowing that an empty *STORE result deletes the destination key.

SADD colors:warm red orange yellow
SADD colors:cool blue green purple
SINTERSTORE colors:overlap colors:warm colors:cool
EXISTS colors:overlap

Output:

(integer) 3
(integer) 3
(integer) 0
(integer) 0

Because the two color sets don’t overlap, SINTERSTORE reports 0 elements stored and deletes colors:overlap entirely rather than leaving an empty set behind. If your code branches on "does this key exist," account for this — don’t assume a prior successful *STORE guarantees the key still exists on a later check.

Best Practices

  • Reach for SINTERCARD instead of SINTER when you only need a count — it avoids transferring potentially large member lists over the network.
  • Cache expensive multi-set computations with *STORE plus an EXPIRE, so repeated reads hit a precomputed set instead of recomputing the intersection/union/diff every request.
  • Remember SDIFF (and SDIFFSTORE) are not commutative — the first key is the "base" set being subtracted from.
  • Never write a *STORE result into a key your application also uses for another purpose; give computed sets their own namespace, like cache:... or derived:....
  • Check EXISTS rather than assuming a destination key is present after a *STORE call — an empty result deletes it.
  • For very large sets (millions of members), avoid running these commands synchronously in a hot request path; Redis is single-threaded, so a huge SUNION or SDIFF blocks every other client while it runs.
  • Use consistent, colon-namespaced key names (user:1001:interests, team:backend) so it’s obvious at a glance which domain each set represents.

Practice Exercises

  • You track page:article42:likes and page:article42:shares, both sets of usernames. Write the command to find users who both liked and shared the article, and a separate command to find the total distinct set of everyone who engaged at all.
  • You maintain skills:job123:required and skills:candidate789:has, both sets of skill names. Write the single command that tells you which required skills the candidate is missing. Think carefully about which key goes first.
  • You want to know how many users are in both segment:beta_testers and segment:power_users without downloading the full member list, and you want that count cached for reuse for the next 5 minutes. Which two commands do you run, in what order?

Summary

  • SINTER, SUNION, and SDIFF compute set intersection, union, and difference entirely on the server, in one atomic round trip.
  • SINTER is O(N*M) driven by the smallest input set; SUNION and SDIFF are O(N) over the total elements across all inputs.
  • SDIFF is order-sensitive — it returns members of the first set missing from the rest; SINTER and SUNION are not.
  • SINTERSTORE/SUNIONSTORE/SDIFFSTORE persist the result into a destination key, always overwriting it, and delete it if the result is empty.
  • SINTERCARD (Redis 7.0+) returns just the intersection count, cheaper than SINTER when you don’t need the members themselves.
  • Mixing set commands with non-set keys raises a WRONGTYPE error — every key has exactly one type.