Add Scores and Range Queries
Master sorted set scores and range queries in Redis: adding scores, retrieving by rank or score range, and practical examples for real-world use.
Focus: add scores and range queries
Imagine you're building a live leaderboard for a gaming app. Players' scores change constantly, and you need to quickly fetch the top 10, or maybe all players with scores between 500 and 1000. Without a purpose-built data structure, you'd be stuck sorting and filtering in application code — slow, error-prone, and hard to scale. Redis sorted sets, with their score-driven mechanics, solve this elegantly: you add or update a member with a score, and the set stays sorted automatically. Then you query by rank or score with commands like ZADD, ZRANGE, and ZRANGEBYSCORE. In this lesson, you'll learn how to add scores and perform range queries to build real-time ranking systems, time-series summaries, and more.
The problem this lesson solves
Standard Redis sets are great for membership checks (SISMEMBER) and union/intersection, but they don't maintain any inherent order. When you need ordered data — like leaderboards, priority queues, or sorted activity feeds — you face a choice: sort in your app (costly and slow), use a sorted list (hard to maintain), or reach for sorted sets.
A sorted set in Redis is like a hash that also keeps members in order based on a numeric score. Each member must be unique; scores can be integers or floating-point numbers (up to double precision). The set is always sorted by score, ascending. This means:
- Adding or updating a member with
ZADDautomatically repositions it. - You can query a slice of the sorted list by rank (e.g., top 5) or by score range (e.g., 80–100).
- Operations are O(log(n)) for add/remove, and O(log(n)+m) for range queries — extremely efficient even with millions of members.
Previously you might have used a list and an external sort, or a plain set with application-side logic. Now you'll see why sorted sets are the go-to for any use case needing order and score-based filtering.
Core concept / mental model
Think of a sorted set as a combination of a hash and a skiplist. The hash ensures O(1) lookups by member name, while the skiplist (plus a hash table for scores) keeps members sorted by score. Every member has a score that determines its rank (position in the sorted order). If two members have the same score, they're ordered lexicographically by member string.
Pro tip: Scores are not unique — you can have multiple members with the same score. Redis handles ties by member name (binary comparison).
The basic flow:
1. ZADD key score member — adds or updates the score of a member.
2. ZRANGE key start stop — retrieves members by their rank position (0-based, ascending).
3. ZREVRANGE — same but descending (high to low).
4. ZRANGEBYSCORE key min max — retrieves members with scores between min and max.
5. ZREM key member — removes a member.
Key terminologies
- Score: A double-precision number (floating point) that determines order.
- Rank: The position of a member in the sorted set (0 = lowest score).
- Querying: You can ask for members by rank range or by score range.
- Lexicographic order: Tie-breaker when scores are equal.
How it works step by step
When you call ZADD, Redis performs an upsert: if the member doesn't exist, it inserts it with the given score; if it does, it updates the score and repositions the member in the skiplist. This is both an add and an update in one command.
To retrieve data:
1. By rank: ZRANGE gives you members from index start to stop (inclusive). Use WITHSCORES to also retrieve the scores.
2. By score: ZRANGEBYSCORE lets you specify min and max scores (inclusive by default). Use (inf for infinity, (5 for exclusive boundaries.
3. Count: ZCOUNT key min max returns the number of members in the score range.
4. Score retrieval: ZSCORE key member fetches the score of a single member.
5. Reverse order: ZREVRANGE and ZREVRANGEBYSCORE invert the order (descending).
Practical example
Suppose you have a game with player scores. You want to: - Add/update Alice, Bob, Carol with different scores. - Show the top 2 players (highest scores). - Show all players with scores between 100 and 450. - Get the rank of Bob.
The commands are straightforward; see the hands-on section below.
Hands-on walkthrough
Let's jump into redis-cli and work through a realistic scenario.
Example 1: Adding scores and querying by rank
# Connect to Redis (assumes redis-server running on localhost:6379)
redis-cli
# Add player scores
ZADD game:scores 300 Alice
ZADD game:scores 450 Bob
ZADD game:scores 200 Carol
ZADD game:scores 350 Dave
ZADD game:scores 500 Eve
# (integer) 1 for each new member
Now retrieve the top 3 players (highest scores):
# Descending order by rank: positions 0 to 2 (first 3)
ZREVRANGE game:scores 0 2
# 1) "Eve"
# 2) "Bob"
# 3) "Dave"
With scores:
ZREVRANGE game:scores 0 2 WITHSCORES
# 1) "Eve"
# 2) 500
# 3) "Bob"
# 4) 450
# 5) "Dave"
# 6) 350
Example 2: Query by score range
Get all players with scores between 200 and 400 (inclusive on both ends):
ZRANGEBYSCORE game:scores 200 400 WITHSCORES
# 1) "Carol"
# 2) 200
# 3) "Alice"
# 4) 300
# 5) "Dave"
# 6) 350
To get scores greater than 350 (exclusive) up to 500 (inclusive):
ZRANGEBYSCORE game:scores (350 500 WITHSCORES
# 1) "Bob"
# 2) 450
# 3) "Eve"
# 4) 500
Example 3: Count and score lookup
# How many players have a score <= 350?
ZCOUNT game:scores -inf 350
# (integer) 3 (Carol, Alice, Dave)
# What's Bob's current score?
ZSCORE game:scores Bob
# "450"
Example 4: Updating a score
# Alice wins bonus: her score goes from 300 to 600
ZADD game:scores 600 Alice
# Verify new ranking: Alice is now #1
ZREVRANGE game:scores 0 5 WITHSCORES
# 1) "Alice"
# 2) 600
# 3) "Eve"
# 4) 500
# 5) "Bob"
# 6) 450
Pro tip:
ZADDis idempotent for updating — just run it with the new score. Redis automatically removes the old entry and inserts the new one in the correct position.
Compare options / when to choose what
| Command | Purpose | When to use |
|---|---|---|
ZRANGE |
Get members by rank range (ascending) | Displaying paginated leaderboards from low to high |
ZREVRANGE |
Get members by rank range (descending) | Showing top N highest scores (most common) |
ZRANGEBYSCORE |
Get members by score range | Filtering scores (e.g., all orders above $50) |
ZREVRANGEBYSCORE |
Get members by score range descending | Reverse order score queries |
ZCOUNT |
Count members in a score range | Knowing how many items pass a threshold |
ZSCORE |
Fetch score of one member | Checking individual score (e.g., user's rating) |
ZRANK / ZREVRANK |
Get index (rank) of a member | Finding position for pagination or highlighting |
Alternatives: - Lists: ordered by insertion/position, not score. Use when you need FIFO/LIFO, not score-based ordering. - Hashes: no inherent order. Use when you need to store field-value pairs by key and never need range queries. - Plain sets: good for membership but no ordering. Use for unique flags or tags.
For most real-time leaderboards (gaming, sales, forums), sorted sets with ZADD + ZREVRANGE are the canonical choice.
Troubleshooting & edge cases
- Forgetting
WITHSCORES— Without it, you only get member names, not their scores. Many beginners wonder why scores don't appear. Always addWITHSCORESif you need them. - Negative scores — Sorted sets allow negative scores (e.g., penalty values). They work fine but may surprise during sorting (lowest position = most negative).
- Floating point precision — Scores are double-precision floats. Avoid comparing for equality with
==in application code; use a tolerance or integer scores if exact equality matters. - Exclusive boundaries — Use
(inffor infinity,(5for "greater than 5" (exclusive). Don't leave out the space:(350is correct,( 350is parsed as the member( 350? No, Redis syntax requires no space between(and the score:(350. - Empty results —
ZRANGEBYSCOREreturns an empty array if no match. Check your min/max order:ZRANGEBYSCORE key 10 5returns nothing because min > max. - High cardinality — While O(log n) is efficient, if you have millions of members, extremely large range queries (returning millions of results) can be heavy. Use pagination with
ZRANGEorZSCANfor iterating large sets.
What you learned & what's next
You now understand how to add scores and perform range queries in Redis sorted sets. You can:
- Use ZADD to insert or update member scores.
- Retrieve top members with ZREVRANGE and scores via WITHSCORES.
- Filter by score range with ZRANGEBYSCORE.
- Count members in a range with ZCOUNT.
- Handle ties, exclusive boundaries, and floating-point quirks.
This skill is directly applicable to leaderboards, priority queues, time-series bucketing (store timestamps as scores), and any scenario requiring ordered, score-based data retrieval.
Next step: In the next lesson, you'll learn how to find a member's rank (position) using ZRANK and ZREVRANK, and how to remove members by rank or score range with ZREMRANGEBYRANK and ZREMRANGEBYSCORE.
Practice recap
Now it's your turn: in redis-cli, create a sorted set called leaderboard and add at least 5 members with distinct scores. Practice querying the top 3 using ZREVRANGE, then all members with scores between 300 and 700 using ZRANGEBYSCORE. Finally, update one member's score with ZADD and confirm their new rank. This exercise cements the flow of add-score-query that you'll use in real applications.
Common mistakes
- Forgetting
WITHSCORESwhen you need both member name and score — without it, you only get the members. - Mixing up
ZRANGE(ascending by rank) with the more commonZREVRANGE(descending) — top-N queries nearly always want descending. - Using
ZRANGEBYSCOREwith min > max, which returns an empty array — always ensure min ≤ max. - Using spaces in exclusive boundaries — correct syntax is
(350(no space), not( 350. - Assuming scores are unique — they are not. Ties are broken by lexicographic member order, which can cause unexpected ranking if you assume strict ordering.
Variations
- Use
ZREVRANGEBYSCOREinstead ofZRANGEBYSCOREwhen you need descending order by score (highest first). - For paginated leaderboards, combine
ZREVRANK(get rank) withZRANGEby rank range to implement "3rd to 5th place" queries. - If scores are large timestamps (e.g., Unix milliseconds), use
ZRANGEBYSCOREto retrieve events within a time window — a form of time-series querying.
Real-world use cases
- Real-time gaming leaderboard: update player scores with
ZADD, show top 10 usingZREVRANGEwithWITHSCORES. - Priority job queue: assign job priority as score, pop highest priority with
ZRANGEBYSCORE ... LIMIT 0 1thenZREM. - Product rating aggregation: store product ratings {member: product_id, score: average_rating}, query top-rated products by score range.
Key takeaways
- Sorted sets keep members ordered by a numeric score, allowing O(log n) add/update and O(log n + m) range queries.
- Use
ZADDto add or update a member's score — it's an upsert command. ZREVRANGEgives you the highest-to-lowest members by rank, perfect for leaderboards.ZRANGEBYSCOREwith exclusive boundaries ((score) enables flexible filtering without manual application logic.- Always include
WITHSCORESwhen you need the numeric values alongside member names. - Scores can be floats and are not unique — ties are sorted lexicographically by member name.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.