Saltar al contenido principal
Recomendamos que los usuarios creen siempre su propio esquema para logs y trazas por las siguientes razones:
  • Elegir una clave primaria - Los esquemas predeterminados usan un ORDER BY optimizado para patrones de acceso específicos. Es poco probable que tus patrones de acceso coincidan con ellos.
  • Extraer estructura - Puede que quieras extraer columnas nuevas a partir de columnas existentes, por ejemplo, la columna Body. Esto puede hacerse con columnas materializadas (y vistas materializadas en casos más complejos). Esto requiere cambios en el esquema.
  • Optimizar mapas - Los esquemas predeterminados usan el tipo Map para almacenar atributos. Estas columnas permiten almacenar metadatos arbitrarios. Aunque es una capacidad esencial, ya que los metadatos de los eventos a menudo no se definen de antemano y, por lo tanto, no pueden almacenarse de otro modo en una base de datos de tipado fuerte como ClickHouse, acceder a las claves de mapa y a sus valores no es tan eficiente como acceder a una columna normal. Esto se soluciona modificando el esquema y asegurando que las claves de mapa a las que se accede con más frecuencia sean columnas de nivel superior; consulta “extracción de estructura con SQL”. Esto requiere un cambio en el esquema.
  • Simplificar el acceso a las claves de mapa - Acceder a claves en mapas requiere una sintaxis más verbosa. Esto puede mitigarse con alias. Consulta “Usar alias” para simplificar las consultas.
  • Índices secundarios - El esquema predeterminado usa índices secundarios para acelerar el acceso a Map y las consultas de texto. Normalmente no son necesarios y ocupan espacio adicional en disco. Pueden usarse, pero conviene probarlos para confirmar que realmente son necesarios. Consulta “Índices secundarios / de omisión de datos”.
  • Usar códecs - Puede que quieras personalizar los códecs de las columnas si se ajustan a los datos previstos y tienes pruebas de que mejoran la compresión.
Describimos en detalle cada uno de los casos de uso anteriores a continuación. Importante: Aunque se anima a los usuarios a ampliar y modificar su esquema para lograr una compresión óptima y un mejor rendimiento de las consultas, deben seguir, siempre que sea posible, la convención de nombres del esquema de OTel para las columnas principales. El plugin de ClickHouse para Grafana asume la existencia de algunas columnas básicas de OTel para ayudar a construir consultas, por ejemplo Timestamp y SeverityText. Las columnas necesarias para logs y trazas están documentadas aquí [1][2] y aquí, respectivamente. Puedes optar por cambiar estos nombres de columna, anulando los valores predeterminados en la configuración del plugin.

Extracción de estructura con SQL

Tanto al ingestar logs estructurados como no estructurados, los usuarios suelen necesitar la capacidad de:
  • Extraer columnas de blobs de texto. Consultarlas será más rápido que usar operaciones sobre cadenas en tiempo de consulta.
  • Extraer claves de mapas. El esquema predeterminado coloca atributos arbitrarios en columnas del tipo Map. Este tipo ofrece una capacidad sin esquema con la ventaja de que los usuarios no tienen que predefinir las columnas de los atributos al definir logs y traces; a menudo, esto es imposible al recopilar logs de Kubernetes y querer garantizar que se conserven las etiquetas del pod de Kubernetes para búsquedas posteriores. Acceder a las claves de un mapa y a sus valores es más lento que consultar columnas normales de ClickHouse. Por lo tanto, a menudo conviene extraer claves de mapas a columnas raíz de la tabla.
Considere las siguientes consultas: Supongamos que queremos contar qué rutas de URL reciben más solicitudes POST usando los logs estructurados. El blob JSON se almacena en la columna Body como un String. Además, también puede almacenarse en la columna LogAttributes como un Map(String, String) si el usuario ha habilitado el json_parser en el collector.
Suponiendo que LogAttributes esté disponible, la consulta para contar qué rutas URL del sitio reciben más solicitudes POST:
Observe el uso de la sintaxis de mapa aquí, por ejemplo, LogAttributes['request_path'], y de la función path para quitar los parámetros de consulta de la URL. Si el usuario no ha habilitado el análisis de JSON en el collector, LogAttributes estará vacío, lo que nos obliga a usar funciones JSON para extraer las columnas del Body de tipo String.
Use ClickHouse preferentemente para el análisisEn general, recomendamos procesar el JSON de los logs estructurados en ClickHouse. Tenemos la certeza de que ClickHouse ofrece la implementación de análisis de JSON más rápida. Sin embargo, entendemos que quizá desee enviar logs a otras sources y no quiera que esta lógica esté en SQL.
Ahora considere lo mismo para los logs no estructurados:
Una consulta similar para los logs no estructurados requiere usar expresiones regulares mediante la función extractAllGroupsVertical.
La mayor complejidad y el coste de las consultas para parsear logs no estructurados (observe la diferencia de rendimiento) es la razón por la que recomendamos a los usuarios usar siempre logs estructurados siempre que sea posible.
Considere DictionariesLa consulta anterior podría optimizarse para aprovechar diccionarios de expresiones regulares. Consulte Using Dictionaries para obtener más información.
Ambos casos de uso pueden resolverse con ClickHouse trasladando la lógica de la consulta anterior al momento de la inserción. A continuación, exploramos varios enfoques y destacamos cuándo conviene usar cada uno.
¿OTel o ClickHouse para el procesamiento?También puede realizar el procesamiento mediante procesadores y operadores del OTel collector, como se describe aquí. En la mayoría de los casos, verá que ClickHouse es significativamente más eficiente en el uso de recursos y más rápido que los procesadores del collector. La principal desventaja de realizar todo el procesamiento de eventos en SQL es el acoplamiento de su solución a ClickHouse. Por ejemplo, puede que desee enviar logs procesados a destinos alternativos desde el OTel collector, p. ej., S3.

