Contexto
SELECT más rápidas.
A diferencia de las bases de datos transaccionales como Postgres, una vista materializada en ClickHouse es simplemente un disparador que ejecuta una consulta sobre bloques de datos a medida que se insertan en una tabla. El resultado de esta consulta se inserta en una segunda “tabla de destino”. Si se insertan más filas, los resultados volverán a enviarse a la tabla de destino, donde los resultados intermedios se actualizarán y fusionarán. Este resultado fusionado equivale a ejecutar la consulta sobre todos los datos originales.
La principal motivación de las vistas materializadas es que los resultados insertados en la tabla de destino representan el resultado de una agregación, un filtrado o una transformación de filas. A menudo, estos resultados son una representación más pequeña de los datos originales (un resumen parcial en el caso de las agregaciones). Esto, junto con el hecho de que la consulta resultante para leer los resultados de la tabla de destino es sencilla, hace que los tiempos de consulta sean menores que si el mismo cálculo se realizara sobre los datos originales, trasladando el cálculo (y, por tanto, la latencia de la consulta) del tiempo de consulta al tiempo de inserción.
Las 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. Esto contrasta con otras bases de datos, donde 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).
Ejemplo
toStartOfDay:
SELECT anterior sobre los datos insertados en votes y envía los resultados a up_down_votes_per_day:
TO es fundamental aquí, ya que indica adónde se enviarán los resultados, es decir, a up_down_votes_per_day.
Podemos volver a llenar nuestra tabla Votes con el insert anterior:
up_down_votes_per_day: deberíamos tener 1 fila por día:
votes) a 5000 al almacenar el resultado de nuestra consulta. Sin embargo, lo importante es que, si se insertan nuevos votos en la tabla votes, se enviarán nuevos valores a up_down_votes_per_day para el día correspondiente, donde se combinarán automáticamente de forma asíncrona en segundo plano, conservando solo una fila por día. Así, up_down_votes_per_day siempre será pequeña y estará actualizada.
Dado que la combinación de filas es asíncrona, puede haber más de una fila por día cuando un usuario consulta los datos. Para asegurarnos de que cualquier fila pendiente se combine en tiempo de consulta, tenemos dos opciones:
- Usar el modificador
FINALen el nombre de la tabla. Hicimos esto en la consulta de recuento anterior. - Agregar por la clave de ordenación utilizada en nuestra tabla final, es decir,
CreationDate, y sumar las métricas. Normalmente, esto es más eficiente y flexible (la tabla puede usarse para otras cosas), pero la primera opción puede ser más sencilla para algunas consultas. A continuación mostramos ambas:
Un ejemplo más complejo
Score y la media de CommentCount. La consulta para calcularlo podría verse así:
posts.
A modo de ejemplo, y para evitar cargar los datos de publicaciones desde S3, crearemos una tabla duplicada posts_null con el mismo esquema que posts. Sin embargo, esta tabla no almacenará ningún dato y simplemente la usará la vista materializada cuando se inserten filas. Para evitar el almacenamiento de datos, podemos usar el tipo de motor de tabla Null.
/dev/null. Nuestra vista materializada calculará y almacenará las estadísticas resumidas cuando nuestra tabla posts_null reciba filas en el momento de la inserción; solo actúa como disparador. Sin embargo, los datos en bruto no se almacenarán. Aunque en nuestro caso probablemente sigamos queriendo almacenar los posts originales, este enfoque puede usarse para calcular agregados evitando la sobrecarga de almacenamiento de los datos en bruto.
La vista materializada queda así:
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 contendrá información adicional que permitirá que este estado parcial se combine con otros estados. Por ejemplo, en el caso de un promedio, incluirá un recuento y una suma de la columna.
Los estados de agregación parciales son necesarios para calcular resultados correctos. Por ejemplo, para calcular un promedio, simplemente promediar los promedios de subrangos produce resultados incorrectos.Ahora creamos la tabla de destino para esta vista,
post_stats_per_day, que almacena estos estados de agregación parciales:
SummingMergeTree era suficiente para almacenar recuentos, necesitamos un tipo de motor más avanzado para otras funciones: AggregatingMergeTree.
Para indicar a ClickHouse que se almacenarán estados de agregación, definimos Score_quantiles y AvgCommentCount con el tipo AggregateFunction, especificando la función de la que provienen los estados parciales y el tipo de sus columnas de origen. Al igual que en SummingMergeTree, las filas con el mismo valor de clave ORDER BY se fusionarán (Day en el ejemplo anterior).
Para poblar post_stats_per_day mediante nuestra vista materializada, simplemente podemos insertar todas las filas de posts en posts_null:
En producción, lo habitual sería adjuntar la vista materializada a la tablaNuestra consulta final debe utilizar el sufijoposts. Aquí hemos usadoposts_nullpara ilustrar la tablaNull.
Merge en nuestras funciones (ya que las columnas almacenan estados de agregación parciales):
GROUP BY en lugar de FINAL.
Otras aplicaciones
Filtrado y transformación
posts_null podría recibir inserciones, con una consulta SELECT que filtre las filas antes de insertarlas en la tabla posts. Por ejemplo, supongamos que quisiéramos transformar una columna Tags de nuestra tabla posts. Esta contiene una lista de nombres de etiquetas delimitada por barras verticales. Al convertirla en un array, podemos agregar con más facilidad por valores individuales de etiqueta.
Podríamos realizar esta transformación al ejecutar unNuestra vista materializada para esta transformación se muestra a continuación:INSERT INTO SELECT. La vista materializada nos permite encapsular esta lógica en el DDL de ClickHouse y mantener simple nuestroINSERT, aplicando la transformación a cualquier fila nueva.
Tabla de consulta
comments:
PostId.
Supongamos que un usuario desea filtrar por un UserId específico y calcular su Score promedio:
PostId con los que filtrar la columna UserId. Estos valores pueden usarse después para realizar una búsqueda eficiente.
En este ejemplo, nuestra vista materializada puede ser muy simple: seleccionar solo PostId y UserId de comments durante la inserción. Estos resultados, a su vez, se envían a una tabla comments_posts_users ordenada por UserId. A continuación, creamos una versión nula de la tabla Comments y la usamos para poblar nuestra vista y la tabla comments_posts_users:
Encadenamiento / cascada de vistas materializadas
Vistas materializadas y JOINs
Vistas materializadas actualizablesLo siguiente se aplica solo a las vistas materializadas incrementales. Las vistas materializadas actualizables ejecutan periódicamente su consulta sobre todo el conjunto de datos de destino y admiten plenamente los JOINs. Considere usarlas para JOINs complejos si se puede tolerar que los resultados estén menos actualizados.
JOIN, pero con una restricción crucial: la vista materializada solo se activa con inserciones en la tabla de origen (la tabla situada más a la izquierda en la consulta). Las tablas del lado derecho de los JOINs no activan actualizaciones, aunque sus datos cambien. Este comportamiento es especialmente importante al crear vistas materializadas incrementales, donde los datos se agregan o transforman en el momento de la inserción.
Cuando una vista materializada incremental se define con un JOIN, la tabla situada más a la izquierda en la consulta SELECT actúa como origen. Cuando se insertan nuevas filas en esta tabla, ClickHouse ejecuta la consulta de la vista materializada solo con esas filas recién insertadas. Las tablas del lado derecho del JOIN se leen completas durante esta ejecución, pero sus cambios por sí solos no activan la vista.
Este comportamiento hace que los JOINs en las vistas materializadas se parezcan a un snapshot join con datos de dimensión estáticos.
Esto funciona bien para enriquecer datos con tablas de referencia o de dimensión. Sin embargo, las actualizaciones en las tablas del lado derecho (p. ej., metadatos de usuario) no actualizarán retroactivamente la vista materializada. Para ver los datos actualizados, deben llegar nuevas inserciones a la tabla de origen.
Ejemplo
users.
Como recordatorio, los esquemas de nuestras tablas son:
users ya está poblada:
Alineación entre agrupación y ordenaciónLa cláusula
GROUP BY de la vista materializada debe incluir DisplayName, UserId y Day para que coincida con el ORDER BY de la tabla de destino SummingMergeTree. Esto garantiza que las filas se agreguen y fusionen correctamente. Si se omite cualquiera de estos elementos, se pueden obtener resultados incorrectos o fusiones ineficientes.daily_badges_by_user.
Buenas prácticas para JOINs en vistas materializadas
-
Use la tabla más a la izquierda como desencadenante. Solo la tabla del lado izquierdo de la sentencia
SELECTactiva la vista materializada. Los cambios en las tablas del lado derecho no desencadenarán actualizaciones. - Inserte antes los datos de las tablas unidas. Asegúrese de que los datos de las tablas unidas existan antes de insertar filas en la tabla de origen. El JOIN se evalúa en el momento de la inserción, por lo que la falta de datos dará lugar a filas sin coincidencia o valores nulos.
- Limite las columnas extraídas de los JOINs. Seleccione solo las columnas necesarias de las tablas unidas para minimizar el uso de memoria y reducir la latencia en la inserción (vea más abajo).
- Evalúe el rendimiento en la inserción. Los JOINs aumentan el coste de las inserciones, especialmente con tablas grandes en el lado derecho. Haga benchmark de las tasas de inserción con datos de producción representativos.
- Prefiera Dictionaries para lookups simples. Use Dictionaries para lookups de clave-valor (por ejemplo, de ID de usuario a nombre) y así evitar operaciones JOIN costosas.
-
Alinee
GROUP BYyORDER BYpara mejorar la eficiencia de la fusión. Al usarSummingMergeTreeoAggregatingMergeTree, asegúrese de queGROUP BYcoincida con la cláusulaORDER BYde la tabla de destino para permitir una fusión eficiente de filas. - Use alias de columna explícitos. Cuando las tablas tengan nombres de columna coincidentes, use alias para evitar ambigüedades y garantizar resultados correctos en la tabla de destino.
- Tenga en cuenta el volumen y la frecuencia de inserción. Los JOINs funcionan bien con cargas de inserción moderadas. Para una ingestión de alto throughput, considere usar tablas de staging, pre-joins u otros enfoques, como Dictionaries y Refreshable Materialized Views.
Uso de la tabla de origen en filtros y JOIN
Escenario de ejemplo
Explicación
mvw1 y mvw2, que realizan operaciones similares, pero con una ligera diferencia en la forma en que hacen referencia a la tabla de origen t0.
En mvw1, la tabla t0 se referencia directamente dentro de una subconsulta (SELECT * FROM t0) en el lado derecho del JOIN. Cuando se insertan datos en t0, la consulta de la vista materializada se ejecuta sustituyendo t0 por el bloque de datos insertado. Esto significa que la operación JOIN se realiza solo sobre las filas recién insertadas, no sobre toda la tabla.
En el segundo caso, al hacer JOIN con vt0, la vista lee todos los datos de t0. Esto garantiza que la operación JOIN tenga en cuenta todas las filas de t0, no solo el bloque recién insertado.
La diferencia clave radica en cómo ClickHouse maneja la tabla de origen en la consulta de la vista materializada. Cuando una vista materializada se desencadena por una inserción, la tabla de origen (t0 en este caso) se sustituye por el bloque de datos insertado. Este comportamiento puede aprovecharse para optimizar las consultas, pero también debe tenerse en cuenta cuidadosamente para evitar resultados inesperados.
Casos de uso y advertencias
IN (SELECT id FROM t0) solo contiene las filas recién insertadas, lo que puede ayudar a filtrar t1 con respecto a ese conjunto.
Ejemplo con Stack Overflow
users.
badges, p. ej.
users usando los ID de usuario de las filas insertadas en badges:
users correspondiente al id de usuario 2936484. Esta consulta también está optimizada mediante la clave de ordenación Id de la tabla.
Vistas materializadas y uniones
UNION ALL se usan habitualmente para combinar datos de varias tablas de origen en un único conjunto de resultados.
Aunque UNION ALL no se admite directamente en las vistas materializadas incrementales, puedes lograr el mismo resultado creando una vista materializada independiente para cada rama de SELECT y escribiendo sus resultados en una tabla de destino compartida.
Para este ejemplo, usaremos el conjunto de datos de Stack Overflow. Considera las tablas badges y comments que aparecen a continuación, que representan las insignias obtenidas por un usuario y los comentarios que hace en las publicaciones:
INSERT INTO:
badges o en comments, un enfoque ingenuo de este problema sería intentar crear una vista materializada con la consulta UNION anterior:
comments. Por ejemplo:
badges no activarán la vista, por lo que user_activity no recibirá actualizaciones:
comments:
badges se reflejan en la tabla user_activity:
Procesamiento paralelo frente a secuencial
parallel_view_processing.
De forma predeterminada, esta configuración es 0 (false), lo que significa que las vistas materializadas se ejecutan secuencialmente en orden de uuid.
Por ejemplo, considere la siguiente tabla source y 3 vistas materializadas, cada una de las cuales envía filas a una tabla target:
target, e incluye también su nombre y la hora de inserción.
Insertar una fila en la tabla source tarda ~3 segundos, y cada vista se ejecuta de manera secuencial:
SELECT:
uuid de las vistas:
parallel_view_processing=1 activado. Con esta opción activada, las vistas se ejecutan en paralelo, sin garantizar el orden en que las filas llegan a la tabla de destino:
Cuándo usar el procesamiento en paralelo
parallel_view_processing=1 puede mejorar significativamente el rendimiento de inserción, como se muestra arriba, especialmente cuando varias vistas materializadas están asociadas a una sola tabla. Sin embargo, es importante entender las contrapartidas:
- Mayor presión durante la inserción: Todas las vistas materializadas se ejecutan simultáneamente, lo que aumenta el uso de CPU y memoria. Si cada vista realiza cálculos pesados o JOINs, esto puede sobrecargar el sistema.
- Necesidad de un orden de ejecución estricto: En flujos de trabajo poco frecuentes en los que el orden de ejecución de las vistas es importante (p. ej., dependencias encadenadas), la ejecución en paralelo puede provocar un estado incoherente o condiciones de carrera. Aunque es posible diseñar una solución para evitarlo, estas configuraciones son frágiles y pueden dejar de funcionar en versiones futuras.
Valores predeterminados históricos y estabilidadLa ejecución secuencial fue la opción predeterminada durante mucho tiempo, en parte debido a la complejidad del manejo de errores. Históricamente, un fallo en una vista materializada podía impedir que se ejecutaran las demás. Las versiones más recientes han mejorado esto al aislar los fallos por bloque, pero la ejecución secuencial sigue ofreciendo una semántica de fallos más clara.
parallel_view_processing=1 cuando:
- Tienes varias vistas materializadas independientes
- Buscas maximizar el rendimiento de inserción
- Conoces la capacidad del sistema para manejar la ejecución concurrente de vistas
- Las vistas materializadas dependen unas de otras
- Necesitas una ejecución predecible y ordenada
- Estás depurando o auditando el comportamiento de inserción y quieres una reproducción determinista
Vistas materializadas y expresiones de tabla comunes (CTE)
Las expresiones de tabla comunes no se materializanClickHouse no materializa las CTE; en su lugar, sustituye la definición de la CTE directamente en la consulta, lo que puede dar lugar a múltiples evaluaciones de la misma expresión (si la CTE se usa más de una vez).
- Si una CTE hace referencia a una tabla distinta de la tabla de origen (es decir, aquella a la que está asociada la vista materializada) y se usa en una cláusula
JOINoIN, se comportará como una subconsulta o unJOIN, no como un disparador. - La vista materializada seguirá activándose solo con inserciones en la tabla de origen principal, pero la CTE se volverá a ejecutar en cada inserción, lo que puede generar una sobrecarga innecesaria, especialmente si la tabla a la que hace referencia es grande.
users se vuelve a evaluar con cada inserción en posts, y la vista materializada no se actualizará cuando se inserten nuevos users, sino solo cuando se inserten posts.
En general, use las CTE para la lógica que opera sobre la misma tabla de origen a la que está asociada la vista materializada, o asegúrese de que las tablas a las que se hace referencia sean pequeñas y sea poco probable que provoquen cuellos de botella de rendimiento. Como alternativa, considere las mismas optimizaciones que para los JOIN con vistas materializadas.