This book covers the use of Redis, an in-memory database/data structure server.

open all | close all

3.5 Sorted sets

ZSETs offer the ability to store a mapping of members to scores (similar to the keys and values of HASHes). These mappings allow us to manipulate the numeric scores,2 and fetch and scan over both members and scores based on the sorted order of the scores. In chapter 1, we showed a brief example that used ZSETs as a way of sorting submitted articles based on time and how many up-votes they had received, and in chapter 2, we had an example that used ZSETs as a way of handling the expiration of old cookies.

In this section, we’ll talk about commands that operate on ZSETs. You’ll learn how to add and update items in ZSETs, as well as how to use the ZSET intersection and union commands. When finished with this section, you’ll have a much clearer understanding about how ZSETs work, which will help you to better understand what we did with them in chapter 1, and how we’ll use them in chapters 5, 6, and 7.

Let’s look at some commonly used ZSET commands in table 3.9.

Table 3.9 Some common ZSET commands
Command Example use and description
ZADD ZADD key-name score member [score member …] — Adds members with the given scores to the ZSET
ZREM ZREM key-name member [member …] — Removes the members from the ZSET, returning the number of members that were removed
ZCARD ZCARD key-name — Returns the number of members in the ZSET
ZINCRBY ZINCRBY key-name increment member — Increments the member in the ZSET
ZCOUNT ZCOUNT key-name min max — Returns the number of members with scores between the provided minimum and maximum
ZRANK ZRANK key-name member — Returns the position of the given member in the ZSET
ZSCORE ZSCORE key-name member — Returns the score of the member in the ZSET
ZRANGE ZRANGE key-name start stop [WITHSCORES] — Returns the members and optionally the scores for the members with ranks between start and stop

We’ve used some of these commands in chapters 1 and 2, so they should already be familiar to you. Let’s quickly revisit the use of some of our commands.

Listing 3.9 A sample interaction showing some common ZSET commands in Redis
>>> conn.zadd('zset-key', 'a', 3, 'b', 2, 'c', 1)

Adding members to ZSETs in Python has the arguments reversed compared to standard Redis, which makes the order the same as HASHes.

>>> conn.zcard('zset-key')

Knowing how large a ZSET is can tell us in some cases if it’s necessary to trim our ZSET.

>>> conn.zincrby('zset-key', 'c', 3)

We can also increment members like we can with STRING and HASH values.

>>> conn.zscore('zset-key', 'b')

Fetching scores of individual members can be useful if we’ve been keeping counters or toplists.

>>> conn.zrank('zset-key', 'c')

By fetching the 0-indexed position of a member, we can then later use ZRANGE to fetch a range of the values easily.

>>> conn.zcount('zset-key', 0, 3)

Counting the number of items with a given range of scores can be quite useful for some tasks.

>>> conn.zrem('zset-key', 'b')

Removing members is as easy as adding them.

>>> conn.zrange('zset-key', 0, -1, withscores=True)
[('a', 3.0), ('c', 4.0)]

For debugging, we usually fetch the entire ZSET with this ZRANGE call, but real use cases will usually fetch items a relatively small group at a time.

You’ll likely remember our use of ZADD, ZREM, ZINCRBY, ZSCORE, and ZRANGE from chapters 1 and 2, so their semantics should come as no surprise. The ZCOUNT command is a little different than the others, primarily meant to let you discover the number of values whose scores are between the provided minimum and maximum scores.

Table 3.10 shows several more ZSET commands in Redis that you’ll find useful.

Table 3.10 Commands for fetching and deleting ranges of data from ZSETs and offering SET-like intersections
Command Example use and description
ZREVRANK ZREVRANK key-name member — Returns the position of the member in the ZSET, with members ordered in reverse
ZREVRANGE ZREVRANGE key-name start stop [WITHSCORES] — Fetches the given members from the ZSET by rank, with members in reverse order
ZRANGEBYSCORE ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] — Fetches the members between min and max
ZREVRANGEBYSCORE ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] — Fetches the members in reverse order between min and max
ZREMRANGEBYRANK ZREMRANGEBYRANK key-name start stop — Removes the items from the ZSET with ranks between start and stop
ZREMRANGEBYSCORE ZREMRANGEBYSCORE key-name min max — Removes the items from the ZSET with scores between min and max
ZINTERSTORE ZINTERSTORE dest-key key-count key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX] — Performs a SET-like intersection of the provided ZSETs
ZUNIONSTORE ZUNIONSTORE dest-key key-count key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX] — Performs a SET-like union of the provided ZSETs