Columnas materializadas

Las columnas materializadas ofrecen la forma más sencilla de extraer la estructura de otras columnas. Los valores de este tipo de columnas siempre se calculan en el momento de la inserción y no se pueden especificar en consultas INSERT.
SobrecargaLas columnas materializadas añaden una sobrecarga de almacenamiento, ya que los valores se extraen y se almacenan en nuevas columnas en disco en el momento de la inserción.
Las columnas materializadas admiten cualquier expresión de ClickHouse y pueden aprovechar cualquiera de las funciones analíticas para procesar cadenas (incluidas las expresiones regulares y las búsquedas) y URL, así como para realizar conversiones de tipo, extraer valores de JSON u operaciones matemáticas. Recomendamos las columnas materializadas para el procesamiento básico. Son especialmente útiles para extraer valores de mapas, promoverlos a columnas de nivel raíz y realizar conversiones de tipo. Suelen resultar más útiles en esquemas muy básicos o junto con vistas materializadas. Considere el siguiente esquema para logs, en el que el collector ha extraído el JSON a la columna LogAttributes:
El esquema equivalente para extraer datos con funciones JSON a partir de un Body de tipo String se puede encontrar aquí. Nuestras tres columnas materializadas extraen la página solicitada, el tipo de solicitud y el dominio de procedencia. Estas acceden a las claves del mapa y aplican funciones a sus valores. La consulta posterior es significativamente más rápida:
De forma predeterminada, las columnas materializadas no se devuelven en un SELECT *. Esto preserva la invariancia de que el resultado de un SELECT * siempre pueda volver a insertarse en la tabla mediante INSERT. Este comportamiento puede deshabilitarse configurando asterisk_include_materialized_columns=1 y puede habilitarse en Grafana (consulte Additional Settings -> Custom Settings en la configuración de la fuente de datos).

Vistas materializadas

Las vistas materializadas proporcionan una forma más potente de aplicar filtrado y transformaciones SQL a logs y trazas. Las vistas materializadas le permiten trasladar el coste de cálculo del tiempo de consulta al tiempo de inserción. Una vista materializada de ClickHouse es simplemente un trigger que ejecuta una consulta sobre bloques de datos a medida que se insertan en una tabla. Los resultados de esta consulta se insertan en una segunda tabla “de destino”.
Actualizaciones en tiempo realLas vistas materializadas en ClickHouse se actualizan en tiempo real a medida que los datos fluyen hacia la tabla en la que se basan, funcionando más como índices que se actualizan continuamente. En cambio, en otras bases de datos las vistas materializadas suelen ser instantáneas estáticas de una consulta que deben actualizarse (de forma similar a las vistas materializadas actualizables de ClickHouse).
En teoría, la consulta asociada a la vista materializada puede ser cualquier consulta, incluida una agregación, aunque existen limitaciones con los joins. Para las transformaciones y cargas de trabajo de filtrado necesarias para logs y trazas, puede asumir que cualquier sentencia SELECT es posible. Debe recordar que la consulta es solo un trigger que se ejecuta sobre las filas que se insertan en una tabla (la tabla de origen), y cuyos resultados se envían a una tabla nueva (la tabla de destino). Para asegurarnos de no persistir los datos dos veces (en las tablas de origen y de destino), podemos cambiar el motor de la tabla de origen para que sea un Null table engine, conservando el esquema original. Nuestros OTel collectors seguirán enviando datos a esta tabla. Por ejemplo, para logs, la tabla otel_logs pasa a ser:
El motor de tabla Null es una potente optimización; piensa en ella como /dev/null. Esta tabla no almacenará ningún dato, pero cualquier vista materializada adjunta seguirá ejecutándose sobre las filas insertadas antes de que se descarten. Considera la siguiente consulta. Esta transforma nuestras filas en un formato que queremos conservar, extrayendo todas las columnas de LogAttributes (suponemos que el collector lo ha establecido mediante el operador json_parser), estableciendo SeverityText y SeverityNumber (a partir de algunas condiciones simples y de la definición de estas columnas). En este caso, además, solo seleccionamos las columnas que sabemos que se rellenarán, ignorando columnas como TraceId, SpanId y TraceFlags.
También extraemos la columna Body indicada anteriormente, por si en el futuro se añaden atributos adicionales que nuestro SQL no extraiga. Esta columna debería comprimirse bien en ClickHouse y se accederá a ella con poca frecuencia, por lo que no afectará al rendimiento de las consultas. Por último, reducimos el Timestamp a un DateTime (para ahorrar espacio; consulte “Optimizing Types”) mediante un cast.
CondicionalesObserve el uso de condicionales más arriba para extraer SeverityText y SeverityNumber. Son muy útiles para formular condiciones complejas y comprobar si hay valores definidos en maps; aquí asumimos ingenuamente que todas las claves existen en LogAttributes. Recomendamos a los usuarios familiarizarse con ellas: serán sus aliadas al analizar logs, además de las funciones para manejar valores NULL!
Necesitamos una tabla para recibir estos resultados. La tabla de destino que se muestra a continuación corresponde a la consulta anterior:
Los tipos seleccionados aquí se basan en las optimizaciones descritas en “Optimización de tipos”.
Fíjese en cómo hemos cambiado drásticamente nuestro esquema. En la práctica, probablemente también tendrá columnas de trazas que querrá conservar, así como la columna ResourceAttributes (normalmente contiene metadatos de Kubernetes). Grafana puede aprovechar las columnas de trazas para ofrecer funcionalidad de vinculación entre logs y trazas; consulte “Uso de Grafana”.
A continuación, creamos una vista materializada otel_logs_mv que ejecuta la consulta SELECT anterior para la tabla otel_logs y envía los resultados a otel_logs_v2.
Esto se visualiza a continuación: Si ahora reiniciamos la configuración del collector usada en “Exportación a ClickHouse”, los datos aparecerán en otel_logs_v2 con el formato deseado. Fíjese en el uso de funciones tipadas de extracción de JSON.
A continuación se muestra una vista materializada equivalente, que extrae columnas de la columna Body mediante funciones JSON:

