Base De Datos ClickHouse®: Qué Es, Cómo Funciona y Cuándo Falla
Tesla ha ingestado más de mil billones de filas en ClickHouse. Cloudflare procesa 11 millones de filas por segundo. Netflix mueve aproximadamente 5 petabytes de logs diarios a través de él.
Estos números son reales. Las empresas no tienen motivos para exagerar.
En Numia, ejecutamos ClickHouse con más de 100.000 millones de filas sirviendo 10 millones de peticiones al día. He pasado suficiente tiempo debuggeando queries, optimizando schemas y aprendiendo a base de golpes qué funciona y qué no. Este artículo es lo que me habría gustado que alguien me explicase antes de empezar.
La mayoría de artículos sobre "qué es ClickHouse" se quedan en el discurso comercial. Este va más allá. Al terminar, entenderás POR QUÉ ClickHouse está diseñado como lo está, CUÁNDO recurrir a él y DÓNDE se viene abajo.
Por Qué Existe ClickHouse
En 2009, Yandex tenía un problema. Su plataforma de análisis web, Yandex.Metrica, procesaba 200 millones de eventos al día. En 2016, esa cifra alcanzó los 25.000 millones de eventos diarios. El reto no era el almacenamiento, sino el análisis en tiempo real sobre datos no agregados.
Las soluciones tradicionales fallaban a esa escala. Los OLAP cubes requerían pre-agregación, lo que mataba la flexibilidad de las queries. Las bases de datos orientadas a filas como PostgreSQL no podían escanear miles de millones de filas con la rapidez necesaria para dashboards interactivos.
Los números que lo rompieron todo: 374 servidores, 20,3 billones de filas, 2 petabytes comprimidos. Los tiempos de carga de informes promediaban 26 segundos. Tras migrar a ClickHouse: 0,8 segundos. Mismos informes, 32 veces más rápido. No por mejor hardware, sino por mejor arquitectura.
Dos decisiones hicieron esto posible:
- Almacenamiento columnar: leer solo los datos que tu query necesita
- Ejecución vectorizada: procesar datos en batches, no fila a fila
El resto del diseño de ClickHouse se deriva de estas dos ideas.
Almacenamiento Columnar
La diferencia entre almacenamiento por filas y columnar es el factor que más influye en el rendimiento de las queries.
Considera una tabla sencilla:
CREATE TABLE events (
timestamp DateTime,
user_id UInt64,
event_type String
)
Almacenamiento por filas (PostgreSQL, MySQL):
[timestamp1, user_id1, event_type1], [timestamp2, user_id2, event_type2]...
Cada fila se almacena junta. Leer una columna implica leerlas todas.
Almacenamiento columnar (ClickHouse):
[timestamp1, timestamp2...], [user_id1, user_id2...], [event_type1, event_type2...]
Cada columna se almacena por separado. Leer una columna significa leer solo esa columna.
Esto importa por cómo funcionan las queries de análisis. "¿Cuántos eventos por día?" solo necesita la columna timestamp. Las bases de datos por filas leen el 100% de los datos. Las columnares leen ~33% (una de tres columnas). ¿Una tabla con 50 columnas donde tu query necesita 3? Las bases por filas lo leen todo. Las columnares leen el 6%.
La otra cara de la moneda: recuperar una fila completa es peor. Una base de datos por filas lee un bloque contiguo. Una columnar lee de cada archivo de columna por separado, un acceso por columna. Con 50 columnas, son 50 lecturas en vez de 1. Por eso las bases de datos columnares son terribles para workloads tipo SELECT * WHERE id = 123.
Engines y MergeTree
MergeTree es donde ClickHouse hace la mayor parte del trabajo. El nombre es literal: los datos llegan en partes y un proceso en background las mergea.
La jerarquía importa:
- Tabla: colección de partes
- Parte: fragmento de datos inmutable creado en cada INSERT
- Gránulo: la unidad más pequeña que ClickHouse lee (por defecto: 8.192 filas)
- Marca: puntero a la ubicación de un gránulo
A diferencia de las bases de datos tradicionales que indexan cada fila (B-tree), ClickHouse usa un índice disperso: una entrada por gránulo, no una por fila. Cada entrada se llama marca, y apunta a donde empieza un gránulo en disco.
Para una tabla con 8,87 millones de filas:
- 8,87M / 8.192 = 1.083 gránulos
- El índice primario almacena una marca por gránulo
- Tamaño total del índice: 96,93 KB en memoria
El índice primario completo cabe en RAM. Siempre. Esto es así por diseño. Cuando lanzas una query, ClickHouse hace una búsqueda binaria sobre estas marcas para encontrar qué gránulos podrían contener tus datos. Con 1.083 marcas, son ~10 pasos. Después lee como mucho 8.192 filas por gránulo coincidente.
Para crear una tabla MergeTree:
CREATE TABLE events (
timestamp DateTime,
user_id UInt64,
event_type LowCardinality(String),
payload String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (event_type, user_id, timestamp);
Las dos decisiones más importantes:
PARTITION BY controla la separación física. Particionar por mes significa que los datos de enero están en archivos distintos a los de febrero. Las queries que filtran por mes se saltan particiones enteras.
ORDER BY controla el orden dentro de cada parte.
El flujo de datos es el siguiente:
- INSERT crea una nueva parte (inmutable)
- Un thread en el background mergea partes pequeñas en otras mayores
- Los merges pueden activar computación en engines especializadss
Las variantes más relevantes:
| Motor | Propósito |
|---|---|
| MergeTree | Motor base |
| ReplacingMergeTree | Deduplica durante el merge |
| AggregatingMergeTree | Pre-agrega durante el merge |
Por Qué ORDER BY Importa
El índice disperso solo puede filtrar si los datos están ordenados de forma que agrupe las filas relevantes en los mismos gránulos. Eso es lo que hace ORDER BY. Controla cómo se ordenan las filas dentro de cada parte, lo que a su vez controla cuántos gránulos puede saltarse ClickHouse.
Para la siguiente query:
SELECT * FROM events
WHERE event_type = 'click' AND user_id = 12345;
Con ORDER BY (event_type, user_id, timestamp):
- La búsqueda binaria encuentra los gránulos de
event_type = 'click' - Dentro de esos, encuentra los gránulos de
user_id = 12345 - Escanea 8.192 filas
Con ORDER BY (timestamp, event_type, user_id):
event_typees segundo en el orden- No puede usar el índice eficientemente
- Escanea potencialmente millones de filas
Misma query, mismos datos, distinto ORDER BY. La diferencia puede ser de 100x en tiempo de ejecución.
La regla real: primero patrones de query, después cardinalidad. Tu ORDER BY debería empezar por las columnas que más aparecen en tus cláusulas WHERE. Una columna por la que nunca filtras es inútil en el índice, da igual su cardinalidad.
Una vez identificadas las columnas por las que realmente haces queries, ordénalas de baja a alta cardinalidad. ¿Por qué? Una columna de baja cardinalidad al principio elimina grandes bloques de gránulos de golpe. Si event_type tiene 10 valores únicos y user_id tiene 1 millón, poner event_type primero elimina ~90% de los gránulos de inmediato. Invierte el orden y user_id no puede ayudar porque cada gránulo contiene muchos user_ids diferentes dispersos por él.
Por ejemplo, si siempre filtras por tenant_id y timestamp pero rara vez por event_type, tu ORDER BY debería ser (tenant_id, timestamp) aunque event_type tenga menor cardinalidad. Un índice que no coincide con tus queries es simplemente ordenación desperdiciada.
La compresión también se beneficia de un buen orden. La documentación muestra compresión de 3:1 con orden deficiente y 39:1 con orden óptimo. Mismos datos, 13 veces mejor compresión solo por el orden de columnas.
Ejecución Vectorizada
Las bases de datos tradicionales procesan fila a fila:
por cada fila:
aplicar filtro
calcular agregación
ClickHouse procesa en batches:
por cada batch de 65.536 filas:
aplicar filtro al batch completo (SIMD)
calcular agregación del batch (SIMD)
SIMD significa Single Instruction, Multiple Data. Una instrucción de CPU procesa múltiples valores simultáneamente. Las CPUs modernas soportan AVX2 (4 doubles a la vez) y AVX-512 (8 doubles a la vez).
ClickHouse tiene múltiples implementaciones SIMD por operador y selecciona automáticamente en función de tu CPU en tiempo de ejecución. Tiene más de 30 variantes precompiladas de tablas hash para diferentes patrones de datos.
En la práctica, esto supone agregaciones 10-100x más rápidas que el procesamiento fila a fila. Es la razón por la que ClickHouse agrega miles de millones de filas en segundos mientras PostgreSQL tarda minutos con los mismos datos.
Compresión
El almacenamiento columnar tiene otra ventaja: valores similares quedan unos junto a otros, y los valores similares se comprimen bien. Timestamps junto a timestamps consiguen compresión de 10:1 a 30:1. Con secuencias monótonas, ClickHouse alcanza 800:1 usando encoding DoubleDelta (más detalles abajo).
Cada columna almacenada en disco se comprime. Cuanto más pequeña sea en disco, menos I/O necesita tu query. ClickHouse aplica compresión en dos capas:
- Un códec especializado transforma los datos para hacerlos más compresibles
- Un algoritmo de compresión general los comprime de verdad
Primero doblas todo con cuidado, luego sellas la bolsa al vacío.
Compresión General
Estos algoritmos funcionan con cualquier dato. Eliges uno según un compromiso sencillo:
| Códec | Velocidad | Compresión |
|---|---|---|
| LZ4 | Descompresión más rápida | Buena (reducción 2-3x) |
| ZSTD | Ligeramente más lento | ~30% mejor que LZ4 |
Ambos descomprimen más rápido de lo que la mayoría de SSDs pueden leer, así que en la práctica apenas notas la diferencia de velocidad. LZ4 es el valor por defecto y una opción segura. Usa ZSTD cuando el coste de almacenamiento importe más que el ahorro marginal de CPU.
Códecs Especializados
Estos no comprimen datos por sí mismos. Transforman los datos a una forma que la compresión general puede manejar mucho mejor. Cada uno apunta a un patrón concreto:
Delta almacena la diferencia entre valores consecutivos en vez de los valores en sí.
Original: [1000, 1001, 1002, 1003]
Tras Delta: [1000, 1, 1, 1]
Una columna llena de números pequeños como 1, 1, 1 se comprime mucho mejor que 1000, 1001, 1002. Ideal para timestamps y cualquier columna monótonamente creciente.
DoubleDelta va un paso más allá y almacena el delta de los deltas.
Original: [1000, 1005, 1010, 1015]
Tras Delta: [1000, 5, 5, 5]
Tras DoubleDelta: [1000, 5, 0, 0, 0]
Cuando el intervalo entre valores es constante (como un sensor que reporta cada 5 segundos), el segundo delta es todo ceros. Esto puede alcanzar compresión de 800:1 en series temporales regulares. Ideal para contadores y secuencias de intervalo regular.
Gorilla usa encoding XOR, diseñada para números en coma flotante. Hace XOR de cada valor con el anterior. Cuando floats consecutivos son similares, el resultado es mayoritariamente ceros.
Original: [36.6, 36.7, 36.6, 36.8]
Tras XOR: [36.6, (mayoritariamente ceros), (mayoritariamente ceros), (mayoritariamente ceros)]
Ideal para métricas y mediciones donde los valores no saltan mucho entre filas.
Juntando Las Piezas
Combinas un códec especializado con uno general por columna, eligiendo según el tipo de dato:
CREATE TABLE metrics (
timestamp DateTime CODEC(Delta, ZSTD),
value Float64 CODEC(Gorilla, LZ4),
counter UInt64 CODEC(DoubleDelta, ZSTD)
)
Si no tienes claro qué usar, los valores por defecto (LZ4, sin códec especializado) funcionan bien. Añade códecs especializados cuando entiendas los patrones de tus datos y quieras exprimir mejor compresión.
Cuándo ClickHouse Brilla
ClickHouse rinde mejor cuando estás escaneando muchos datos y agregándolos. Análisis de logs, series temporales, tracking de eventos, dashboards en tiempo real. Cloudflare procesa 11M filas/seg a través de él. PostHog construyó todo su producto de analytics encima. Los timestamps se ordenan de forma natural y comprimen bien con encoding Delta, lo que hace las cargas de series temporales particularmente rápidas.
Los patrones de query que funcionan bien son agregaciones (COUNT, SUM, AVG) sobre millones de filas, filtros por rango temporal como WHERE timestamp > now() - INTERVAL 7 DAY, y GROUP BY sobre columnas de baja cardinalidad como país o tipo de dispositivo.
Algunos números de producción:
| Empresa | Escala |
|---|---|
| Tesla | Más de 1 mil billones de filas |
| Netflix | ~5 PB logs/día |
| Cloudflare | 11M filas/seg, 47 Gbps |
| Yandex | 20,3 billones de filas (2014) |
Cuándo ClickHouse No Funciona
ClickHouse no es una base de datos de propósito general. Me he dado contra cada uno de estas paredes en producción.
No hay transacciones ACID ni bloqueo a nivel de fila. Si necesitas garantías transaccionales, usa PostgreSQL.
Los point lookups en columnas que no están en el ORDER BY son dolorosos:
-- Lento si user_id no es lo primero en ORDER BY
SELECT * FROM events WHERE user_id = 12345;
El índice disperso no puede saltarse gránulos eficientemente, así que acabas escaneando todo.
Los updates no funcionan como esperarías. ClickHouse no actualiza in situ. Puedes usar ReplacingMergeTree, pero el timing del merge no está garantizado. CollapsingMergeTree funciona pero requiere delete/insert explícito con columnas de signo. Nada de esto es tan limpio como UPDATE ... WHERE.
Los JOINs en columnas de alta cardinalidad son problemáticos:
-- Millones de claves de join únicas = dolor
SELECT * FROM events e JOIN users u ON e.user_id = u.id;
El lado derecho del JOIN se carga en memoria. Tablas grandes provocan OOM. La solución es denormalizar tus datos o usar diccionarios.
Y por último, los inserts pequeños y frecuentes. Cada INSERT crea una parte en disco. Miles de inserts pequeños significa miles de partes, lo que genera overhead de merge. Agrupa tus inserts para evitar problemas(1.000-100.000 filas cada vez).
El Modelo Mental
ClickHouse es rápido porque solo lee las columnas que necesitas, mantiene todo el índice primario en RAM (96 KB para 8,87 millones de filas), procesa filas en batches SIMD y comprime todo con un factor de 10-30x para que haya menos que leer de disco.
Falla cuando necesitas transacciones, lookups rápidos en columnas no indexadas, updates frecuentes, JOINs con millones de claves únicas o inserts de filas individuales a alto volumen.
Esa es la contrapartida. ClickHouse sacrifica flexibilidad de escritura y garantías transaccionales a cambio de velocidad de lectura en queries analíticas. Si eso encaja con tu workload, nada se le acerca. Si no, te pelearás con el sistema a cada paso.
Publicado originalmente en obsessionDB. Lee el artículo original aquí.