This is the first time that you’ve seen a few of these commands. If some of the ZREV* commands are confusing, remember that they work the same as their nonreversed counterparts, except that the ZSET behaves as if it were in reverse order (sorted by score from high to low). You can see a few examples of their use in the next listing.

Listing 3.10 A sample interaction showing ZINTERSTORE and ZUNIONSTORE
>>> conn.zadd('zset-1', 'a', 1, 'b', 2, 'c', 3)
>>> conn.zadd('zset-2', 'b', 4, 'c', 1, 'd', 0)

We’ll start out by creating a couple of ZSETs.

>>> conn.zinterstore('zset-i', ['zset-1', 'zset-2'])
>>> conn.zrange('zset-i', 0, -1, withscores=True)
[('c', 4.0), ('b', 6.0)]

When performing ZINTERSTORE or ZUNIONSTORE, our default aggregate is sum, so scores of items that are in multiple ZSETs are added.

>>> conn.zunionstore('zset-u', ['zset-1', 'zset-2'], aggregate='min')
>>> conn.zrange('zset-u', 0, -1, withscores=True)
[('d', 0.0), ('a', 1.0), ('c', 1.0), ('b', 2.0)]

It’s easy to provide different aggregates, though we’re limited to sum, min, and max.

>>> conn.sadd('set-1', 'a', 'd') 2
>>> conn.zunionstore('zset-u2', ['zset-1', 'zset-2', 'set-1']) 4L
>>> conn.zrange('zset-u2', 0, -1, withscores=True)
[('d', 1.0), ('a', 2.0), ('c', 4.0), ('b', 6.0)]

We can also pass SETs as inputs to ZINTERSTORE and ZUNIONSTORE; they behave as though they were ZSETs with all scores equal to 1.

ZSET union and intersection can be difficult to understand at first glance, so let’s look at some figures that show what happens during the processes of both intersection and union. Figure 3.1 shows the intersection of the two ZSETs and the final ZSET result. In this case, our aggregate is the default of sum, so scores are added.

Unlike intersection, when we perform a union operation, items that exist in at least one of the input ZSETs are included in the output. Figure 3.2 shows the result of performing a union operation with a different aggregate function, min, which takes the minimum score if a member is in multiple input ZSETs.

In chapter 1, we used the fact that we can include SETs as part of ZSET union and intersection operations. This feature allowed us to easily add and remove articles from groups without needing to propagate scoring and insertion times into additional ZSETs. Figure 3.3 shows a ZUNIONSTORE call that combines two ZSETs with one SET to produce a final ZSET.

Figure 3.1 What happens when calling conn.zinterstore(‘zset-i’, [‘zset-1’, ‘zset-2’]); elements that exist in both zset-1 and zset-2 are added together to get zset-i
Figure 3.2 What happens when calling conn.zunionstore(‘zset-u’, [‘zset-1’, ‘zset-2′], aggregate=’min’); elements that exist in either zset-1 or zset-2 are combined with the minimum function to get zset-u
Figure 3.3 What happens when calling conn.zunionstore(‘zset-u2’, [‘zset-1’, ‘zset-2’, ‘set-1’]); elements that exist in any of zset-1, zset-2, or set-1 are combined via addition to get zset-u2

In chapter 7, we’ll use ZINTERSTORE and ZUNIONSTORE as parts of a few different types of search. We’ll also talk about a few different ways to combine ZSET scores with the optional WEIGHTS parameter to further extend the types of problems that can be solved with SETs and ZSETs.

As you’re developing applications, you may have come upon a pattern known as publish/subscribe, also referred to as pub/sub. Redis includes this functionality, which we’ll cover next.

2 Scores are actually stored inside Redis as IEEE 754 floating-point doubles.