Cuidado con los tipos

Las vistas materializadas anteriores se basan en conversiones implícitas de tipos, especialmente al usar el mapa LogAttributes. ClickHouse suele convertir de forma transparente el valor extraído al tipo de la tabla de destino, lo que reduce la sintaxis necesaria. Sin embargo, recomendamos probar siempre las vistas usando la sentencia SELECT de la vista junto con una sentencia INSERT INTO sobre una tabla de destino con el mismo esquema. Esto debería confirmar que los tipos se manejan correctamente. Debe prestarse especial atención a los siguientes casos:
  • Si una clave no existe en un mapa, se devolverá una cadena vacía. En el caso de los valores numéricos, será necesario asignarlos a un valor adecuado. Esto puede lograrse con condicionales, p. ej., if(LogAttributes['status'] = ", 200, LogAttributes['status']), o con funciones de conversión si los valores predeterminados son aceptables, p. ej., toUInt8OrDefault(LogAttributes['status'] )
  • Algunos tipos no siempre se convertirán; por ejemplo, las representaciones en cadena de valores numéricos no se convertirán en valores enum.
  • Las funciones de extracción de JSON devuelven valores predeterminados para su tipo si no encuentran un valor. Asegúrate de que esos valores tengan sentido.
Evita NullableEvita usar Nullable en ClickHouse para datos de observabilidad. Rara vez es necesario en logs y trazas poder distinguir entre vacío y nulo. Esta funcionalidad añade una sobrecarga de almacenamiento adicional y afectará negativamente al rendimiento de las consultas. Consulta aquí para obtener más detalles.

Elegir una clave primaria (de ordenación)

Una vez que hayas extraído las columnas que necesitas, puedes empezar a optimizar tu clave primaria/de ordenación. Puedes aplicar algunas reglas sencillas para ayudarte a elegir una clave de ordenación. A veces pueden entrar en conflicto entre sí, así que considérelas en este orden. A partir de este proceso, puedes identificar varias claves; normalmente, 4 o 5 suelen ser suficientes:
  1. Selecciona columnas que se ajusten a tus filtros habituales y patrones de acceso. Si normalmente empiezas las investigaciones de observabilidad filtrando por una columna específica, por ejemplo, el nombre del pod, esa columna se usará con frecuencia en las cláusulas WHERE. Prioriza incluir estas columnas en tu clave frente a aquellas que se usan con menos frecuencia.
  2. Da preferencia a las columnas que, al filtrar, ayuden a excluir un gran porcentaje del total de filas, reduciendo así la cantidad de datos que hay que leer. Los nombres de servicio y los códigos de estado suelen ser buenos candidatos; en este último caso, solo si filtras por valores que excluyen la mayoría de las filas. Por ejemplo, filtrar por códigos 200 coincidirá con la mayoría de las filas en la mayoría de los sistemas, mientras que los errores 500 corresponderán a un subconjunto pequeño.
  3. Da preferencia a las columnas que probablemente estén muy correlacionadas con otras columnas de la tabla. Esto ayudará a garantizar que esos valores también se almacenen de forma contigua, mejorando la compresión.
  4. Las operaciones GROUP BY y ORDER BY sobre columnas de la clave de ordenación pueden ser más eficientes en el uso de memoria.

Una vez identificado el subconjunto de columnas para la clave de ordenación, estas deben declararse en un orden específico. Este orden puede influir significativamente tanto en la eficiencia del filtrado sobre columnas secundarias de la clave en las consultas como en la relación de compresión de los archivos de datos de la tabla. En general, lo mejor es ordenar las claves en orden ascendente de cardinalidad. Esto debe equilibrarse con el hecho de que filtrar por columnas que aparecen más tarde en la clave de ordenación será menos eficiente que filtrar por las que aparecen antes en la tupla. Equilibra estos comportamientos y ten en cuenta tus patrones de acceso. Y, sobre todo, prueba variantes. Para entender mejor las claves de ordenación y cómo optimizarlas, recomendamos este artículo.
Primero, la estructuraRecomendamos decidir las claves de ordenación una vez que hayas estructurado tus logs. No uses claves en mapas de atributos para la clave de ordenación ni expresiones de extracción de JSON. Asegúrate de que las claves de ordenación estén como columnas de nivel superior en tu tabla.

Uso de mapas

En ejemplos anteriores se muestra el uso de la sintaxis map['key'] para acceder a valores en las columnas Map(String, String). Además de usar la notación de mapa para acceder a las claves anidadas, ClickHouse ofrece funciones de map especializadas para filtrar o seleccionar estas columnas. Por ejemplo, la siguiente consulta identifica todas las claves únicas disponibles en la columna LogAttributes mediante la función mapKeys, seguida de la función groupArrayDistinctArray (un combinador).
Evita los puntosNo recomendamos usar puntos en los nombres de columnas de tipo Map y es posible que dejemos de admitir su uso. Usa un _.

Uso de alias

Consultar tipos Map es más lento que consultar columnas normales; consulta “Aceleración de consultas”. Además, la sintaxis es más compleja y puede resultar engorrosa de escribir. Para resolver este último problema, recomendamos usar columnas Alias. Las columnas ALIAS se calculan en tiempo de consulta y no se almacenan en la tabla. Por lo tanto, es imposible hacer un INSERT de un valor en una columna de este tipo. Con alias, podemos hacer referencia a claves de Map y simplificar la sintaxis, exponiendo de forma transparente las entradas de Map como si fueran una columna normal. Considera el siguiente ejemplo:
Tenemos varias columnas materializadas y una columna ALIAS, RemoteAddr, que accede al mapa LogAttributes. Ahora podemos consultar los valores de LogAttributes['remote_addr'] a través de esta columna, lo que simplifica nuestra consulta; es decir:
Además, agregar ALIAS es muy sencillo con el comando ALTER TABLE. Estas columnas están disponibles de inmediato; por ejemplo.
Alias excluido de forma predeterminadaDe forma predeterminada, SELECT * excluye las columnas ALIAS. Este comportamiento se puede desactivar estableciendo asterisk_include_alias_columns=1.

Optimización de tipos

Las prácticas recomendadas generales de ClickHouse para optimizar los tipos también se aplican a este caso de uso de ClickHouse.

Uso de codecs

Además de las optimizaciones de tipos, puedes seguir las mejores prácticas generales para codecs al intentar optimizar la compresión en los esquemas de observabilidad de ClickHouse. En general, el codec ZSTD suele ser muy adecuado para conjuntos de datos de logging y trazas. Aumentar el valor de compresión respecto al valor predeterminado de 1 puede mejorar la compresión. Sin embargo, conviene probarlo, ya que los valores más altos implican una mayor sobrecarga de CPU en el momento de la inserción. Normalmente, observamos poca mejora al aumentar este valor. Además, aunque los timestamps se benefician de la codificación delta en términos de compresión, se ha demostrado que pueden degradar el rendimiento de las consultas lentas si esta columna se utiliza en la clave primaria/de ordenación. Recomendamos a los usuarios evaluar el equilibrio entre compresión y rendimiento de las consultas en cada caso.

Uso de diccionarios

Diccionarios son una funcionalidad clave de ClickHouse, ya que proporcionan una representación clave-valor en memoria de datos procedentes de varias fuentes internas y externas, optimizada para consultas de búsqueda de latencia ultrabaja. Esto resulta útil en distintos escenarios: desde enriquecer sobre la marcha los datos ingeridos sin ralentizar el proceso de ingestión hasta mejorar el rendimiento general de las consultas, con un beneficio especial para los JOINs. Aunque en los casos de uso de observabilidad rara vez se necesitan joins, los diccionarios siguen siendo útiles para tareas de enriquecimiento, tanto en el momento de la inserción como en el de la consulta. A continuación, proporcionamos ejemplos de ambos casos.
Aceleración de joinsLos usuarios interesados en acelerar joins con diccionarios pueden encontrar más detalles aquí.

Tiempo de inserción vs. tiempo de consulta

Los diccionarios pueden usarse para enriquecer conjuntos de datos en el momento de la consulta o de la inserción. Cada uno de estos enfoques tiene sus propias ventajas e inconvenientes. En resumen:
  • Tiempo de inserción - Suele ser la opción adecuada si el valor de enriquecimiento no cambia y existe en una fuente externa que puede usarse para rellenar el diccionario. En este caso, enriquecer la fila en el momento de la inserción evita tener que hacer la búsqueda en el diccionario en tiempo de consulta. Esto tiene un coste en el rendimiento de inserción, además de una sobrecarga adicional de almacenamiento, ya que los valores enriquecidos se almacenarán como columnas.
  • Tiempo de consulta - Si los valores de un diccionario cambian con frecuencia, las búsquedas en tiempo de consulta suelen ser más adecuadas. Esto evita tener que actualizar columnas (y reescribir datos) si cambian los valores asignados. Esta flexibilidad tiene como contrapartida el coste de hacer la búsqueda en tiempo de consulta. Este coste suele ser apreciable si la búsqueda se requiere para muchas filas; por ejemplo, al usar una búsqueda en el diccionario en una cláusula de filtro. Para el enriquecimiento de resultados, es decir, en el SELECT, esta sobrecarga normalmente no suele ser apreciable.
Recomendamos que los usuarios se familiaricen con los conceptos básicos de los diccionarios. Los diccionarios proporcionan una tabla de búsqueda en memoria de la que pueden recuperarse valores mediante funciones especializadas. Para ver ejemplos sencillos de enriquecimiento, consulte la guía sobre Diccionarios aquí. A continuación, nos centramos en tareas habituales de enriquecimiento de observabilidad.

Uso de diccionarios IP

Enriquecer geográficamente logs y traces con valores de latitud y longitud a partir de direcciones IP es un requisito habitual en observabilidad. Podemos lograrlo usando el Diccionario estructurado ip_trie. Usamos el dataset público de DB-IP a nivel de ciudad, proporcionado por DB-IP.com bajo los términos de la licencia CC BY 4.0. En el archivo readme, podemos ver que los datos están estructurados de la siguiente manera:
Dada esta estructura, empecemos por echar un vistazo a los datos con la función de tabla url():
Para simplificarnos la vida, usemos el motor de tabla URL() para crear una tabla de ClickHouse con nuestros nombres de campo y confirmar el número total de filas:
Como nuestro diccionario ip_trie requiere que los rangos de direcciones IP se expresen en notación CIDR, tendremos que transformar ip_range_start y ip_range_end. La notación CIDR de cada rango puede calcularse de forma concisa con la siguiente consulta:
En la consulta anterior ocurren muchas cosas. Para quienes tengan interés, lean esta excelente explicación. Si no, basta con saber que lo anterior calcula un CIDR para un rango de IP.
Para nuestros fines, solo necesitaremos el rango de IP, el código de país y las coordenadas, así que creemos una tabla nueva e insertemos nuestros datos de Geo IP:
Para realizar búsquedas de IP de baja latencia en ClickHouse, usaremos diccionarios para almacenar en memoria la asignación de clave -> atributos de nuestros datos de Geo IP. ClickHouse proporciona una ip_trie estructura de diccionario para asignar nuestros prefijos de red (bloques CIDR) a coordenadas y códigos de país. La siguiente consulta especifica un diccionario con este diseño y usa la tabla anterior como origen.
Podemos seleccionar filas del diccionario y confirmar que este conjunto de datos está disponible para consultas:
Actualización periódicaLos diccionarios de ClickHouse se actualizan periódicamente en función de los datos de la tabla subyacente y de la cláusula lifetime utilizada anteriormente. Para actualizar nuestro diccionario Geo IP para que refleje los cambios más recientes en el conjunto de datos DB-IP, solo tenemos que volver a insertar datos de la tabla remota geoip_url en nuestra tabla geoip, con las transformaciones aplicadas.
Ahora que tenemos los datos de Geo IP cargados en nuestro diccionario ip_trie (que, convenientemente, también se llama ip_trie), podemos usarlo para la geolocalización por IP. Esto puede hacerse con la función dictGet(), de la siguiente manera:
Fíjese en la rapidez de recuperación aquí. Esto nos permite enriquecer los logs. En este caso, elegimos realizar el enriquecimiento en tiempo de consulta. Volviendo a nuestro conjunto de datos original de logs, podemos usar lo anterior para agrupar nuestros logs por país. A continuación, se asume que usamos el esquema resultante de nuestra vista materializada anterior, que tiene una columna RemoteAddress extraída.
Dado que la asignación entre una IP y una ubicación geográfica puede cambiar, es probable que los usuarios quieran saber desde dónde se originó la solicitud en el momento en que se realizó, no cuál es la ubicación geográfica actual de esa misma dirección. Por este motivo, aquí suele preferirse el enriquecimiento en el momento de la indexación. Esto puede hacerse mediante columnas materializadas, como se muestra a continuación, o en el select de una vista materializada:
Actualización periódicaEs probable que los usuarios quieran que el diccionario de enriquecimiento de IP se actualice periódicamente a partir de datos nuevos. Esto puede lograrse mediante la cláusula LIFETIME del diccionario, que hace que se recargue periódicamente desde la tabla subyacente. Para actualizar la tabla subyacente, consulta “vistas materializadas actualizables”.
Los países y coordenadas anteriores ofrecen posibilidades de visualización que van más allá de agrupar y filtrar por país. Como inspiración, consulta “Visualizar datos geográficos”.

Uso de diccionarios de expresiones regulares (análisis de user agent)

El análisis de cadenas user agent es un problema clásico de expresiones regulares y un requisito habitual en conjuntos de datos basados en logs y traces. ClickHouse proporciona un análisis eficiente de user agents mediante diccionarios de árbol de expresiones regulares. Los diccionarios de árbol de expresiones regulares se definen en la versión open-source de ClickHouse mediante el tipo de dictionary source YAMLRegExpTree, que proporciona la path a un archivo YAML que contiene el árbol de expresiones regulares. Si desea proporcionar su propio diccionario de expresiones regulares, puede consultar aquí los detalles de la structure requerida. A continuación, nos centramos en el análisis de user agent con uap-core y cargamos nuestro diccionario en el formato CSV admitido. Este enfoque es compatible con OSS y ClickHouse Cloud.
En los ejemplos siguientes, usamos snapshots de las expresiones regulares más recientes de uap-core para el análisis de user agent de junio de 2024. El archivo más reciente, que se actualiza ocasionalmente, puede encontrarse aquí. Puede seguir los pasos aquí para cargarlo en el archivo CSV utilizado a continuación.
Cree las siguientes tablas Memory. En ellas se almacenan nuestras expresiones regulares para analizar dispositivos, navegadores y sistemas operativos.
Estas tablas pueden poblarse con los siguientes archivos CSV alojados públicamente mediante la función de tabla url:
Con nuestras tablas Memory ya pobladas, podemos cargar nuestros diccionarios de expresiones regulares. Ten en cuenta que debemos especificar como columnas los valores de clave; estos serán los atributos que podremos extraer del user agent.
Con estos diccionarios cargados, podemos usar un user-agent de ejemplo y poner a prueba nuestras nuevas capacidades de extracción con diccionarios:
Dado que las reglas relacionadas con los user agents rara vez cambian, y que el diccionario solo necesita actualizarse cuando aparecen nuevos navegadores, sistemas operativos y dispositivos, tiene sentido realizar esta extracción en el momento de la inserción. Podemos realizar este trabajo mediante una columna materializada o una vista materializada. A continuación, modificamos la vista materializada utilizada anteriormente:
Para ello, debemos modificar el esquema de la tabla de destino otel_logs_v2:
Después de reiniciar el collector e ingestar logs estructurados, siguiendo los pasos documentados anteriormente, podemos consultar las columnas Device, Browser y Os que acabamos de extraer.
Tuples para estructuras complejasFíjate en el uso de Tuples para estas columnas de user agent. Se recomienda usar Tuples para estructuras complejas en las que la jerarquía se conoce de antemano. Las subcolumnas ofrecen el mismo rendimiento que las columnas normales (a diferencia de las claves de Map), a la vez que permiten tipos heterogéneos.

Más información

Para obtener más ejemplos y detalles sobre los diccionarios, recomendamos los siguientes artículos:

Acelerar las consultas

ClickHouse admite varias técnicas para acelerar el rendimiento de las consultas. Lo siguiente debe considerarse solo después de elegir una clave primaria/de ordenación adecuada para optimizar los patrones de acceso más habituales y maximizar la compresión. Por lo general, esto suele tener el mayor impacto en el rendimiento con el menor esfuerzo.

Uso de vistas materializadas (incrementales) para agregaciones

En secciones anteriores, exploramos el uso de vistas materializadas para la transformación y el filtrado de datos. Sin embargo, las vistas materializadas también pueden usarse para precalcular agregaciones en el momento de la inserción y almacenar el resultado. Este resultado puede actualizarse con los resultados de inserciones posteriores, lo que permite, en la práctica, precalcular una agregación en el momento de la inserción. La idea principal aquí es que los resultados suelen ser una representación más pequeña de los datos originales (un resumen parcial en el caso de las agregaciones). Al combinar esto con una consulta más simple para leer los resultados de la tabla de destino, los tiempos de consulta serán menores que si el mismo cálculo se realizara sobre los datos originales. Considere la siguiente consulta, en la que calculamos el tráfico total por hora usando nuestros logs estructurados:
Podemos imaginar que este podría ser un gráfico de líneas habitual que los usuarios crean con Grafana. Es cierto que esta consulta es muy rápida: el conjunto de datos solo tiene 10 millones de filas, ¡y ClickHouse es rápido! Sin embargo, si lo escalamos a miles de millones y billones de filas, idealmente nos gustaría mantener este rendimiento en las consultas.
Esta consulta sería 10 veces más rápida si usáramos la tabla otel_logs_v2, que es el resultado de nuestra vista materializada anterior, la cual extrae la clave size del mapa LogAttributes. Aquí usamos los datos sin procesar solo con fines ilustrativos y recomendamos usar la vista anterior si esta es una consulta habitual.
Necesitamos una tabla que reciba los resultados si queremos calcular esto en el momento de la inserción mediante una vista materializada. Esta tabla solo debe conservar 1 fila por hora. Si se recibe una actualización para una hora existente, las demás columnas deben fusionarse en la fila ya existente de esa hora. Para que se produzca esta fusión de estados incrementales, deben almacenarse estados parciales para las demás columnas. Esto requiere un tipo especial de motor en ClickHouse: SummingMergeTree. Este reemplaza todas las filas con la misma clave de ordenación por una sola fila que contiene los valores sumados de las columnas numéricas. La siguiente tabla fusionará todas las filas con la misma fecha, sumando todas las columnas numéricas.
Para ilustrar nuestra vista materializada, supongamos que la tabla bytes_per_hour está vacía y todavía no ha recibido ningún dato. Nuestra vista materializada ejecuta el SELECT anterior sobre los datos insertados en otel_logs (esto se hará sobre bloques del tamaño configurado), y los resultados se envían a bytes_per_hour. La sintaxis se muestra a continuación:
La cláusula TO es clave aquí, ya que indica adónde se enviarán los resultados, es decir, a bytes_per_hour. Si reiniciamos nuestro OTel collector y reenviamos los logs, la tabla bytes_per_hour se irá rellenando de forma incremental con el resultado de la consulta anterior. Al terminar, podremos comprobar el tamaño de la tabla bytes_per_hour: deberíamos tener 1 fila por hora:
Hemos reducido considerablemente el número de filas, de 10m (en otel_logs) a 113, al almacenar el resultado de nuestra consulta. La clave aquí es que, si se insertan nuevos logs en la tabla otel_logs, se enviarán nuevos valores a bytes_per_hour para su hora correspondiente, donde se fusionarán automáticamente de forma asíncrona en segundo plano. Al mantener solo una fila por hora, bytes_per_hour seguirá siendo siempre pequeña y estará actualizada. Como la fusión de filas es asíncrona, puede haber más de una fila por hora cuando un usuario haga una consulta. Para asegurarnos de que las filas pendientes se fusionen en tiempo de consulta, tenemos dos opciones:
  • Usar el modificador FINAL en el nombre de la tabla (que es lo que hicimos para la consulta de recuento anterior).
  • Agregar por la clave de ordenación utilizada en nuestra tabla final, es decir, Timestamp, y sumar las métricas.
Normalmente, la segunda opción es más eficiente y flexible (la tabla puede usarse para otras cosas), pero la primera puede ser más sencilla para algunas consultas. A continuación mostramos ambas:
Esto ha acelerado nuestra consulta de 0.6s a 0.008s: ¡más de 75 veces más rápido!
Este ahorro de tiempo puede ser aún mayor en conjuntos de datos más grandes y con consultas más complejas. Consulta ejemplos aquí.

Un ejemplo más complejo

El ejemplo anterior agrega un recuento simple por hora con SummingMergeTree. Las estadísticas que van más allá de las sumas simples requieren un motor de tabla de destino distinto: AggregatingMergeTree. Supongamos que queremos calcular el número de direcciones IP únicas (o de usuarios únicos) por día. La consulta para esto:
Para persistir un recuento de cardinalidad para actualizaciones incrementales, se requiere AggregatingMergeTree.
Para garantizar que ClickHouse sepa que se almacenarán estados de agregación, definimos la columna UniqueUsers con el tipo AggregateFunction, especificando la función de la que proceden los estados parciales (uniq) y el tipo de la columna de origen (IPv4). Al igual que en SummingMergeTree, las filas con el mismo valor de la clave ORDER BY se combinarán (Hour en el ejemplo anterior). La vista materializada asociada utiliza la consulta anterior:
Observa cómo añadimos el sufijo State al final de nuestras funciones de agregación. Esto garantiza que se devuelva el estado de agregación de la función en lugar del resultado final. Este incluirá información adicional que permitirá combinar este estado parcial con otros estados. Una vez recargados los datos mediante el reinicio del collector, podemos confirmar que hay 113 filas disponibles en la tabla unique_visitors_per_hour.
Nuestra consulta final debe usar el sufijo Merge en nuestras funciones (ya que las columnas almacenan estados de agregación parciales):
Ten en cuenta que aquí usamos GROUP BY en lugar de FINAL.

Uso de vistas materializadas (incrementales) para búsquedas rápidas

Debe tener en cuenta los patrones de acceso al elegir la clave de ordenación de ClickHouse, junto con las columnas que se usan con frecuencia en las cláusulas de filtro y agregación. Esto puede resultar restrictivo en casos de uso de observabilidad, donde los usuarios tienen patrones de acceso más diversos que no pueden encapsularse en un único conjunto de columnas. Esto se ilustra mejor con un ejemplo integrado en los esquemas predeterminados de OTel. Considere el esquema predeterminado para las trazas:
Este esquema está optimizado para filtrar por ServiceName, SpanName y Timestamp. En tracing, los usuarios también necesitan poder hacer consultas por un TraceId específico y recuperar los spans asociados a la traza correspondiente. Aunque esto está presente en la clave de ordenación, su posición al final significa que el filtrado no será tan eficiente y probablemente hará necesario escanear cantidades significativas de datos al recuperar una sola traza. El OTel collector también instala una vista materializada y la tabla asociada para abordar este problema. La tabla y la vista se muestran a continuación:
La vista garantiza de forma efectiva que la tabla otel_traces_trace_id_ts tenga la marca temporal mínima y máxima de la traza. Esta tabla, ordenada por TraceId, permite recuperar estas marcas temporales de manera eficiente. Estos rangos de marcas temporales pueden, a su vez, utilizarse al consultar la tabla principal otel_traces. Más concretamente, al recuperar una traza por su id, Grafana utiliza la siguiente consulta:
La CTE de este ejemplo identifica la marca de tiempo mínima y máxima para el ID de la traza ae9226c78d1d360601e6383928e4d22d, antes de usarla para filtrar la tabla principal otel_traces y recuperar sus spans asociados. Este mismo enfoque puede aplicarse a patrones de acceso similares. Analizamos un ejemplo parecido en Modelado de datos aquí.

Uso de proyecciones

Las proyecciones de ClickHouse permiten especificar varias cláusulas ORDER BY para una tabla. En secciones anteriores, exploramos cómo pueden usarse las vistas materializadas en ClickHouse para precalcular agregaciones, transformar filas y optimizar las consultas de observabilidad para distintos patrones de acceso. Mostramos un ejemplo en el que la vista materializada envía filas a una tabla de destino con una clave de ordenación distinta de la tabla original que recibe las inserciones, con el fin de optimizar las búsquedas por ID de la traza. Las proyecciones pueden usarse para resolver el mismo problema, ya que permiten al usuario optimizar consultas sobre una columna que no forma parte de la clave primaria. En teoría, esta capacidad puede usarse para proporcionar varias claves de ordenación para una tabla, con una desventaja importante: la duplicación de datos. En concreto, los datos deberán escribirse en el orden de la clave primaria principal, además del orden especificado para cada proyección. Esto ralentizará las inserciones y consumirá más espacio en disco.
Proyecciones frente a vistas materializadasLas proyecciones ofrecen muchas de las mismas capacidades que las vistas materializadas, pero deben usarse con moderación, y a menudo se prefiere estas últimas. Debe comprender sus inconvenientes y cuándo resultan adecuadas. Por ejemplo, aunque las proyecciones pueden usarse para precalcular agregaciones, recomendamos a los usuarios utilizar vistas materializadas para ello.
Considere la siguiente consulta, que filtra nuestra tabla otel_logs_v2 por códigos de error 500. Es probable que este sea un patrón de acceso habitual en logging, ya que los usuarios suelen querer filtrar por códigos de error:
Usa Null para medir el rendimientoAquí no mostramos los resultados con FORMAT Null. Esto hace que se lean todos los resultados, pero no se devuelvan, evitando así que la consulta termine antes de tiempo debido a un LIMIT. Esto es solo para mostrar el tiempo que se tarda en escanear las 10m filas completas.
La consulta anterior requiere un escaneo lineal con la clave de ordenación elegida (ServiceName, Timestamp). Aunque podríamos añadir Status al final de la clave de ordenación para mejorar el rendimiento de la consulta anterior, también podemos añadir una proyección.
Ten en cuenta que primero tenemos que crear la proyección y luego materializarla. Este último comando hace que los datos se almacenen dos veces en disco en dos órdenes diferentes. La proyección también puede definirse al crear los datos, como se muestra a continuación, y se mantendrá automáticamente a medida que se insertan los datos.
Es importante destacar que, si la proyección se crea mediante un ALTER, su creación es asíncrona cuando se emite el comando MATERIALIZE PROJECTION. Puede comprobar el progreso de esta operación con la siguiente consulta y esperar a que is_done=1.
Si repetimos la consulta anterior, podemos ver que el rendimiento ha mejorado significativamente a costa de un mayor almacenamiento (consulta “Medir el tamaño y la compresión de las tablas” para saber cómo medirlo).
En el ejemplo anterior, especificamos en la proyección las columnas utilizadas en la consulta anterior. Esto significa que solo esas columnas se almacenarán en disco como parte de la proyección, ordenadas por Status. Si, en cambio, usáramos SELECT * aquí, se almacenarían todas las columnas. Aunque esto permitiría que más consultas (que usen cualquier subconjunto de columnas) se beneficiaran de la proyección, también implicaría un almacenamiento adicional. Para medir el espacio en disco y la compresión, consulte “Medir el tamaño y la compresión de las tablas”.

Índices secundarios/de omisión de datos

Por muy bien ajustada que esté la clave primaria en ClickHouse, algunas consultas requerirán inevitablemente un escaneo completo de la tabla. Aunque esto puede mitigarse con vistas materializadas (y proyecciones para algunas consultas), estas requieren mantenimiento adicional y que los usuarios sepan que están disponibles para asegurarse de aprovecharlas. Mientras que las bases de datos relacionales tradicionales resuelven esto con índices secundarios, estos son ineficaces en bases de datos orientadas a columnas como ClickHouse. En su lugar, ClickHouse utiliza índices de “omisión”, que pueden mejorar significativamente el rendimiento de las consultas al permitir que la base de datos se salte grandes fragmentos de datos en los que no hay valores coincidentes. Los esquemas OTel predeterminados usan índices secundarios para intentar acelerar el acceso a campos de tipo Map. Aunque, por lo general, consideramos que son poco eficaces y no recomendamos copiarlos en su esquema personalizado, los índices de omisión pueden seguir siendo útiles. Debe leer y comprender la guía sobre índices secundarios antes de intentar aplicarlos. En general, son eficaces cuando existe una fuerte correlación entre la clave primaria y la columna/expresión de destino que no forma parte de ella, y los usuarios buscan valores poco frecuentes, es decir, aquellos que no aparecen en muchos gránulos. ClickHouse proporciona un índice de texto especializado para la búsqueda de texto completo. Este índice crea un índice invertido sobre datos de texto tokenizados, lo que permite realizar consultas de búsqueda rápidas basadas en tokens. Los índices de texto están disponibles a partir de la versión 26.2 de ClickHouse. Se pueden definir en los siguientes tipos de columna de tablas MergeTree: String, FixedString, Array(String), Array(FixedString) y columnas Map (mediante las funciones de map mapKeys y mapValues) en tablas MergeTree. Un índice de texto requiere un argumento tokenizer en su definición. De forma opcional, se puede especificar una función de preprocesamiento para transformar la cadena de entrada antes de la tokenización. Las funciones recomendadas para buscar en el índice son: hasAnyTokens y hasAllTokens. Algunas funciones tradicionales de búsqueda de cadenas también se optimizan automáticamente cuando hay un índice de texto. Consulta la documentación para obtener más detalles y ver las funciones compatibles aquí y aquí. En los ejemplos siguientes, usamos un conjunto de datos de logs estructurados.
También podemos usar hasAnyTokens sin un índice de texto, pero la consulta hará un escaneo completo lento de la columna Body:

Añadir un índice de texto

Se puede añadir un índice de texto en la columna Body durante la creación de la tabla:
o agregarse más adelante con ALTER TABLE:
Si ejecutamos de nuevo la misma consulta SELECT, hará una búsqueda en el índice de texto. El volumen de datos accedidos se reduce de gigabytes a megabytes y el rendimiento mejora unas 45 veces.

Uso de un preprocesador

En este conjunto de datos, la columna Body contiene una cadena en formato JSON con varios pares clave-valor (por ejemplo, msg, id, ctx, attr, etc.). Supongamos que solo nos interesa buscar en el campo msg. En lugar de indexar toda la cadena JSON, podemos definir un preprocesador para extraer únicamente el valor de msg antes de la tokenización. Por ejemplo:
En este ejemplo, el preprocesador:
  • reduce la cantidad de texto que se tokeniza e indexa,
  • disminuye el tamaño del índice,
  • reduce la probabilidad de falsos positivos, y
  • mejora el rendimiento de las consultas.
En comparación con el índice sin preprocesamiento, el rendimiento mejora aproximadamente 2x. El uso de un preprocesador también reduce el tamaño del índice de gigabytes a unos pocos cientos de kilobytes, aproximadamente el 0,01 % del tamaño original
**Otros índices para la búsqueda de texto Encontrará más detalles sobre los índices de salto secundarios aquí.

Extracción de datos de mapas

El tipo Map es habitual en los esquemas de OTel. Este tipo requiere que los valores y las claves tengan el mismo tipo, lo que resulta suficiente para metadatos como las etiquetas de Kubernetes. Ten en cuenta que, al consultar una subclave de un tipo Map, se carga la columna padre completa. Si el mapa tiene muchas claves, esto puede suponer una penalización significativa en la consulta, ya que es necesario leer más datos del disco que si la clave existiera como una columna. Si consultas con frecuencia una clave concreta, considera moverla a su propia columna dedicada en el nivel raíz. Normalmente, esta es una tarea que surge como respuesta a patrones de acceso comunes después del despliegue, y puede ser difícil de prever antes de pasar a producción. Consulta “Gestionar cambios de esquema” para saber cómo modificar tu esquema después del despliegue.

Medir el tamaño y la compresión de las tablas

Una de las principales razones por las que ClickHouse se utiliza para la observabilidad es la compresión. Además de reducir drásticamente los costes de almacenamiento, tener menos datos en disco implica menos E/S y consultas e inserciones más rápidas. La reducción de E/S compensará con creces la sobrecarga en CPU de cualquier algoritmo de compresión. Por lo tanto, mejorar la compresión de los datos debe ser la prioridad al trabajar para garantizar que las consultas de ClickHouse sean rápidas. Puedes encontrar más información sobre cómo medir la compresión aquí.
Última modificación el 19 de junio de 2026