<!-- Canonical: https://lucioduran.com/es/blog/clickhouse-olap-real-time-analytics-architecture
Language: es
Content-SHA256: ad9f0a0dc2fcb4c99adf8d7185a8d1f3a4e79ea63e18cb9bcb2c9e1a1b756439
-->

Databases

# ClickHouse: OLAP Columnar para Analytics en Tiempo Real sobre 10 Mil Millones de Filas

2026-03-016 min lectura

[Lucio Durán](<https://lucioduran.com/es>)
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/clickhouse-olap-real-time-analytics-architecture>)

## Almacenamiento Columnar y Rendimiento de Queries Analíticas

En una base de datos orientada a filas como [PostgreSQL](<https://www.postgresql.org/>), los datos se almacenan fila por fila. Cuando corrés ` SELECT country, COUNT(*) FROM events WHERE event = 'click' GROUP BY country `, PostgreSQL lee cada fila del disco, incluyendo columnas que no se necesita. En una tabla con 50 columnas, se está leyendo 50x más datos de lo necesario.

ClickHouse almacena datos columna por columna. Esa misma query solo lee las columnas ` event ` y ` country `. En una tabla con 10 mil millones de filas y 50 columnas, eso es ~98% menos I/O. Pero mejora, cada columna se comprime independientemente. La columna ` event ` puede tener solo 20 valores distintos en 10 mil millones de filas, que se comprime de 80GB (strings crudos) a ~500MB con LZ4 + dictionary encoding.

## MergeTree: El Motor Que Lo Hace Funcionar

Cada tabla de ClickHouse está respaldada por un motor de tabla. MergeTree es el motor principal para workloads analíticos:

```sql
CREATE TABLE events (
 tenant_id UInt32,
 event_date Date,
 event_time DateTime64(3),
 event_type LowCardinality(String),
 user_id UInt64,
 session_id String,
 page_url String,
 country LowCardinality(FixedString(2)),
 device_type LowCardinality(String),
 duration_ms UInt32,
 properties Map(String, String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (tenant_id, event_date, event_type, user_id)
TTL event_date + INTERVAL 13 MONTH
SETTINGS index_granularity = 8192;
```

Decisiones clave de diseño:

**PARTITION BY**: Particiones mensuales por fecha de evento. Las particiones son la unidad de gestión de datos, es posible eliminar, mover o desprender meses enteros eficientemente.

**ORDER BY**: Esta es la clave primaria y la decisión más importante. ClickHouse almacena datos ordenados por esta clave y construye un índice sparse (una entrada por cada ` index_granularity ` filas) para data skipping. Las queries que filtran por las columnas líderes de la clave ORDER BY saltean bloques enteros de datos sin leerlos.

` tenant_id ` va primero porque cada query en un sistema multi-tenant filtra por tenant. Después ` event_date ` para queries de rango temporal, ` event_type ` para filtrado por categoría, y ` user_id ` para drilldowns a nivel de usuario.

## Vistas Materializadas: Pre-Agregación en Tiempo de Ingesta

Las vistas materializadas en ClickHouse no son lo que podrías esperar de PostgreSQL, se actualizan incrementalmente al momento del insert, no se refrescan periódicamente:

```sql
CREATE MATERIALIZED VIEW events_daily_mv
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (tenant_id, event_date, event_type, country)
AS SELECT
 tenant_id,
 event_date,
 event_type,
 country,
 count() AS event_count,
 uniqState(user_id) AS unique_users,
 sumState(duration_ms) AS total_duration_ms
FROM events
GROUP BY tenant_id, event_date, event_type, country;
```

Cuando hacés ` INSERT INTO events `, ClickHouse automáticamente transforma las nuevas filas a través del SELECT de la vista materializada e inserta los resultados agregados en la tabla ` events_daily_mv `.

```sql
SELECT
 event_date,
 event_type,
 sum(event_count) AS events,
 uniqMerge(unique_users) AS users,
 sumMerge(total_duration_ms) / sum(event_count) AS avg_duration_ms
FROM events_daily_mv
WHERE tenant_id = 42
 AND event_date BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY event_date, event_type
ORDER BY event_date;
```

Esta query escanea la tabla pre-agregada (~10.000 filas para un mes) en vez de la tabla de eventos crudos (~500 millones de filas para el mismo tenant y período). Tiempo de respuesta: 15ms vs 1.800ms.

Un setup típico incluye cuatro vistas materializadas sobre la tabla de eventos. Overhead total de almacenamiento para las cuatro tablas MV: ~3% de la tabla de eventos crudos. La mejora de rendimiento de queries es 50-200x para queries de dashboard.

## Optimización de Queries en 10B Filas

Las técnicas de optimización reales que importan a escala:

### 1. Filtrar por columnas del ORDER BY

```sql
-- Rápido: tenant_id y event_date son las primeras dos columnas del ORDER BY
SELECT event_type, count()
FROM events
WHERE tenant_id = 42 AND event_date = '2026-03-15'
GROUP BY event_type;
-- Escanea: ~2M filas de 10B (0.02%)

-- Lento: country no está en la clave ORDER BY
SELECT country, count()
FROM events
WHERE country = 'US'
GROUP BY country;
-- Escanea: las 10B filas todas
```

### 2. Funciones aproximadas para analytics a gran escala

```sql
-- Conteo de únicos exacto: 4.2 segundos en 10B filas
SELECT uniqExact(user_id) FROM events WHERE tenant_id = 42;

-- Conteo de únicos aproximado (2% error): 0.3 segundos
SELECT uniq(user_id) FROM events WHERE tenant_id = 42;
```

Para dashboards donde +-2% de precisión está bien, ` uniq() ` (HyperLogLog) es 14x más rápido que ` uniqExact() `. Los stakeholders de negocio nunca notaron la diferencia.

## ClickHouse vs DuckDB vs Druid: Análisis Comparativo

Evaluando workloads de producción en los tres, cada uno brilla en diferentes áreas:

| Criterio | ClickHouse | DuckDB | Druid |
| --- | --- | --- | --- |
| Escala de datos | 100GB - PB | 1GB - 100GB | 100GB - PB |
| Usuarios concurrentes | Docenas a cientos | Un solo usuario | Cientos a miles |
| Ingesta | Batch (alto throughput) | Batch/archivo | Streaming tiempo real |
| Latencia de query (10B filas) | 200ms - 5s | N/A (solo local) | 100ms - 2s |
| Complejidad operacional | Media | Ninguna (embebido) | Alta |
| Soporte SQL | SQL analítico completo | SQL completo + extensiones | SQL limitado |
| Mejor para | API backend de analytics | Data science, análisis local | Dashboards tiempo real |

**DuckDB** es notable por lo que hace, queries analíticos en archivos locales con cero infraestructura. Es excelente para prototipar queries antes de deployar a ClickHouse y para análisis ad-hoc en exports CSV/Parquet. Pero no es un servidor. No es posible apuntar 50 usuarios de dashboard a una instancia de DuckDB.

**Druid** está construido a propósito para analytics en tiempo real con queries sub-segundo en dimensiones de muy alta cardinalidad. Maneja queries concurrentes mejor que ClickHouse a escala extrema. Pero la complejidad operacional de Druid es significativamente mayor, tiene seis tipos de nodos, cada uno necesitando capacity planning separado. ClickHouse tiene un tipo de nodo.

## La Arquitectura de Producción

```
Kafka ──► Buffer Table ──► Events Table ──► Vistas Materializadas
 │
 ┌───────┼───────┐
 │ │ │
 Queries Queries Queries
 Dashboard API Ad-hoc
```

Cluster ClickHouse: 3 nodos, cada uno con 32 cores, 128GB RAM, 4TB NVMe SSD. ReplicatedMergeTree con 2 réplicas para tolerancia a fallos.

```sql
CREATE TABLE events_buffer AS events
ENGINE = Buffer(currentDatabase(), events, 16, 10, 100, 10000, 100000, 1000000, 10000000);
```

La Buffer table acumula inserts en memoria y flushea a la tabla events subyacente en batches. Esto convierte los inserts por mensaje del consumidor de Kafka en batch inserts eficientes de 10.000-100.000 filas.

Tasa de ingesta: ~120.000 eventos/segundo sostenidos. Carga de queries: ~200 queries/segundo entre todos los usuarios de dashboard. Latencia P99 para queries de dashboard (pegando a vistas materializadas): 180ms. P99 para queries ad-hoc en eventos crudos (un mes completo, un solo tenant): 3,2 segundos.

Todo corre en tres servidores que cuestan alrededor de $2.400/mes. El setup equivalente en PostgreSQL (que ni siquiera podía servir las queries a tiempo) estaba corriendo en un r6g.8xlarge ($2.800/mes) y seguía tirando timeout.

ClickHouse no es magia. Son trade-offs de ingeniería hechos explícitos: entregás transacciones, updates a nivel de fila, y garantías ACID. A cambio, obteners rendimiento de queries analíticos que es genuinamente 100-1000x más rápido que bases de datos de propósito general. Para workloads de analytics, ese trade-off es obvio una vez que viste los números.

## Preguntas frecuentes

### Cuándo debería usar ClickHouse en vez de PostgreSQL para analytics?

Cuando tus queries de analytics escanean millones de filas con agregaciones (COUNT, SUM, AVG, percentiles) y tu volumen de datos excede lo que PostgreSQL maneja cómodamente (aproximadamente 100M+ filas). ClickHouse está construido a propósito para estos workloads y puede ser 100-1000x más rápido que PostgreSQL en queries de agregación. Sin embargo, ClickHouse no es un reemplazo de PostgreSQL, no soporta transacciones, foreign keys, ni lookups eficientes por clave primaria.

### Cómo se compara ClickHouse con DuckDB?

DuckDB es una base de datos analítica embebida, corre dentro del proceso de tu aplicación sin servidor. Es excelente para analytics de un solo usuario, notebooks de data science, y procesamiento de datasets hasta ~100GB. ClickHouse es un servidor distribuido diseñado para acceso concurrente multi-usuario, ingesta en tiempo real, y datasets desde 100GB hasta petabytes. Utilizar DuckDB para análisis local, ClickHouse para servicios de analytics en producción.

### ClickHouse puede manejar ingesta de datos en tiempo real?

Sí, pero con caveats. ClickHouse puede ingestar millones de filas por segundo, pero está optimizado para inserts en batch (1000+ filas por INSERT). Los inserts de una sola fila crean partes de datos diminutas que aumentan la presión de merge y degradan el rendimiento de queries. Utilizar una buffer table o inserts en batch desde Kafka/colas de mensajes para patrones de ingesta en tiempo real.

## Tips

1. **Tip:** Se recomienda definir siempre la clave ` ORDER BY ` en tablas MergeTree para que coincida con los patrones más comunes de ` WHERE ` y ` GROUP BY `. ClickHouse usa la clave primaria para data skipping, si tus queries filtran por ` tenant_id ` y ` event_date `, colocarlos primero en el ` ORDER BY `. Las queries pueden pasar de 12 segundos a 200ms solo reordenando la clave primaria para coincidir con los patrones de acceso.
2. **Tip:** Se recomienda utilizar ` LowCardinality(String) ` en vez de ` String ` plano para cualquier columna con menos de ~10.000 valores distintos (códigos de país, tipos de evento, campos de estado). Hace dictionary-encoding de los valores y puede reducir el almacenamiento un 80% mientras hace el filtrado 3-5x más rápido. Convertir una tabla de eventos de 2TB a LowCardinality en tres columnas puede encogerla a 400GB.
3. **Tip:** Nunca se debe utilizar ` SELECT * ` en ClickHouse. El almacenamiento columnar significa que cada columna es un archivo separado, ` SELECT * ` lee cada columna del disco aunque solo necesites tres. En una tabla con 50 columnas, ` SELECT col1, col2, col3 ` corre 18x más rápido que ` SELECT * ` para el mismo filtro. Es necesario ser explícito sobre las columnas requeridas.
4. **Tip:** Se recomienda establecer ` max_threads ` por query basándote en tu workload. El default usa todos los cores disponibles, que está bien para analytics de un solo usuario pero va a matar de hambre a queries concurrentes. Para una API de analytics multi-tenant sirviendo 50 usuarios concurrentes, establecer ` max_threads = 4 ` por query produce latencias p99 mucho más consistentes, 800ms en vez de swings salvajes entre 200ms y 5s.

- clickhouse
- olap
- analytics
- columnar-database
- mergetree
- materialized-views
- real-time-analytics
- duckdb
- druid

[Volver al Blog](<https://lucioduran.com/es/blog>)
