Redis Core Concepts: Sets and Sorted Sets

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 (like INCRBY for 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

  1. 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, SADD them 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)?
  2. User Interest Groups:

    • Imagine you have different interest groups: group:tech, group:sports, group:movies.
    • Users can join multiple groups. Use SADD to add users to these groups (e.g., user:Alice to group:tech and group:movies).
    • Find all users who are interested in both tech and movies.
    • Find all users who are interested in sports but not tech.
    • Find all unique users interested in any of the three topics.
    • Challenge: Implement a way to remove a user from a specific group.
  3. Gaming Leaderboard with Real-time Updates:

    • Create a Sorted Set game:highscores.
    • Simulate players submitting scores: PlayerX scores 100, PlayerY scores 150, PlayerZ scores 80. Use ZADD.
    • PlayerX plays again and scores 130. Update their score (hint: ZADD or ZINCRBY).
    • Retrieve the top 5 players with their scores (highest score first).
    • Retrieve the rank (position) of PlayerX on 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).

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.