ClickHouse es muy rápido y muy literal. Hace lo que le pediste, y varias de las cosas que le puedes pedir devuelven una respuesta que está mal de una forma de la que no te avisa nada: sin error, sin nulo, sin resultado vacío. Solo un número un poco demasiado pequeño, o un poco demasiado grande, o el texto «NaN» donde debería haber un porcentaje.
Estas son tres con las que nos topamos construyendo analítica sobre él. Cada una estaba en producción antes de que nadie se diera cuenta, que es la parte que vale la pena escribir.
1. Un estado de agregado no es un número
El cumulado diario guarda los visitantes únicos y las sesiones como AggregateFunction(uniq, UUID), un boceto serializado, no un recuento. Ese es todo el objetivo: puedes fusionar bocetos de varios días sin volver a las filas brutas.
La trampa es que la columna sigue pareciendo una columna. De ahí salen dos cosas, y es la segunda la que muerde.
Las filas no se pliegan hasta que corre una fusión de fondo. La misma clave puede existir varias veces, así que leer la columna y sumar lo que sale subestima, en silencio y de forma plausible. No hay error y no hay síntoma evidente; el número es simplemente más bajo que la verdad en una cantidad que cambia según cuándo fuera la última fusión. Hace falta uniqMerge y un GROUP BY, siempre.
Y un cliente SQL no puede dibujar el estado bruto en absoluto. El controlador JDBC de ClickHouse solo sabe deserializar estados uniq sobre tipos enteros nativos, así que cualquier SELECT * sobre un estado uniq con un UUID falla de plano:
Only native integer types are supported but we got: UUID
Lo que significa que hacer clic en la tabla en DataGrip o DBeaver (lo más normal que hace cualquiera mientras depura) lanza una excepción, y parece una conexión rota y no una decisión de modelado de datos que tomaste hace meses.
La solución es una vista que fusione los estados y exponga UInt64 normales, para que navegar funcione y los números estén bien:
CREATE VIEW stats_daily_v AS
SELECT
site_id, date, path,
sum(page_views) AS page_views,
uniqMerge(unique_visitors) AS unique_visitors,
uniqMerge(sessions) AS sessions
FROM stats_daily
GROUP BY site_id, date, path;
El código de la aplicación consultaba la tabla base con uniqMerge directamente. La vista existía para que un humano que abriera la base de datos viera la verdad en vez de una excepción.
En septiembre de 2026 eliminamos el rollup. Nunca lo leyó nada: el panel consulta las tablas en bruto, y el agregado se reconstruía en cada inserción sin un solo lector.
2. avgIf sobre ninguna fila devuelve NaN, no NULL
Esta llegó a producción y se quedó.
avgIf(duration_ms, is_bounce = 0)
Sobre un periodo sin filas coincidentes, ClickHouse devuelve el Float64 nan. No NULL, que sí habrías tratado, porque tratar los nulos es algo que todo el mundo se acuerda de hacer. nan.
Dos consecuencias, y caen sobre gente distinta:
- El panel dibujaba literalmente el texto «NaN» donde debería haber una tasa de rebote y una duración media. Feo, evidentemente mal, denunciado rápido.
- La API de consulta devolvía HTTP 500. JSON no puede codificar un double no finito en absoluto, así que la serialización lanzaba una excepción, y para exactamente los periodos que un cliente tiene más probabilidades de consultar: hoy, antes del primer visitante de la mañana, y cualquier rango sobre un sitio tranquilo.
La segunda es mucho peor que la primera y se encontró mucho más tarde. Un panel que dice «NaN» recibe un informe de fallo en una hora. Una API que devuelve 500 solo cuando la respuesta habría sido «no ha pasado nada» parece un punto de acceso inestable, y recibe un bucle de reintentos en vez de un informe de fallo.
La solución es una línea en la frontera de lectura: aplanar todo lo no finito a cero, que es la misma respuesta que da ya el camino del denominador cero, así que las dos coinciden:
var value = Convert.ToDouble(reader.GetValue(ordinal));
return double.IsFinite(value) ? value : 0;
Cero es honesto aquí porque el recuento de visitas que hay al lado también dice cero. Es el contexto lo que hace veraz a un cero en vez de una suposición.
3. Un ReplacingMergeTree que miente sin FINAL
El tiempo en la página no es el hueco entre dos páginas vistas. Un visitante que deja una pestaña abierta en segundo plano no está leyendo, y la última página de una visita no tiene página siguiente a la que restarse. Así que el rastreador informa del tiempo visible, y lo informa más de una vez: quien cambia de pestaña, vuelve y se va otra vez ha ocultado la página dos veces de verdad, y cada informe lleva el total acumulado en curso.
Sumarlos contaría el mismo minuto varias veces e informaría de un tiempo en la página descabelladamente sobreestimado. Así que la tabla es un ReplacingMergeTree con clave por visita y página, versionado por los milisegundos de interacción: el informe más largo gana y los parciales anteriores se pliegan dentro.
Lo que significa que cada lectura necesita FINAL, y una lectura que se olvide ve las filas parciales además de la final. El número no es basura; es plausible, solo que demasiado grande. Ese es el tipo de error que sobrevive a una revisión.
El coste honesto de esa clave, porque un diseño así siempre tiene uno: un visitante que vuelve a la misma página más tarde en la misma visita produce una fila y no dos, y la lectura más corta se descarta en vez de sumarse. Subcuenta en un caso poco común, que es la dirección correcta en la que equivocarse para una métrica cuyo propósito entero es dejar de sobreestimar la atención.
Qué tienen en común las tres
Ninguna lanzó un error en el momento en que se cometió el fallo. Dos produjeron números que parecían perfectamente razonables, y la tercera produjo un cierre inesperado en otro sitio completamente distinto, horas o semanas después.
La lección que nos llevamos de verdad: para cada agregado, escribe a qué se parecería una respuesta equivocada, y busca esa forma en vez de buscar una excepción. Un producto de analítica que va un 8 % bajo es peor que uno caído, porque nadie abre un ticket contra un 8 %.
Si quieres ver qué pinta tienen los números cuando están bien, hay una demostración en directo funcionando sobre una tienda real, sin cuenta, y el glosario dice exactamente qué cuenta cada uno.
Los tres fallos estaban detrás del producto Analytics, y ninguno se anunció, que es el argumento para comprobar un número con un segundo método en vez de fiarse de que parezca plausible.