Redis offers two powerful data structures for managing collections of unique items: Sets and Sorted Sets. While both store unique members, Sorted Sets add an extra dimension by associating a numerical score with each member, allowing for ordering and range queries. These are indispensable for features like user tagging, unique visitor tracking, leaderboards, and priority queues.
In this chapter, we’ll explore:
- The fundamental differences and uses of Sets.
- Commands for adding, removing, and querying members in Sets.
- Set operations like union, intersection, and difference.
- The structure and advantages of Sorted Sets.
- Commands for adding, updating, and querying members in Sorted Sets based on score or rank.
Redis Sets
A Redis Set is an unordered collection of unique strings. Think of it like a mathematical set. You can add elements, remove elements, and check for the existence of an element, but the order of elements is not guaranteed.
key -> { member1, member2, member3, ... }
Key characteristics:
- Uniqueness: Each member in a Set must be unique. Adding an existing member has no effect.
- Unordered: Elements are not stored in any particular order.
- Fast membership testing: Checking if an element is a member is an O(1) operation.
- Set operations: Supports operations like union, intersection, and difference between multiple sets.
Basic Set Commands
1. SADD key member [member ...] (Set Add)
Adds one or more members to the set stored at key. Returns the number of new members added.
2. SMEMBERS key (Set Members)
Returns all members of the set stored at key.
3. SISMEMBER key member (Set Is Member)
Returns 1 if member is a member of the set, 0 otherwise.
4. SREM key member [member ...] (Set Remove)
Removes one or more members from the set stored at key. Returns the number of members removed.
Node.js Example:
// redis-sets.js
const Redis = require('ioredis');
const redis = new Redis();
async function setBasicExample() {
try {
const uniqueUsersKey = 'online:users';
await redis.del(uniqueUsersKey); // Clear previous data
// SADD: Add unique users
let addedCount = await redis.sadd(uniqueUsersKey, 'Alice', 'Bob', 'Alice', 'Charlie');
console.log(`SADD ${uniqueUsersKey} -> Added ${addedCount} new users.`); // Output: Added 3 new users.
// SMEMBERS: Get all unique users
let onlineUsers = await redis.smembers(uniqueUsersKey);
console.log(`SMEMBERS ${uniqueUsersKey} -> ${JSON.stringify(onlineUsers.sort())}`); // Output: ["Alice", "Bob", "Charlie"] (order may vary, sorting for consistent output)
// SISMEMBER: Check if a user is online
let isAliceOnline = await redis.sismember(uniqueUsersKey, 'Alice');
console.log(`SISMEMBER ${uniqueUsersKey} Alice -> ${isAliceOnline ? 'Yes' : 'No'}`); // Output: Yes
let isDavidOnline = await redis.sismember(uniqueUsersKey, 'David');
console.log(`SISMEMBER ${uniqueUsersKey} David -> ${isDavidOnline ? 'Yes' : 'No'}`); // Output: No
// SREM: Remove a user
addedCount = await redis.srem(uniqueUsersKey, 'Bob');
console.log(`SREM ${uniqueUsersKey} Bob -> Removed ${addedCount} user.`); // Output: Removed 1 user.
onlineUsers = await redis.smembers(uniqueUsersKey);
console.log(`SMEMBERS ${uniqueUsersKey} after SREM -> ${JSON.stringify(onlineUsers.sort())}`); // Output: ["Alice", "Charlie"]
} catch (err) {
console.error('Error in setBasicExample:', err);
}
}
// setBasicExample().then(() => redis.quit());
Python Example:
# redis_sets.py
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def set_basic_example():
try:
subscribedUsersKey = 'newsletter:subscribers'
r.delete(subscribedUsersKey)
# SADD: Add subscribers
added_count = r.sadd(subscribedUsersKey, 'user1@example.com', 'user2@example.com', 'user1@example.com')
print(f"SADD {subscribedUsersKey} -> Added {added_count} new subscribers.") # Output: Added 2 new subscribers.
# SMEMBERS: Get all subscribers
subscribers = [s.decode('utf-8') for s in r.smembers(subscribedUsersKey)]
print(f"SMEMBERS {subscribedUsersKey} -> {sorted(subscribers)}") # Output: ['user1@example.com', 'user2@example.com']
# SISMEMBER: Check subscription status
isUser1Subscribed = r.sismember(subscribedUsersKey, 'user1@example.com')
print(f"SISMEMBER {subscribedUsersKey} user1@example.com -> {'Yes' if isUser1Subscribed else 'No'}") # Output: Yes
isUser3Subscribed = r.sismember(subscribedUsersKey, 'user3@example.com')
print(f"SISMEMBER {subscribedUsersKey} user3@example.com -> {'Yes' if isUser3Subscribed else 'No'}") # Output: No
# SREM: Unsubscribe a user
removed_count = r.srem(subscribedUsersKey, 'user2@example.com')
print(f"SREM {subscribedUsersKey} user2@example.com -> Removed {removed_count} subscriber.") # Output: Removed 1 subscriber.
subscribers = [s.decode('utf-8') for s in r.smembers(subscribedUsersKey)]
print(f"SMEMBERS {subscribedUsersKey} after SREM -> {sorted(subscribers)}") # Output: ['user1@example.com']
except Exception as e:
print(f"Error in set_basic_example: {e}")
# set_basic_example()
# r.close()
Set Operations
Redis offers commands to perform common set theory operations:
SINTER key [key ...]: Returns the intersection of all the sets.SUNION key [key ...]: Returns the union of all the sets.SDIFF key [key ...]: Returns the members of the first set that are not present in the other sets.
Node.js Example:
// ... (previous setup)
async function setOperationsExample() {
try {
const sportFans = 'fans:sport';
const musicFans = 'fans:music';
const movieFans = 'fans:movie';
await redis.del(sportFans, musicFans, movieFans);
await redis.sadd(sportFans, 'Alice', 'Bob', 'Charlie', 'David');
await redis.sadd(musicFans, 'Bob', 'David', 'Eve', 'Frank');
await redis.sadd(movieFans, 'Charlie', 'David', 'Eve', 'Grace');
console.log(`Sport Fans: ${JSON.stringify((await redis.smembers(sportFans)).sort())}`);
console.log(`Music Fans: ${JSON.stringify((await redis.smembers(musicFans)).sort())}`);
console.log(`Movie Fans: ${JSON.stringify((await redis.smembers(movieFans)).sort())}`);
// SINTER: Find common fans (intersection of sport and music)
let commonFans = await redis.sinter(sportFans, musicFans);
console.log(`\nCommon Sport & Music Fans (SINTER): ${JSON.stringify(commonFans.sort())}`); // Output: ["Bob", "David"]
// SUNION: Find all unique fans (union of sport, music, movie)
let allFans = await redis.sunion(sportFans, musicFans, movieFans);
console.log(`All unique Fans (SUNION): ${JSON.stringify(allFans.sort())}`); // Output: ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace"]
// SDIFF: Fans who like sport but not music
let sportOnlyFans = await redis.sdiff(sportFans, musicFans);
console.log(`Sport Fans only (SDIFF): ${JSON.stringify(sportOnlyFans.sort())}`); // Output: ["Alice", "Charlie"]
} catch (err) {
console.error('Error in setOperationsExample:', err);
} finally {
await redis.del(sportFans, musicFans, movieFans);
}
}
// setOperationsExample().then(() => redis.quit());
Redis Sorted Sets
A Redis Sorted Set is similar to a regular Set, but every member is associated with a score (a floating-point number). The members are always kept sorted by their scores. If two members have the same score, they are sorted lexicographically (alphabetically).
key -> { (member1, score1), (member2, score2), ... } (ordered by score)
Key characteristics:
- Uniqueness + Order: Members are unique AND sorted by their scores.
- Fast range queries: Retrieve members by score range or by rank (position in the sorted list).
- Update scores: Scores can be updated, and the member’s position in the sorted set will change accordingly.
- Ideal for leaderboards: Perfect for ranking items/users.
Basic Sorted Set Commands
1. ZADD key [NX|XX] [CH] [INCR] score member [score member ...] (Sorted Set Add)
Adds one or more members with their scores to the sorted set stored at key.
NX: Only add new elements (don’t update existing scores).XX: Only update existing elements (don’t add new ones).CH: Return the number of elements changed (added or updated).INCR: Increments the score of a member (likeINCRBYfor Strings).
2. ZRANGE key start stop [WITHSCORES] (Sorted Set Range)
Returns a range of members from the sorted set stored at key by index (rank). 0 is the first element, -1 is the last. WITHSCORES returns both members and their scores.
3. ZSCORE key member (Sorted Set Score)
Returns the score of member in the sorted set stored at key.
4. ZREM key member [member ...] (Sorted Set Remove)
Removes one or more members from the sorted set stored at key.
5. ZINCRBY key increment member (Sorted Set Increment By)
Increments the score of member in the sorted set stored at key by increment.
Node.js Example:
// redis-sorted-sets.js
const Redis = require('ioredis');
const redis = new Redis();
async function sortedSetBasicExample() {
try {
const gameLeaderboardKey = 'game:leaderboard';
await redis.del(gameLeaderboardKey); // Clear previous data
// ZADD: Add players with scores
// ZADD key score member [score member ...]
let addedCount = await redis.zadd(gameLeaderboardKey, 100, 'Alice', 150, 'Bob', 120, 'Charlie');
console.log(`ZADD ${gameLeaderboardKey} -> Added ${addedCount} new players.`); // Output: Added 3 new players.
// ZRANGE: Get top players (by rank)
// ZRANGE key start stop [WITHSCORES]
let topPlayers = await redis.zrange(gameLeaderboardKey, 0, -1, 'WITHSCORES');
console.log(`\nLeaderboard (ZRANGE 0 -1 WITHSCORES): ${JSON.stringify(topPlayers)}`);
// Output: ["Alice", "100", "Charlie", "120", "Bob", "150"] (lowest score first by default)
// ZREVRANGE: Get top players (highest score first)
let highestScorers = await redis.zrevrange(gameLeaderboardKey, 0, 1, 'WITHSCORES'); // Top 2
console.log(`Top 2 players (ZREVRANGE 0 1 WITHSCORES): ${JSON.stringify(highestScorers)}`);
// Output: ["Bob", "150", "Charlie", "120"]
// ZINCRBY: Update a player's score
let newScore = await redis.zincrby(gameLeaderboardKey, 30, 'Alice'); // Alice scores 30 more points
console.log(`Alice's new score (ZINCRBY): ${newScore}`); // Output: 130
// ZSCORE: Get specific player's score
let bobScore = await redis.zscore(gameLeaderboardKey, 'Bob');
console.log(`Bob's score (ZSCORE): ${bobScore}`); // Output: 150
// ZRANK / ZREVRANK: Get player's rank
let aliceRank = await redis.zrank(gameLeaderboardKey, 'Alice'); // 0-based rank, lowest score first
let aliceRevRank = await redis.zrevrank(gameLeaderboardKey, 'Alice'); // 0-based rank, highest score first
console.log(`Alice's rank (ascending): ${aliceRank}, Alice's rank (descending): ${aliceRevRank}`);
// Output example (after Alice's score update): Alice rank (asc): 0, Alice rank (desc): 1 (Bob is 0, Alice is 1, Charlie is 2 - scores 150, 130, 120)
// ZREM: Remove a player
let removedCount = await redis.zrem(gameLeaderboardKey, 'Charlie');
console.log(`ZREM ${gameLeaderboardKey} Charlie -> Removed ${removedCount} player.`); // Output: Removed 1 player.
topPlayers = await redis.zrange(gameLeaderboardKey, 0, -1, 'WITHSCORES');
console.log(`\nLeaderboard after ZREM: ${JSON.stringify(topPlayers)}`);
// Output: ["Alice", "130", "Bob", "150"]
} catch (err) {
console.error('Error in sortedSetBasicExample:', err);
} finally {
await redis.del(gameLeaderboardKey);
}
}
// sortedSetBasicExample().then(() => redis.quit());
Python Example:
# ... (previous setup)
def sorted_set_basic_example():
try:
productRatingsKey = 'product:ratings:P001'
r.delete(productRatingsKey)
# ZADD: Add users and their ratings
added_count = r.zadd(productRatingsKey, {'userA': 4.5, 'userB': 3.0, 'userC': 5.0})
print(f"ZADD {productRatingsKey} -> Added {added_count} new ratings.")
# ZRANGE: Get ratings in ascending order
ratings_asc = r.zrange(productRatingsKey, 0, -1, withscores=True)
print(f"\nRatings (ZRANGE 0 -1 WITHSCORES): {[ (m.decode('utf-8'), s) for m,s in ratings_asc ]}")
# Output: [('userB', 3.0), ('userA', 4.5), ('userC', 5.0)]
# ZREVRANGE: Get ratings in descending order
ratings_desc = r.zrevrange(productRatingsKey, 0, 1, withscores=True) # Top 2
print(f"Top 2 Ratings (ZREVRANGE 0 1 WITHSCORES): {[ (m.decode('utf-8'), s) for m,s in ratings_desc ]}")
# Output: [('userC', 5.0), ('userA', 4.5)]
# ZINCRBY: Update a user's rating
new_rating = r.zincrby(productRatingsKey, 0.5, 'userA') # userA improves rating
print(f"UserA's new rating (ZINCRBY): {new_rating}") # Output: 5.0
# ZSCORE: Get specific user's rating
userB_score = r.zscore(productRatingsKey, 'userB')
print(f"UserB's score (ZSCORE): {userB_score}") # Output: 3.0
# ZRANK / ZREVRANK: Get user's rank
userCRank = r.zrank(productRatingsKey, 'userC')
userCRevRank = r.zrevrank(productRatingsKey, 'userC')
print(f"UserC's rank (ascending): {userCRank}, UserC's rank (descending): {userCRevRank}")
# After userA's update, scores: (userB, 3.0), (userC, 5.0), (userA, 5.0)
# Assuming lexicographical tie-breaking, userA might be before userC if names are similar, or after
# For example: userB=0, userA=1, userC=2 (if userA before userC)
# userB=0, userC=1, userA=2 (if userC before userA)
# We need to sort by actual scores to understand ranks
# The output reflects 0-based ranks in Redis.
# Current list ascending: [('userB', 3.0), ('userA', 5.0), ('userC', 5.0)] based on lexicographical order for same score
# So: userB rank 0, userA rank 1, userC rank 2.
# ZRANK userC -> 2
# ZREVRANK userC -> 0 (userC has highest score, and lexicographically before userA for same score)
# ZREM: Remove a user's rating
removed_count = r.zrem(productRatingsKey, 'userB')
print(f"ZREM {productRatingsKey} userB -> Removed {removed_count} rating.")
ratings_final = r.zrange(productRatingsKey, 0, -1, withscores=True)
print(f"\nRatings after ZREM: {[ (m.decode('utf-8'), s) for m,s in ratings_final ]}")
# Output: [('userA', 5.0), ('userC', 5.0)]
except Exception as e:
print(f"Error in sorted_set_basic_example: {e}")
# sorted_set_basic_example()
# r.close()
Other Useful Sorted Set Commands
ZCARD key: Returns the number of members in the sorted set.ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]: Returns all members with scores within a given range.ZCOUNT key min max: Returns the number of members with scores within a given range.
Full Python Example with Sets and Sorted Sets
# full_set_sorted_set_operations.py
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
def run_all_set_sorted_set_examples():
print('--- Running Set and Sorted Set Examples ---')
# --- Sets: Online users ---
online_users_key = 'app:online_users'
active_in_game_key = 'game:active_players'
r.delete(online_users_key, active_in_game_key)
print("\n--- Redis Sets (Unique Members) ---")
r.sadd(online_users_key, 'Alice', 'Bob', 'Charlie', 'Alice')
r.sadd(active_in_game_key, 'Bob', 'Charlie', 'David')
print(f"Online Users: {[u.decode('utf-8') for u in r.smembers(online_users_key)]}")
print(f"Active Game Players: {[u.decode('utf-8') for u in r.smembers(active_in_game_key)]}")
# Intersection: Users online AND active in game
common_players = r.sinter(online_users_key, active_in_game_key)
print(f"Common Online & Active Players (SINTER): {[u.decode('utf-8') for u in common_players]}") # Expected: Bob, Charlie
# Union: All unique users who are either online OR active in game
all_players = r.sunion(online_users_key, active_in_game_key)
print(f"All unique Players (SUNION): {[u.decode('utf-8') for u in all_players]}") # Expected: Alice, Bob, Charlie, David
# Difference: Users online but NOT active in game
idle_online_players = r.sdiff(online_users_key, active_in_game_key)
print(f"Idle Online Players (SDIFF): {[u.decode('utf-8') for u in idle_online_players]}") # Expected: Alice
# Check membership
print(f"Is Alice online? {r.sismember(online_users_key, 'Alice')}")
print(f"Is David online? {r.sismember(online_users_key, 'David')}")
# Remove a user
r.srem(online_users_key, 'Bob')
print(f"Online Users after Bob left: {[u.decode('utf-8') for u in r.smembers(online_users_key)]}")
# --- Sorted Sets: Leaderboard ---
leaderboard_key = 'game:leaderboard:global'
r.delete(leaderboard_key)
print("\n--- Redis Sorted Sets (Ranked Members) ---")
# ZADD with initial scores
r.zadd(leaderboard_key, {'PlayerA': 100, 'PlayerB': 150, 'PlayerC': 120, 'PlayerD': 80})
print(f"Initial Leaderboard: {[ (m.decode('utf-8'), s) for m,s in r.zrevrange(leaderboard_key, 0, -1, withscores=True)]}")
# ZINCRBY: PlayerB scores more points
r.zincrby(leaderboard_key, 25, 'PlayerB')
print(f"PlayerB score updated. Current: {r.zscore(leaderboard_key, 'PlayerB')}")
print(f"Leaderboard after PlayerB update: {[ (m.decode('utf-8'), s) for m,s in r.zrevrange(leaderboard_key, 0, -1, withscores=True)]}")
# ZADD with NX (only add if not exists) - won't update PlayerA
r.zadd(leaderboard_key, {'PlayerA': 200}, nx=True)
print(f"PlayerA score (ZADD NX, should be 100): {r.zscore(leaderboard_key, 'PlayerA')}")
# ZADD with XX (only update if exists) - will add PlayerE if not existed
r.zadd(leaderboard_key, {'PlayerE': 180}, xx=True) # This will not add PlayerE as it's XX
print(f"PlayerE score (ZADD XX, should be None): {r.zscore(leaderboard_key, 'PlayerE')}")
# Correct usage of ZADD XX to update an existing member (e.g. if PlayerE exists from earlier)
# ZRANK and ZREVRANK
print(f"Rank of PlayerC (ascending): {r.zrank(leaderboard_key, 'PlayerC')}") # 0-based
print(f"Rank of PlayerC (descending): {r.zrevrank(leaderboard_key, 'PlayerC')}") # 0-based
# ZRANGEBYSCORE - Players with scores between 100 and 150 (inclusive)
players_in_range = r.zrangebyscore(leaderboard_key, 100, 150, withscores=True)
print(f"Players with scores 100-150: {[ (m.decode('utf-8'), s) for m,s in players_in_range]}")
# ZCARD: total players
print(f"Total players on leaderboard: {r.zcard(leaderboard_key)}")
print('--- Set and Sorted Set Examples Complete ---')
r.delete(online_users_key, active_in_game_key, leaderboard_key) # Clean up
r.close()
# run_all_set_sorted_set_examples()
Exercises / Mini-Challenges
Unique Visitors Tracking:
- Create a Redis Set
website:visitors:2025-11-07. - When a new “visitor” (represented by a unique ID, e.g., ‘ip-1’, ‘user-123’) arrives,
SADDthem to this set. - Retrieve the total count of unique visitors for the day.
- Challenge: How would you extend this to track unique visitors over multiple days and then get the unique visitors for a whole week (using set operations)?
- Create a Redis Set
User Interest Groups:
- Imagine you have different interest groups:
group:tech,group:sports,group:movies. - Users can join multiple groups. Use
SADDto add users to these groups (e.g.,user:Alicetogroup:techandgroup:movies). - Find all users who are interested in both
techandmovies. - Find all users who are interested in
sportsbut nottech. - Find all unique users interested in any of the three topics.
- Challenge: Implement a way to remove a user from a specific group.
- Imagine you have different interest groups:
Gaming Leaderboard with Real-time Updates:
- Create a Sorted Set
game:highscores. - Simulate players submitting scores:
PlayerXscores100,PlayerYscores150,PlayerZscores80. UseZADD. PlayerXplays again and scores130. Update their score (hint:ZADDorZINCRBY).- Retrieve the top 5 players with their scores (highest score first).
- Retrieve the rank (position) of
PlayerXon the leaderboard. - Challenge: If a player’s score ties with another, what happens to their order? How can you ensure consistent ordering for ties (e.g., by player name alphabetically)? (Redis handles this by default for same scores).
- Create a Sorted Set
By mastering Sets and Sorted Sets, you unlock powerful capabilities for managing unique collections, performing common set algebra, and building real-time ranking systems in your applications. Up next, we’ll dive into Intermediate Topics like Transactions and Pipelining, which are essential for optimizing performance and ensuring data consistency.