Imagen destacada para Guía avanzada de ClickHouse: optimización, vistas materializadas y trucos reales

Guía avanzada de ClickHouse: optimización, vistas materializadas y trucos reales

Publicado el:

Tiempo de lectura: 20 min

Tema: Tecnologia

Autor: Leandro Valencia

#ClickHouse#optimización#SQL#rendimiento#OLAP#self-hosted

Cómo sacar el máximo a ClickHouse: ORDER BY, vistas materializadas, projections, codecs, TTL, tiering a S3 y diagnóstico de consultas lentas. Con SQL real.

Tabla de Contenidos

Principio fundamental: leer menos datos

Todo lo que viene a continuación es una variación de la misma idea. ClickHouse no es rápido porque procese datos deprisa —también lo hace— sino porque procesa muchos menos datos de los que crees.

Cada consulta que lanzas te dice exactamente cuánto ha leído:

6 rows in set. Elapsed: 0.021 sec. Processed 1.14 million rows, 8.32 MB

Esa línea es tu métrica principal. Si tu tabla tiene 500 millones de filas y una consulta procesa 500 millones, no has optimizado nada: estás haciendo un escaneo completo muy rápido. El objetivo siempre es bajar ese número.

Las herramientas para bajarlo, en orden de impacto: la clave de ordenación, las vistas materializadas, las projections, los índices de salto y la partición. En ese orden.

1. La clave de ordenación: la decisión que lo determina todo

Si solo te llevas una cosa de este post, que sea esta sección.

Cómo funciona el índice disperso

ClickHouse no tiene índices B-tree por fila. Ordena físicamente los datos en disco según tu ORDER BY y guarda una marca cada 8.192 filas (un gránulo), anotando el valor de la clave en esa posición. El índice completo ocupa unos pocos megabytes por terabyte.

Cuando filtras por una columna que está en el ORDER BY, ClickHouse mira ese índice, identifica qué gránulos podrían contener valores coincidentes y lee solo esos. Todo lo demás no se toca.

La consecuencia es directa: una columna solo acelera tu consulta si está en el prefijo del ORDER BY. Si tu clave es (evento, fecha, usuario_id), filtrar por evento es rapidísimo, filtrar por evento AND fecha también, pero filtrar solo por usuario_id obliga a un escaneo completo. El orden importa igual que en un índice compuesto de Postgres, pero las consecuencias son mucho mayores.

Las tres reglas para elegir el ORDER BY

Regla 1: primero las columnas por las que filtras siempre. Mira tus consultas reales, no las hipotéticas. Si el 95% lleva WHERE tenant_id = ?, tenant_id va primero.

Regla 2: a igualdad de uso, menor cardinalidad primero. Una columna con 6 valores distintos (país) agrupa mejor los datos que una con 50.000 (usuario_id). Poner alta cardinalidad delante fragmenta los gránulos y arruina tanto la compresión como el salto de bloques.

Regla 3: la columna temporal casi siempre va, pero rara vez primera. Es tentador poner timestamp al inicio porque "todo se filtra por fecha". Suele ser mejor (tenant_id, evento, fecha) que (fecha, tenant_id, evento), porque la fecha ya está bastante correlacionada con el orden natural de inserción.

Un ejemplo de tabla bien diseñada:

