Introduction
Counting how often things happen sounds trivial until you have billions of events per day. Count-Min Sketch (CMS) solves frequency estimation in constant memory, making it the go-to structure when you need approximate counts at massive scale.
Key Concepts
- Frequency estimation: Approximating how many times a specific item has appeared in a data stream.
- Counter matrix: A grid of counters with
drows (hash functions) andwcolumns (counter slots) that stores compressed frequency information. - Hash collision: When two different items map to the same counter position, inflating counts. CMS mitigates this by using multiple hash functions and taking the minimum.
- Overestimation bias: CMS can only overestimate counts (never underestimate), because collisions add to counters but never subtract.
Real World Context
Anytime you need to answer "how many times has X happened?" at scale, CMS is your friend. Ad impression counting, network traffic analysis, page view tracking, and query popularity ranking all benefit from CMS. A single CMS can replace millions of individual counters while using only a few kilobytes of memory.
Deep Dive
CMS works by hashing each item through d independent hash functions. Each hash maps the item to one column in its row. On insertion, all d positions are incremented. On query, all d positions are read and the minimum value is returned.
┌─────────────────────────────────────────────────────────────┐
│ Matrix of counters (d rows × w columns): │
│ │
│ hash1(item) → row1, col3 │
│ hash2(item) → row2, col7 │
│ hash3(item) → row3, col2 │
│ │
│ ADD: Increment all hashed positions │
│ QUERY: Return MINIMUM of hashed positions │
│ (Minimum because collisions only increase counts) │
└─────────────────────────────────────────────────────────────┘
Redis provides two ways to create a CMS:
redis# By explicit dimensions (width × depth) CMS.INITBYDIM wordcount 2000 5 # By error rate and probability CMS.INITBYPROB wordcount 0.001 0.01 # 0.001 = error margin (epsilon) # 0.01 = probability of exceeding error (delta)
Once created, you add and query items:
redis# Increment counts CMS.INCRBY wordcount "apple" 3 "banana" 2 "cherry" 1 # Query estimated counts CMS.QUERY wordcount "apple" "banana" "cherry" # Returns: [3, 2, 1] (approximately)
You can also merge multiple sketches, which is useful for combining counts across time windows or distributed nodes:
redisCMS.MERGE combined 2 sketch1 sketch2 CMS.MERGE combined 2 sketch1 sketch2 WEIGHTS 1 2
Common Pitfalls
- Treating CMS counts as exact — CMS only provides upper-bound estimates. If your application requires exact counts for small datasets, use a hash map instead.
- Using too few columns (width) — A narrow sketch causes more hash collisions, leading to larger overestimates. Always size your sketch based on expected error tolerance.
- Forgetting that CMS never underestimates — If CMS.QUERY returns 0, the item truly has never been seen. But a non-zero result may be inflated by collisions.
Best Practices
- Use CMS.INITBYPROB for most cases — Specifying error rate and probability is more intuitive than choosing raw dimensions. Let Redis compute optimal width and depth.
- Combine with Top-K for trending analysis — CMS alone cannot tell you which items are most frequent. Pair it with TOPK.RESERVE to maintain a ranked list of heavy hitters.
- Merge sketches for time-windowed analysis — Create per-hour or per-day sketches and merge them for broader time ranges without re-processing raw data.
Summary
- Count-Min Sketch estimates item frequencies in constant memory using a matrix of counters and multiple hash functions.
- It never underestimates: reported counts are always greater than or equal to the true count.
- CMS.INITBYPROB lets you specify error tolerance directly; CMS.QUERY returns the minimum across hash functions.
- Ideal for page views, network traffic, ad impressions, and any high-volume counting problem.
Code Examples
import redis
r = redis.Redis()
class PageViewCounter:
def __init__(self):
self.key = 'pageviews:counter'
try:
r.execute_command('CMS.INITBYPROB', self.key, 0.001, 0.01)
except redis.ResponseError:
pass # Already exists
def record_view(self, page_url):
"""Record a page view"""
r.execute_command('CMS.INCRBY', self.key, page_url, 1)
def get_views(self, page_url):
"""Get approximate view count"""
result = r.execute_command('CMS.QUERY', self.key, page_url)
return result[0]
def get_multiple_views(self, *page_urls):
"""Get view counts for multiple pages"""
result = r.execute_command('CMS.QUERY', self.key, *page_urls)
return dict(zip(page_urls, result))
counter = PageViewCounter()
counter.record_view('/home')
counter.record_view('/about')
counter.record_view('/home')
print(counter.get_views('/home')) # ~2