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, y eso es lo que merece la pena contar.
1. Un estado de agregado no es un número
El resumen diario guardaba los visitantes únicos y las sesiones como AggregateFunction(uniq, UUID), un sketch serializado, no un recuento. Esa es precisamente la idea: puedes fusionar sketches 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 la segunda es la que hace daño.
Las filas no se combinan hasta que se ejecuta una fusión en segundo plano. 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 mostrar 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 explorar la tabla 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 resumen diario. Para entonces ya no lo leía 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 mostraba literalmente el texto «NaN» donde debería haber una tasa de rebote y una duración media. Feo, evidentemente mal, notificado enseguida.
- 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: convertir en cero todo lo que no sea finito, 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 convierte un cero en un dato veraz y no en 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 una página siguiente con la que restar. 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 enormemente 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 funden en él.
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. En un caso poco común cuenta de menos, y para una métrica cuyo único propósito es no sobreestimar la atención, esa es la dirección correcta en la que equivocarse.
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 fallo grave 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 se queda un 8 % corto es peor que uno caído, porque nadie abre un ticket por 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 necesidad de cuenta, y el glosario dice exactamente qué cuenta cada uno.
Los tres fallos estaban detrás del producto Analytics, y ninguno avisó por sí solo, lo que es un argumento para comprobar un número con un segundo método en vez de fiarse de que parezca plausible.