How to build a real-time leaderboard that handles millions of entries with O(log N) updatesβusing Redis Sorted Set instead of SQL ORDER BY.
π The Problem: Why SQL Struggles with Real-time Rankings
The High-Frequency Update Challenge
In competitive systemsβgames, trading platforms, live streamingβscores change constantly. Thousands of updates per second, and users expect rankings to reflect changes instantly.
SQL’s Limitations
Read-time Sorting
Traditional SQL approach:
|
|
Problems:
- Without index: O(N log N) sort on every query
- With B-tree index: Reads are fast (O(K) for LIMIT K), but…
Write Pressure on Index
The real bottleneck is writes:
- Every score update triggers B-tree rebalancing
- High-frequency writes cause lock contention
- Index fragmentation degrades performance over time
- MVCC overhead in concurrent update scenarios
|
|
Maintaining global sorted order under high write rates leads to disproportionate index maintenance cost:
- β Fast reads (show top 100) β achievable with proper indexing
- β Fast writes (update scores constantly) β achievable in isolation
- β Both simultaneously at high frequency β B-tree rebalancing + lock contention becomes the bottleneck
β οΈ This doesn’t mean SQL “can’t” do leaderboards. Advanced patterns (materialized views, CQRS, async aggregation) can work. But for the naive single-table model, Redis offers a simpler path.
π§ The Solution: Redis Sorted Set (ZSet)
Data Structure Internals
ZSet is implemented as skip list + hash table:
- Skip list β Maintains sorted order with O(log N) insert/update/delete
- Hash table β Provides O(1) member lookup
|
|
Insert-time Sorting
Unlike SQL’s read-time sorting, ZSet sorts on write:
| Operation | SQL (with index) | Redis ZSet |
|---|---|---|
| Insert/Update | O(log N) + lock/latch overhead | O(log N) serialized |
| Read Top K | O(log N + K) | O(log N + K) |
| Get Rank | O(log N) with covering index | O(log N) |
Both offer similar asymptotic complexity. The difference is contention model: SQL has lock/latch contention under concurrent writes; Redis serializes operations in a single-threaded event loop, shifting the constraint to CPU throughput.
The key advantage: Redis avoids intra-data-structure lock contention by serializing all operations.
Data Model
|
|
π» Implementation
Basic Operations
|
|
Data Enrichment Pattern
ZSet only stores IDs and scores. For full data:
|
|
When scores are equal, ZSet sorts by member name (lexicographically). For custom tie-breaking:
|
|
β οΈ Production warning: Float encoding has precision drift risks for large scores or long-running leaderboards. A more robust approach is using structured member keys (
playerId|timestamp) or a secondary index.
β οΈ Production Caveats
Memory Constraints
- ZSet stores all members + scores in RAM
- 1 million entries β 50-100 MB (depending on member string length)
- Monitor memory usage; implement TTL or periodic cleanup for old seasons
Cold Data Strategy
Don’t keep historical leaderboards in Redis forever:
|
|
Cluster Mode Considerations
In Redis Cluster, related keys should use hash tags to ensure they’re on the same shard:
|
|
Score Precision
Redis scores are IEEE 754 double-precision floats:
- Safe integer range: Β±2^53
- For monetary values, consider storing as integers (cents, not dollars)
π Summary
- SQL struggles with high-frequency write + read scenarios due to index maintenance overhead
- ZSet = skip list + hash β O(log N) insert, O(log N) rank lookup, no lock contention
- Insert-time sorting β Reads are essentially free; sorting happens on write
- Trade-off β Memory-bound; requires data enrichment for full details
For leaderboards with millions of entries and thousands of updates per second, Redis ZSet delivers millisecond-level response times. The win isn’t Big-Oβit’s the contention model.