COUNT DISTINCT es exacto pero costoso: Flink debe mantener cada clave distinta en el estado, y este crece de forma lineal con el volumen de datos. Las funciones aproximadas como APPROX_COUNT_DISTINCT reducen la sobrecarga, pero introducen errores estadísticos. El tipo de dato BITMAP elimina ambos problemas: ofrece el mismo recuento exacto que COUNT DISTINCT al comprimir identificadores enteros en objetos bitmap compactos y reemplazar el mantenimiento del estado con operaciones bit a bit eficientes, todo ello con una fracción del costo de memoria y CPU.
El uso de BITMAP para deduplicación es diferente de los índices bitmap que se emplean en motores de almacenamiento columnar. Ambas técnicas comparten el nombre, pero resuelven problemas distintos: los índices bitmap filtran columnas de baja cardinalidad, mientras que la deduplicación con BITMAP cuenta valores distintos.
Limitaciones
Solo Ververica Runtime (VVR) 11.5 y versiones posteriores admiten el tipo de dato BITMAP y sus funciones relacionadas.
El tipo BITMAP almacena 32-bit unsigned integers.
Conceptos clave
Funcionamiento de BITMAP
BITMAP se basa en el estándar RoaringBitmap. Cada bitmap representa un conjunto de enteros sin signo de 32 bits. En lugar de escanear filas y rastrear claves individuales en el estado de Flink, BITMAP agrega los identificadores en un objeto binario compacto y calcula la cardinalidad (el recuento de valores distintos) mediante operaciones de tiempo constante.
Dos rutas de construcción
Cada bitmap en estos ejemplos se crea de una de dos formas:
Agregación desde datos sin procesar:
BITMAP_BUILD_AGGoBITMAP_BUILD_CARDINALITY_AGGagregan identificadores enteros de un flujo directamente en un bitmap.Deserialización desde almacenamiento:
BITMAP_FROM_BYTESconvierte un blob binario, escrito previamente porBITMAP_TO_BYTES, nuevamente en un bitmap para realizar operaciones adicionales.
Comprender qué ruta aplica en cada ejemplo le ayudará a seguir los patrones SQL que se presentan a continuación.
Aditividad: una decisión arquitectónica clave
La forma en que genera los resultados del bitmap determina lo que los consumidores posteriores pueden hacer con los datos:
|
Tipo de salida |
Función |
¿Aditivo? |
Cuándo utilizarlo |
|
Recuento escalar (BIGINT) |
|
No: los recuentos de diferentes ventanas no se pueden sumar |
Paneles en tiempo real, monitoreo sencillo |
|
Bitmap serializado (BYTES) |
|
Sí: los procesos posteriores pueden unir bitmaps de cualquier granularidad temporal |
Capas de almacén de datos, integración con procesamiento analítico en línea (OLAP), uniones de flujos y lotes |
Un recuento escalar "no aditivo" significa que no puede sumar dos recuentos de UV de 1 minuto para obtener un UV deduplicado de 2 minutos. La deduplicación a través de límites temporales requiere los propios objetos bitmap.
La preagregación en múltiples dimensiones tiene una restricción de escalado: para admitir consultas sobre cualquier combinación de N dimensiones, debe definir 2^N grupos. Planifique sus combinaciones de dimensiones en consecuencia.
Uso
Los cuatro ejemplos siguientes avanzan de lo simple a lo complejo:
|
Ejemplo |
Patrón |
Salida |
|
1. Deduplicación exacta en tiempo real |
Reemplazo directo de |
Recuento escalar de UV por ventana |
|
2. Almacenamiento de bitmaps a nivel de minuto |
Construcción y persistencia de bitmaps serializados |
Objetos bitmap binarios para su reutilización |
|
3. Filtrado de segmentos de usuarios |
Consolidación y operaciones de conjuntos sobre bitmaps almacenados |
Recuento de UV filtrado por hora |
|
4. Análisis de retención en tiempo real |
Unión de flujo y lote sobre objetos bitmap |
Recuento de usuarios retenidos frente al día anterior |
Para consultar la referencia completa de funciones BITMAP, consulte Funciones BITMAP.
Ejemplo 1: Deduplicación exacta en tiempo real
Cuente visitantes únicos (UV) a nivel de minuto directamente desde un flujo de eventos en tiempo real. Esta es la sustitución más sencilla de COUNT DISTINCT: ofrece el mismo resultado exacto con un menor consumo de recursos.
DDL
-- Source: real-time event stream
CREATE TEMPORARY TABLE user_events
(
user_id INT
,tag STRING -- Event tag
,event_time TIMESTAMP(3)
,WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
)
WITH ('connector' = 'kafka' ...)
;
-- Sink: minute-level UV statistics
CREATE TEMPORARY TABLE minute_uv
(
ts_date STRING
,ts_hour STRING
,ts_minute STRING
,tag STRING -- Event tag
,minute_uv BIGINT -- Deduplicated UV count
)
WITH ('connector' = 'jdbc' ...)
;
DML
INSERT INTO minute_uv
SELECT
DATE_FORMAT(window_start, 'yyyy-MM-dd') AS ts_date,
DATE_FORMAT(window_start, 'HH') AS ts_hour,
DATE_FORMAT(window_start, 'mm') AS ts_minute,
tag,
-- Builds a bitmap from user_ids in the window, then immediately returns
-- its cardinality (the exact deduplicated count).
-- Equivalent to COUNT(DISTINCT user_id) but with lower memory overhead.
BITMAP_BUILD_CARDINALITY_AGG(user_id) AS uv
FROM TABLE(
TUMBLE(
TABLE user_events,
DESCRIPTOR(event_time),
INTERVAL '1' MINUTE
)
)
GROUP BY
window_start,
window_end,
tag;
Cuándo usar este patrón: paneles en tiempo real y métricas de monitoreo por minuto.
La salida es un recuento escalar y no es aditiva. No puede sumar dos recuentos de UV de 1 minuto para obtener un UV deduplicado de 2 minutos. Para agregaciones que abarcan múltiples ventanas, utilice el Ejemplo 2.
Ejemplo 2: Almacenamiento de bitmaps a nivel de minuto
En lugar de generar un recuento escalar, serialice el propio bitmap en el almacenamiento. Los trabajos posteriores pueden unir estos blobs binarios en cualquier granularidad temporal (de minutos a horas o días) sin reprocesar los datos originales del evento.
DDL
-- Source: real-time event stream (same schema as Example 1)
CREATE TEMPORARY TABLE user_events
(
user_id INT
,tag STRING -- Event tag
,event_time TIMESTAMP(3)
,WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
)
WITH ('connector' = 'kafka' ...)
;
-- Sink: minute-level serialized bitmaps
CREATE TEMPORARY TABLE minute_bitmaps
(
ts_date STRING
,ts_hour STRING
,ts_minute STRING
,tag STRING -- Event tag
,minute_bm BYTES -- Serialized bitmap (RoaringBitmap binary format)
)
WITH ('connector' = 'jdbc' ...)
;
DML
INSERT INTO minute_bitmaps
SELECT
DATE_FORMAT(window_start, 'yyyy-MM-dd') AS ts_date,
DATE_FORMAT(window_start, 'HH') AS ts_hour,
DATE_FORMAT(window_start, 'mm') AS ts_minute,
tag,
-- Two-step process:
-- 1. BITMAP_BUILD_AGG: aggregates user_ids within the tumbling window
-- into an in-memory bitmap object.
-- 2. BITMAP_TO_BYTES: encodes the bitmap into RoaringBitmap binary format
-- for persistent storage.
BITMAP_TO_BYTES(
BITMAP_BUILD_AGG(user_id)
) AS user_id_bitmap
FROM TABLE(
TUMBLE(
TABLE user_events,
DESCRIPTOR(event_time),
INTERVAL '1' MINUTE
)
)
GROUP BY
window_start,
window_end,
tag;
Cuándo usar este patrón: para construir una capa intermedia de detalle de almacén de datos (DWD) o de servicio de almacén de datos (DWS), o para alimentar motores OLAP que admiten bitmaps de forma nativa, como StarRocks o Hologres.
Ventaja clave: la salida BYTES serializada conserva toda la información del conjunto. Cualquier trabajo posterior puede llamar a BITMAP_OR_AGG(BITMAP_FROM_BYTES(...)) para unir bitmaps de nivel de minuto en bitmaps de nivel de hora o de día, lo que permite consolidaciones flexibles por granularidad temporal sin acceder a los datos sin procesar originales.
Ejemplo 3: Filtrado de segmentos de usuarios
Con los bitmaps a nivel de minuto del Ejemplo 2, cuente los usuarios por hora que buscaron una palabra clave (etiqueta A) y agregaron un producto al carrito (etiqueta B), pero no completaron el pago (etiqueta C).
Todas las operaciones de conjuntos se ejecutan sobre un pequeño número de objetos bitmap en memoria, sin escaneos ni redistribuciones de tablas grandes.
DDL
-- Source: pre-computed minute-level bitmaps (produced by Example 2)
CREATE TEMPORARY TABLE minute_bitmaps
(
ts_date STRING
,ts_hour STRING
,ts_minute STRING
,tag STRING -- Event tag
,minute_bm BYTES -- Pre-computed serialized minute-level bitmap
)
WITH ('connector' = 'jdbc' ...)
;
-- Sink: hourly filtered UV counts
CREATE TEMPORARY TABLE hour_combined_uv
(
ts_date STRING
,ts_hour STRING
,tag STRING -- Metric label
,uv BIGINT
)
WITH ('connector' = 'jdbc' ...)
;
DML
La consulta se ejecuta en tres pasos lógicos:
Deserialización y consolidación: convierta los BYTES almacenados nuevamente en objetos bitmap y luego use
BITMAP_OR_AGGpara unir todos los bitmaps de nivel de minuto dentro de la hora; obtendrá un bitmap unido por etiqueta.Operaciones de conjuntos: aplique intersección y diferencia sobre los bitmaps horarios en memoria.
Recuento: llame a
BITMAP_CARDINALITYsobre el conjunto resultante.
INSERT INTO hour_combined_uv
SELECT
ts_date,
ts_hour,
'A and B andnot C' AS metric_name,
-- Step 3: Count the elements in the final bitmap.
BITMAP_CARDINALITY(
-- Step 2: Set operations -- (Tag A ∩ Tag B) − Tag C.
BITMAP_ANDNOT(
BITMAP_AND(hour_bm_a, hour_bm_b),
hour_bm_c
)
) AS uv
FROM (
SELECT
ts_date,
ts_hour,
-- Step 1: Deserialize stored BYTES and union all minute-level bitmaps
-- within the hour into a single hourly bitmap per tag.
BITMAP_OR_AGG(BITMAP_FROM_BYTES(minute_bm))
FILTER (WHERE tag = 'A') AS hour_bm_a,
BITMAP_OR_AGG(BITMAP_FROM_BYTES(minute_bm))
FILTER (WHERE tag = 'B') AS hour_bm_b,
BITMAP_OR_AGG(BITMAP_FROM_BYTES(minute_bm))
FILTER (WHERE tag = 'C') AS hour_bm_c
FROM minute_bitmaps
WHERE tag IN ('A', 'B', 'C')
GROUP BY
ts_date,
ts_hour
);
Cuándo usar este patrón: análisis de segmentos de usuarios, análisis de embudos o cualquier escenario que requiera un filtrado flexible entre dimensiones.
Ventaja clave: la lógica de negocio está completamente desacoplada de la generación de datos. Para agregar o cambiar una condición de filtro, modifique solo la consulta SQL; no se requieren cambios en el pipeline de origen.
Ejemplo 4: Análisis de retención en tiempo real
Calcule el recuento en tiempo real de visitantes recurrentes para una página específica: usuarios que aparecieron hoy y también aparecieron ayer.
Este proceso combina un flujo de bitmaps en tiempo real a nivel de minuto (datos de hoy, actualizados continuamente) con un bitmap precalculado a nivel de día desde el almacenamiento (datos de ayer, estáticos). La intersección proporciona el recuento de usuarios retenidos en milisegundos, sin necesidad de uniones de tablas grandes.
DDL
-- Source 1: real-time minute-level bitmaps for today (produced by Example 2)
CREATE TEMPORARY TABLE minute_bitmaps
(
ts_date STRING
,ts_hour STRING
,ts_minute STRING
,tag STRING -- Page name
,minute_bm BYTES -- Pre-computed serialized minute-level bitmap
)
WITH ('connector' = 'jdbc' ...)
;
-- Source 2: pre-computed day-level bitmaps from storage (historical data)
CREATE TEMPORARY TABLE daily_bitmaps
(
ts_date STRING
,tag STRING -- Page name
,daily_bm BYTES -- Pre-computed serialized day-level bitmap
)
WITH ('connector' = 'jdbc' ...)
;
-- Sink: real-time retention metrics
CREATE TEMPORARY TABLE realtime_retention_metrics
(
ts_date STRING
,tag STRING -- Page name
,retained_users BIGINT -- Users present both today and yesterday
,total_users BIGINT -- Total visitors yesterday (retention denominator)
,update_time TIMESTAMP(3)
,PRIMARY KEY (ts_date, tag) NOT ENFORCED
)
WITH ('connector' = 'jdbc' ...)
;
DML
La consulta se ejecuta en tres pasos lógicos:
Agregación en tiempo real (subconsulta T): une continuamente los fragmentos de bitmap a nivel de minuto del día de hoy en un bitmap acumulativo "hasta la fecha actual".
Unión de flujo y lote: une el bitmap en tiempo real de hoy con el bitmap diario estático de ayer desde el almacenamiento.
Intersección:
BITMAP_ANDsobre los dos bitmaps produce el conjunto de usuarios presentes en ambos días.
INSERT INTO realtime_retention_metrics
SELECT
T.ts_date,
T.tag,
-- Metric 1: retained users -- appeared today AND yesterday.
BITMAP_CARDINALITY(
BITMAP_AND(
T.today_bm, -- Real-time bitmap for today
BITMAP_FROM_BYTES(Y.daily_bm) -- Yesterday's historical bitmap
)
) AS retained_users,
-- Metric 2: yesterday's total visitors (retention denominator).
BITMAP_CARDINALITY(
BITMAP_FROM_BYTES(Y.daily_bm)
) AS total_users,
CURRENT_TIMESTAMP AS update_time
FROM (
-- Subquery T: aggregate today's minute-level slices in real time
-- into a single cumulative bitmap for the current day.
SELECT
ts_date,
tag,
BITMAP_OR_AGG(BITMAP_FROM_BYTES(minute_bm)) AS today_bm
FROM minute_bitmaps
GROUP BY ts_date, tag
) T
-- Stream-batch join: match each real-time record with the previous day's bitmap.
INNER JOIN daily_bitmaps Y
ON T.tag = Y.tag
AND CAST(T.ts_date AS DATE) = CAST(Y.ts_date AS DATE) + INTERVAL '1' DAY;
Cuándo usar este patrón: monitoreo en tiempo real de la respuesta de los usuarios a promociones, o análisis de la fidelidad de usuarios activos día contra día.
Ventajas clave:
Rapidez: las soluciones tradicionales unen dos tablas con cientos de millones de filas para calcular
COUNT DISTINCT. BITMAP realiza el mismo cálculo sobre unos pocos objetos binarios en milisegundos.Eficiencia de almacenamiento: los datos históricos, como
daily_bitmaps, residen en almacenamiento externo (Hadoop Distributed File System [HDFS], OSS o JDBC), lo que mantiene pequeño el estado de Flink.
Pasos siguientes
Funciones BITMAP: referencia completa de funciones que incluye sintaxis, tipos de parámetros y valores de retorno.