단 몇 밀리초 만에 수백만 명의 사용자를 세분화한 방법

작성자

카테고리:

← 피드로
DEV Community · JackTT · 2026-08-03 개발(SW)

User segmentation requirement

Imagine you need to send a push notification to users who satisfy all of the following conditions:

  • Push notification is enabled
  • User is a VIP
  • Active within the last 30 days
  • Following the Voucher Hot category

The traditional approach is to query multiple tables:

SELECT DISTINCT u.id
FROM users u
JOIN user_configs c ON c.user_id = u.id
JOIN devices d ON d.user_id = u.id
JOIN follows f ON f.user_id = u.id
WHERE c.push_optin = 1
  AND c.mute = 0
  AND d.fcm_token IS NOT NULL
  AND f.category = 'voucher_hot'
  AND u.last_active >= NOW() - INTERVAL 30 DAY;

Enter fullscreen mode Exit fullscreen mode

As your user base grows into the millions, every campaign requires joining multiple large tables, filtering millions of records, and repeatedly computing the same audience. Query latency increases significantly, making real-time segmentation increasingly difficult.

A Different Approach

Instead of querying the database every time, we precompute each boolean attribute as a bitmap.

Think of a bitmap as a huge array containing only 0 and 1, where the index corresponds to the user ID.

For example, a bitmap representing whether a user has enabled push notifications:

  • User ID : 0 1 2 3 4 5 6 7
  • Bitmap : 1 0 1 1 0 0 1 1

To check whether user 123 has enabled notifications, simply read bit 123.

1 → enabled
0 → disabled

Enter fullscreen mode Exit fullscreen mode

Each bitmap represents exactly one boolean property:

  • bitmap:push_optin
  • bitmap:vip
  • bitmap:active30
  • bitmap:follow:voucher_hot

Memory Usage

Bitmap is extremely memory efficient.

Each user requires only one bit.

For 1 million users:

1.000.000 bits
~ 125.000 bytes
~ 122 KB

Enter fullscreen mode Exit fullscreen mode

That means every segment only consumes about 122 KB of Redis memory.

Even 100 different segments require only around 12 MB.

Finding Intersections

Suppose you want all users that are:

VIP
AND Push Opt-in
AND Active30
AND Following Voucher Hot

Enter fullscreen mode Exit fullscreen mode

Redis can calculate the result with a single command:

BITOP AND result vip push_optin active30 follow_voucher_hot

Enter fullscreen mode Exit fullscreen mode

Need the number of matched users?

BITCOUNT result

Enter fullscreen mode Exit fullscreen mode

  • No SQL joins.
  • No table scans.
  • No iterating over millions of users in application code.

Why Is It So Fast?

Redis Bitmap is implemented on top of the Redis String data type. Internally, Redis stores the bitmap as a contiguous sequence of bytes, allowing the CPU to process memory sequentially without pointer chasing or hash lookups.

When executing BITOP, Redis doesn’t process users one by one. Instead, the CPU performs bitwise operations directly on blocks of memory.

For example:

10110010
11001000
--------
10000000

Enter fullscreen mode Exit fullscreen mode

On modern 64-bit CPUs, each instruction processes at least 64 bits at once, and many processors can process even larger blocks using SIMD instructions.

This is why bitmap operations over millions of users typically complete in just a few milliseconds.

Bitmap Commands

The Redis commands you’ll use most frequently are:

Command Purpose

Command Syntax Description SETBIT SETBIT key offset value Set the bit at offset to 0 or 1. GETBIT GETBIT key offset Get the bit value at offset. BITCOUNT BITCOUNT key [start end [BYTE or BIT]] Count the number of bits set to 1. BITPOS BITPOS key bit [start [end [BYTE or BIT]]] Find the position of the first 0 or 1 bit. BITOP BITOP <AND, OR, XOR, NOT> destkey key [key ...] Perform bitwise operations across one or more bitmaps and store the result in destkey.

Chaining Multiple Operations

BITOP supports applying a single operation to multiple bitmaps in one command.

For example, to compute the intersection of three segments:

BITOP AND result bitmap_1 bitmap_2 bitmap_3

Enter fullscreen mode Exit fullscreen mode

However, BITOP only supports one operation per command. For nested expressions, you’ll need to create temporary bitmaps.

For example, to evaluate:

(bitmap_1 OR bitmap_2) AND bitmap_3

Enter fullscreen mode Exit fullscreen mode

You would execute:

BITOP OR tmp bitmap_1 bitmap_2 EXPIRE tmp 60 BITOP AND result tmp bitmap_3 DEL tmp

Enter fullscreen mode Exit fullscreen mode

Since intermediate bitmaps are only temporary, either delete them immediately after use or assign a short TTL to ensure they are cleaned up automatically, especially if the process fails before reaching the cleanup step.

A Common Pitfall: EXCLUDE

Many developers implement exclusion like this:

A AND NOT B

Enter fullscreen mode Exit fullscreen mode

Using Redis:

BITOP NOT tmp mute
BITOP AND result active30 tmp

Enter fullscreen mode Exit fullscreen mode

This can introduce a silent bug.

BITOP NOT only inverts the existing length of the source bitmap. If mute is shorter than active30, Redis pads the remaining bits with zeros during the AND operation, unintentionally excluding many valid users.

The safer approach is:

(A XOR B) AND A

Enter fullscreen mode Exit fullscreen mode

Redis implementation:

BITOP XOR tmp active30 mute
BITOP AND result tmp active30

Enter fullscreen mode Exit fullscreen mode

This produces the correct result even when the two bitmaps have different lengths.

Conclusion

Redis Bitmap doesn’t replace your database.

Instead, it acts as an in-memory index for boolean attributes. Once each attribute is represented as a bitmap, complex audience expressions become a series of CPU-optimized bitwise operations instead of expensive SQL joins.

For systems handling millions of users, Redis Bitmap is one of the simplest and most effective techniques for building high-performance segmentation engines.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다