CREATE TABLE eventos
(
    tenant_id   UInt32,
    fecha       Date,
    timestamp   DateTime CODEC(Delta, ZSTD(1)),
    evento      LowCardinality(String),
    pais        LowCardinality(String),
    usuario_id  UInt32 CODEC(Delta, ZSTD(1)),
    url         String CODEC(ZSTD(3)),
    duracion_ms UInt32 CODEC(T64, ZSTD(1))
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(fecha)
ORDER BY (tenant_id, evento, fecha, usuario_id);

Separar clave primaria de clave de ordenación

Un detalle que poca gente usa: puedes tener un ORDER BY largo y una PRIMARY KEY más corta.

ENGINE = MergeTree
ORDER BY (tenant_id, evento, fecha, usuario_id)
PRIMARY KEY (tenant_id, evento)

El ORDER BY controla el orden físico (y por tanto la compresión), mientras que la PRIMARY KEY controla qué se guarda en el índice en memoria. Si tu clave de ordenación tiene cinco columnas y solo filtras por las dos primeras, esto reduce el uso de RAM del índice sin perder nada.

Cómo verificar que tu clave funciona

Activa el trace y observa cuántos gránulos se descartan:

SET send_logs_level = 'trace';

SELECT count() FROM eventos WHERE tenant_id = 42 AND evento = 'purchase';

En los logs verás líneas como:

Key condition: (column 0 in [42, 42]), (column 1 in ['purchase', 'purchase'])
Selected 3/48 parts by partition key, 3 parts by primary key, 14/61035 marks by primary key

14/61035 marks significa que leíste el 0,02% de la tabla. Eso es una clave bien elegida. Si ves 61035/61035, tu filtro no está usando el índice.

2. Vistas materializadas: mover el trabajo al momento de la inserción

Esta es la característica que más impacto tiene y la que más se malinterpreta.

Una vista materializada en ClickHouse no es una caché ni una tabla que se refresca periódicamente. Es un trigger de inserción: cada vez que llega un bloque de datos a la tabla origen, se ejecuta la consulta sobre ese bloque y el resultado se escribe en la tabla destino. Los datos originales nunca se vuelven a leer.

Esto tiene una implicación crítica: una vista materializada solo ve los datos que se insertan después de crearla. Los históricos hay que rellenarlos a mano.

El patrón estándar

Tres piezas: tabla origen, tabla destino de agregados, y la vista que las conecta.

-- 1. Tabla destino con estados de agregación
CREATE TABLE eventos_diarios
(
    tenant_id       UInt32,
    fecha           Date,
    evento          LowCardinality(String),
    pais            LowCardinality(String),
    total           UInt64,
    usuarios_unicos AggregateFunction(uniq, UInt32),
    duracion_media  AggregateFunction(avg, UInt32)
)
ENGINE = SummingMergeTree
ORDER BY (tenant_id, fecha, evento, pais);

-- 2. La vista materializada que la alimenta
CREATE MATERIALIZED VIEW mv_eventos_diarios
TO eventos_diarios
AS
SELECT
    tenant_id,
    fecha,
    evento,
    pais,
    count()                 AS total,
    uniqState(usuario_id)   AS usuarios_unicos,
    avgState(duracion_ms)   AS duracion_media
FROM eventos
GROUP BY tenant_id, fecha, evento, pais;

Fíjate en uniqState y avgState. El sufijo -State guarda el estado intermedio de la agregación en lugar del resultado final. Esto es lo que permite combinar agregados de bloques distintos correctamente.

Para consultar, usas el sufijo -Merge:

SELECT
    fecha,
    evento,
    sum(total)                    AS eventos,
    uniqMerge(usuarios_unicos)    AS usuarios,
    avgMerge(duracion_media)      AS duracion
FROM eventos_diarios
WHERE tenant_id = 42
  AND fecha >= today() - 30
GROUP BY fecha, evento
ORDER BY fecha;

Por qué esto importa tanto: esta consulta lee unas pocas miles de filas pre-agregadas en lugar de cientos de millones de filas crudas. La diferencia no es de 2x, es de dos o tres órdenes de magnitud. Y como el cálculo se hace en la inserción, el coste está distribuido en el tiempo en vez de concentrado en el momento en que tu usuario mira el dashboard.

Un error frecuente: usar avg() en lugar de avgState() y luego promediar promedios al consultar. Eso da resultados incorrectos porque la media de medias no es la media. Los estados de agregación existen precisamente para evitar esto.

Rellenar el histórico

Como las vistas solo capturan datos nuevos, tras crear la vista rellena hacia atrás con un INSERT ... SELECT sobre la misma consulta:

INSERT INTO eventos_diarios
SELECT
    tenant_id, fecha, evento, pais,
    count()                 AS total,
    uniqState(usuario_id)   AS usuarios_unicos,
    avgState(duracion_ms)   AS duracion_media
FROM eventos
WHERE fecha < today()
GROUP BY tenant_id, fecha, evento, pais;

Hazlo por rangos de fechas si la tabla es grande, para no reventar la memoria.

Vistas materializadas refrescables

Desde hace algunas versiones existe también la variante refrescable, que sí re-ejecuta la consulta completa periódicamente:

CREATE MATERIALIZED VIEW mv_resumen
REFRESH EVERY 1 HOUR
ENGINE = MergeTree ORDER BY fecha
AS SELECT ... ;

Útil cuando necesitas JOINs con tablas de dimensiones que cambian, algo que las vistas incrementales manejan mal. Más cara, pero mucho más simple de razonar.

3. Projections: la misma tabla ordenada de dos formas

Aquí está el problema clásico: tu ORDER BY está optimizado para filtrar por tenant_id, pero también necesitas consultas rápidas filtrando por url. Solo puedes tener un orden físico.

Las projections resuelven esto guardando una copia adicional de los datos ordenada de otra forma, dentro de la misma tabla. ClickHouse elige automáticamente cuál usar.

ALTER TABLE eventos ADD PROJECTION proj_por_url
(
    SELECT
        url,
        fecha,
        count(),
        avg(duracion_ms)
    GROUP BY url, fecha
);

-- Materializar para los datos existentes
ALTER TABLE eventos MATERIALIZE PROJECTION proj_por_url;

Ahora una consulta que agrupe por url usará la projection sin que tengas que cambiar el SQL. Verifícalo con EXPLAIN:

EXPLAIN indexes = 1
SELECT url, count() FROM eventos WHERE fecha >= today() - 7 GROUP BY url;

Projections vs. vistas materializadas. La pregunta obvia. Mi criterio:

Usa projections cuando quieres los mismos datos con otro orden o una agregación derivada de la misma tabla, y valoras que sea transparente (no cambias las consultas, la consistencia es automática).

Usa vistas materializadas cuando necesitas transformar los datos, escribir en una tabla con TTL distinto, encadenar varias etapas o combinar varias fuentes. Y cuando la tabla destino debe sobrevivir de forma independiente.

El coste de las projections es almacenamiento (guardas los datos dos veces) y velocidad de inserción. No añadas cinco projections a una tabla que recibe inserts constantes.

4. Codecs de compresión: el ajuste más infravalorado

Menos bytes en disco = menos I/O = consultas más rápidas. La compresión no es un tema de ahorro de almacenamiento, es un tema de rendimiento.

ClickHouse aplica LZ4 por defecto y funciona bien, pero afinar por columna da mejoras reales. La documentación oficial recomienda, en orden de importancia:

ZSTD como base. Ofrece las mejores tasas de compresión y ZSTD(1) es un buen valor por defecto para la mayoría de tipos. Subir por encima de ZSTD(3) rara vez compensa el coste de inserción.

Delta para secuencias de fechas y enteros. Funciona muy bien con secuencias monótonas o con diferencias pequeñas entre valores consecutivos. Timestamps e IDs autoincrementales son el caso canónico. Si el resultado de la primera derivada no es lo bastante pequeño, prueba DoubleDelta.

Delta mejora a ZSTD. Se combinan bien: CODEC(Delta, ZSTD(1)) suele batir a cualquiera de los dos por separado.

LZ4 si empata con ZSTD. Si obtienes compresión comparable, prefiere LZ4 porque descomprime más rápido y consume menos CPU. En la práctica ZSTD gana por bastante margen en la mayoría de casos.

T64 para rangos pequeños o datos dispersos. Efectivo cuando el rango de valores dentro de un bloque es reducido. Evítalo con números aleatorios.

Gorilla para floats de tipo sensor. Diseñado para lecturas de medidores con variaciones pequeñas.

En la práctica:

CREATE TABLE metricas
(
    timestamp  DateTime      CODEC(Delta, ZSTD(1)),
    sensor_id  UInt32        CODEC(Delta, ZSTD(1)),
    temperatura Float32      CODEC(Gorilla, ZSTD(1)),
    estado     LowCardinality(String),
    payload    String        CODEC(ZSTD(3))
)
ENGINE = MergeTree
ORDER BY (sensor_id, timestamp);

Mide antes y después

SELECT
    name,
    formatReadableSize(sum(data_compressed_bytes))   AS comprimido,
    formatReadableSize(sum(data_uncompressed_bytes)) AS sin_comprimir,
    round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE table = 'metricas'
GROUP BY name
ORDER BY sum(data_compressed_bytes) DESC;

Ordena por tamaño comprimido y ataca solo las dos o tres columnas más grandes. Optimizar el codec de una columna que ocupa 3 MB es tiempo perdido.

Nota sobre partes compactas: si ves ceros en compressed_size, es porque tus partes son de tipo compact en lugar de wide (ocurre con inserciones pequeñas). Lo controlan min_bytes_for_wide_part y min_rows_for_wide_part.

El tipo de dato también es compresión

Antes de tocar codecs, revisa tipos. Un UInt8 en lugar de un Int64 para un valor de 0 a 100 es una reducción de 8x gratuita. LowCardinality(String) para columnas con menos de ~10.000 valores distintos. DateTime en lugar de String para fechas. La documentación de ClickHouse muestra que solo optimizando tipos y clave de ordenación, un mismo dataset pasó de 50 GB a 25 GB comprimidos.

5. Índices de salto: úsalos con cuidado

Los data skipping indexes permiten saltar bloques filtrando por columnas que no están en el ORDER BY. Suenan como la solución universal y casi nunca lo son.

ALTER TABLE eventos ADD INDEX idx_pais pais TYPE set(100) GRANULARITY 4;
ALTER TABLE eventos MATERIALIZE INDEX idx_pais;

Tipos principales:

minmax guarda el mínimo y máximo por bloque. El más barato de aplicar. Ideal para columnas aproximadamente ordenadas y para filtros por rango.

set(N) guarda hasta N valores distintos por bloque. Bueno con baja cardinalidad dentro de cada bloque pero alta cardinalidad global.

bloom_filter para probar pertenencia a conjuntos grandes de valores. Funciona sobre arrays y maps.

text es el índice invertido real para búsqueda de texto completo. Es el recomendado hoy; los antiguos tokenbf_v1 y ngrambf_v1 están marcados como deprecados.

La advertencia importante

La documentación oficial es explícita en esto y merece repetirlo: el impulso natural de añadir un índice a una columna que consultas mucho suele ser incorrecto en ClickHouse.

Un índice de salto solo sirve si existe correlación fuerte entre la clave primaria y la columna indexada. Si los valores de esa columna están repartidos aleatoriamente por toda la tabla, cada bloque contendrá alguno y no se saltará nada. Habrás pagado el coste del índice (en inserción y en consulta) a cambio de cero beneficio.

Antes de añadir un índice de salto, prueba en este orden: cambiar el ORDER BY, añadir una projection, o crear una vista materializada. Los índices de salto son el último recurso, no el primero.

Un caso donde sí brillan: valores raros pero importantes. Un set index sobre error_code en una tabla de logs permite saltarse la enorme mayoría de bloques sin errores.

6. TTL y tiering: datos que se limpian solos

La retención automática es una de las mejores funcionalidades operativas de ClickHouse y una de las menos usadas.

Borrado automático

ALTER TABLE eventos MODIFY TTL fecha + INTERVAL 90 DAY;

Los datos con más de 90 días desaparecen en las fusiones de fondo. Sin cron, sin script, sin supervisión.

Agregación al envejecer

Más interesante: reducir la resolución de los datos antiguos en vez de borrarlos.

-- La clave de agrupación del TTL debe ser prefijo de la clave primaria
CREATE TABLE metricas_sensores
(
    timestamp   DateTime CODEC(Delta, ZSTD(1)),
    sensor_id   UInt32   CODEC(Delta, ZSTD(1)),
    temperatura Float32  CODEC(Gorilla, ZSTD(1)),
    estado      LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (sensor_id, toStartOfHour(timestamp), timestamp);

ALTER TABLE metricas_sensores MODIFY TTL
    timestamp + INTERVAL 7 DAY
        GROUP BY sensor_id, toStartOfHour(timestamp)
        SET temperatura = avg(temperatura);

Datos por segundo durante una semana, por hora después. Mantienes el histórico útil ocupando una fracción del espacio.

Ojo con este detalle, porque es donde falla todo el mundo la primera vez: ClickHouse exige que las columnas del GROUP BY del TTL sean un prefijo de la clave primaria. Si tu tabla está ordenada por (sensor_id, timestamp) y agrupas por toStartOfHour(timestamp), obtendrás el error TTL Expression GROUP BY key should be a prefix of primary key. La solución es incluir la expresión redondeada en el propio ORDER BY, como en el ejemplo de arriba.

Tiering a S3

El truco que hace viable la retención larga con coste bajo. Configura un disco S3 y una política de almacenamiento:

<!-- /etc/clickhouse-server/config.d/storage.xml -->
<clickhouse>
  <storage_configuration>
    <disks>
      <s3_frio>
        <type>s3</type>
        <endpoint>https://mi-bucket.s3.eu-west-1.amazonaws.com/clickhouse/</endpoint>
        <access_key_id>TU_ACCESS_KEY</access_key_id>
        <secret_access_key>TU_SECRET_KEY</secret_access_key>
      </s3_frio>
    </disks>
    <policies>
      <caliente_frio>
        <volumes>
          <caliente><disk>default</disk></caliente>
          <frio><disk>s3_frio</disk></frio>
        </volumes>
      </caliente_frio>
    </policies>
  </storage_configuration>
</clickhouse>

Y aplícala:

ALTER TABLE eventos MODIFY SETTING storage_policy = 'caliente_frio';

ALTER TABLE eventos MODIFY TTL
    fecha + INTERVAL 30 DAY TO VOLUME 'frio',
    fecha + INTERVAL 365 DAY DELETE;

Los últimos 30 días en SSD local (rápido), el resto en S3 (barato), y todo se borra al año. Para un proyecto pequeño esto es lo que convierte "guardo 30 días porque no puedo pagar más" en "guardo dos años por céntimos". Las consultas sobre datos fríos son más lentas, obviamente, pero siguen funcionando y son las que menos se hacen.

7. Inserción: el error que hunde a todo el mundo

El problema número uno en producción no son las consultas lentas, es Too many parts.

Cada INSERT crea un part nuevo en disco. Los parts se fusionan en segundo plano, pero si insertas más rápido de lo que se fusionan, se acumulan hasta que el servidor rechaza escrituras.

Regla: inserta en lotes

Mínimo 1.000 filas por insert; idealmente entre 10.000 y 100.000. Acumula en tu aplicación y descarga por tamaño o por tiempo (lo que ocurra antes).

Alternativa: inserts asíncronos

Si tu arquitectura no permite acumular fácilmente —por ejemplo, muchos procesos escribiendo eventos sueltos— deja que ClickHouse lo haga por ti:

SET async_insert = 1;
SET wait_for_async_insert = 1;

ClickHouse acumula en un buffer del servidor y descarga en lotes. Con wait_for_async_insert = 1 el cliente espera confirmación de escritura real (más seguro); con 0 recibe confirmación inmediata (más rápido, pero puedes perder datos si el servidor cae antes del flush).

Ajusta el comportamiento del buffer con async_insert_max_data_size y async_insert_busy_timeout_ms.

Diagnóstico

SELECT
    table,
    count()                                  AS num_parts,
    sum(rows)                                AS filas,
    formatReadableSize(sum(bytes_on_disk))   AS tamano
FROM system.parts
WHERE active AND database = 'creacosas'
GROUP BY table
ORDER BY num_parts DESC;

Si una tabla tiene cientos de parts activos, tu patrón de inserción es el problema.

Y no, OPTIMIZE TABLE ... FINAL no es la solución. Fuerza una fusión completa que es carísima en tablas grandes y no arregla la causa. La documentación oficial recomienda explícitamente evitarlo.

8. Diagnóstico: encontrar qué va mal

El query log es tu mejor herramienta

ClickHouse registra cada consulta con sus métricas:

SELECT
    query_duration_ms,
    formatReadableQuantity(read_rows)    AS filas_leidas,
    formatReadableSize(read_bytes)       AS bytes_leidos,
    formatReadableSize(memory_usage)     AS memoria,
    substring(query, 1, 120)             AS consulta
FROM system.query_log
WHERE type = 'QueryFinish'
  AND event_time > now() - INTERVAL 1 HOUR
ORDER BY query_duration_ms DESC
LIMIT 20;

Ordena por duración y ataca las peores. Compara read_rows con el total de la tabla: si están cerca, tu filtro no está usando el índice.

EXPLAIN

EXPLAIN indexes = 1
SELECT ... ;

Te muestra qué índices y projections se están usando y cuántos gránulos se descartan en cada paso.

PREWHERE

ClickHouse suele aplicar esta optimización automáticamente, pero puedes forzarla. PREWHERE lee primero las columnas del filtro, descarta filas, y solo entonces lee el resto de columnas:

SELECT url, payload_grande
FROM eventos
PREWHERE evento = 'error'      -- lee solo esta columna primero
WHERE duracion_ms > 5000;

Es especialmente útil cuando filtras por una columna pequeña y seleccionas columnas grandes.

Caché de consultas

Para dashboards con consultas repetidas e idénticas:

SELECT ... SETTINGS use_query_cache = 1;

Configura el TTL globalmente. No es una bala de plata —solo sirve si las consultas se repiten literalmente— pero en un dashboard que varias personas miran a la vez, ahorra bastante.

9. Consultas más inteligentes

Agregación aproximada cuando la exactitud no es crítica. uniq() usa HyperLogLog y es mucho más rápido y ligero en memoria que uniqExact(). Para un dashboard de "usuarios únicos", un error del 0,5% es irrelevante.

Diccionarios en lugar de JOINs. Si haces JOIN repetidamente contra una tabla de dimensiones pequeña, cárgala como diccionario en memoria:

CREATE DICTIONARY dic_paises (
    codigo String,
    nombre String
)
PRIMARY KEY codigo
SOURCE(CLICKHOUSE(TABLE 'paises'))
LAYOUT(COMPLEX_KEY_HASHED())
LIFETIME(3600);

SELECT dictGet('dic_paises', 'nombre', pais) AS pais_nombre, count()
FROM eventos GROUP BY pais;

Un dictGet es una consulta a un hash en memoria: órdenes de magnitud más rápido que un JOIN.

Funciones de combinador. -If, -Array, -Merge evitan subconsultas enteras:

SELECT
    fecha,
    countIf(evento = 'purchase')                      AS compras,
    countIf(evento = 'signup')                        AS registros,
    avgIf(duracion_ms, evento = 'pageview')           AS duracion_pageview
FROM eventos
GROUP BY fecha;

Una sola pasada sobre los datos en lugar de tres.

windowFunnel para embudos de conversión. Calcular un embudo en SQL estándar es doloroso. Aquí es una función:

SELECT
    nivel,
    count() AS usuarios
FROM (
    SELECT
        usuario_id,
        windowFunnel(3600)(
            timestamp,
            evento = 'pageview',
            evento = 'signup',
            evento = 'purchase'
        ) AS nivel
    FROM eventos
    WHERE fecha >= today() - 30
    GROUP BY usuario_id
)
GROUP BY nivel
ORDER BY nivel;

Mi checklist de optimización

El orden en que ataco un ClickHouse lento, por relación impacto/esfuerzo:

  1. Mirar system.query_log y encontrar las consultas realmente lentas. No optimices a ciegas.
  2. Comprobar si el ORDER BY sirve a los filtros más frecuentes. Si no, rehacer la tabla. Duele, y es lo que más resultado da.
  3. Revisar tipos de datos. LowCardinality, enteros del tamaño correcto, quitar Nullable innecesarios.
  4. Crear vistas materializadas para las agregaciones que alimentan dashboards.
  5. Verificar el patrón de inserción. Contar parts activos; batching si hay demasiados.
  6. Aplicar codecs a las dos o tres columnas más grandes.
  7. Añadir projections para el segundo patrón de acceso más común.
  8. Configurar TTL y tiering para que el coste de almacenamiento no crezca sin control.
  9. Índices de salto, solo si todo lo anterior no ha bastado y hay correlación real.

Los pasos 1 a 4 suelen resolver el 90% de los problemas. Si estás en el paso 9 con frecuencia, probablemente el problema esté en el diseño del esquema, no en la falta de índices.

Una reflexión final sobre optimizar

La mejor optimización que he hecho nunca en ClickHouse fue borrar una tabla.

Teníamos una tabla de eventos crudos que nadie consultaba directamente: todo el mundo usaba las vistas materializadas. Estaba ahí "por si acaso", ocupando la mayor parte del disco y ralentizando las fusiones. Le pusimos un TTL de 30 días, y todo —inserciones, consultas, backups— mejoró.

La tentación en ClickHouse es afinar. Hay tantas palancas —codecs, índices, projections, ajustes de merge— que es fácil pasar semanas ajustando parámetros para ganar un 15%. Casi siempre hay una decisión de diseño que da un 10x y está a la vista: una clave de ordenación mal elegida, una vista materializada que no existe, o datos que no deberías estar guardando.

Optimiza el diseño antes que los parámetros. Y mide siempre: la línea de "processed N rows" al final de cada consulta te dice la verdad, sin importar lo elegante que sea tu configuración.


Preguntas frecuentes

¿Cómo elijo el ORDER BY en ClickHouse?

Pon primero las columnas por las que filtras en casi todas las consultas, y a igualdad de uso, las de menor cardinalidad antes que las de mayor. Verifica el resultado con send_logs_level='trace' mirando cuántas marcas se seleccionan.

¿Cuál es la diferencia entre vista materializada y projection?

La vista materializada escribe en una tabla independiente y se dispara en la inserción, permitiendo transformaciones y TTL propios. La projection guarda una copia alternativa dentro de la misma tabla y ClickHouse la elige automáticamente, sin cambiar tus consultas.

¿Por qué mi ClickHouse da el error "Too many parts"?

Estás insertando en lotes demasiado pequeños o con demasiada frecuencia. Agrupa en lotes de al menos 1.000 filas o activa async_insert = 1.

¿Debo usar OPTIMIZE TABLE FINAL?

Como norma general, no. Fuerza fusiones muy costosas y no soluciona la causa del problema. La documentación oficial recomienda evitarlo.

¿Qué codec de compresión debo usar?

ZSTD(1) como base general, Delta combinado con ZSTD para timestamps y enteros secuenciales, y Gorilla para floats de sensores. Mide siempre con system.columns antes y después.

¿Cómo reduzco el coste de almacenamiento en ClickHouse?

TTL para borrar datos antiguos, TTL con GROUP BY para reducir la resolución del histórico, y políticas de almacenamiento por niveles moviendo los datos fríos a S3.

Posts Relacionados

Continúa explorando contenido similar que te puede interesar

Guía avanzada de ClickHouse: optimización, vistas materializadas y trucos reales