简介
告警也能从 materialized views 中受益,并会自动利用它们。
这可以降低运行大量告警的计算开销,尤其是这类任务通常执行得非常频繁。
缩短执行时间有助于提升响应速度并降低资源消耗。
什么是增量materialized view
SELECT 查询。
与 Postgres 这类事务型数据库不同,ClickHouse 的 materialized view 并不是存储的快照。相反,它更像一个 trigger:当数据块写入源表时,它会对这些数据块执行查询。该查询的输出会写入一个单独的目标表。随着更多数据写入,新的部分结果会被追加并合并到目标表中。合并后的结果等同于对整个原始数据集执行 aggregation。
使用 materialized view 的主要原因在于,写入目标表的数据代表的是 aggregation、过滤或转换的结果。在 ClickStack 中,它们仅用于 aggregations。这些结果通常远小于原始输入数据,而且往往表示部分 aggregation 状态。再加上查询预聚合目标表本身更简单,相比在查询时对原始数据执行相同计算,这能显著降低查询延迟。
ClickHouse 中的 materialized view 会随着数据流入源表而持续更新,行为更像始终保持最新的索引。这与许多其他数据库不同:在那些数据库中,materialized view 是静态快照,必须定期刷新,类似于 ClickHouse 的 可刷新materialized view。
增量materialized view 只会在新数据到达时计算视图的变更,将计算前移到写入时。由于 ClickHouse 对摄取进行了高度优化,因此,相比查询执行时获得的收益,为每个写入块维护视图所增加的成本很小。aggregation 的计算成本被分摊到多次写入中,而不是在每次读取时反复付出。因此,查询预聚合结果的代价远低于重新计算这些结果,即使在 PB 级规模下,也能为下游可视化带来更低的运营成本和接近实时的性能。
这种模型与那些在每次更新时重新计算整个视图,或依赖定时刷新的系统有本质区别。若要更深入了解 materialized view 的工作原理以及如何创建它们,请参阅上方链接的指南。
每个 materialized view 都会带来额外的写入时开销,因此应谨慎使用。
单个 materialized view 可以针对不同分组计算多个指标,例如按 1 分钟桶统计每个服务名称的最小值、最大值和 p95 耗时。这样,一个视图就可以服务于多个可视化,而不只是一个。因此,将指标整合到共享视图中非常重要,这样才能最大化每个视图的价值,并确保它能在各个仪表盘和工作流中复用。
选择需要加速的可视化
识别高价值可视化项
- 刷新频繁且长期持续展示的仪表盘图表,例如显示在墙上大屏上的高层监控仪表盘。
- runbook 中使用的诊断工作流:在事件响应期间需要反复查看特定图表,并且要求快速返回结果。
- HyperDX 的核心体验,包括:
- 搜索页面中的直方图视图。
- 预设仪表盘中使用的可视化项,例如 APM、Services 或 Kubernetes 视图。
权衡收益与写入时成本
在投入生产环境之前,务必验证 materialized view 带来的资源开销,尤其是 CPU 使用率、磁盘 I/O 和合并活动。每个 materialized view 都会增加写入时的工作量,并产生额外的 parts,因此必须确保合并能够跟上,且 part 数量保持稳定。你可以通过开源 ClickHouse 中的系统表和内置可观测性仪表盘进行监控,也可以使用内置指标以及 ClickHouse Cloud 中的监控仪表盘。有关如何诊断和缓解 part 数量过多的问题,请参见 parts 过多。
toStartOfMinute 之类的函数,按时间间隔对数据进行分组。不过,许多可视化还会共用额外的分组键,例如服务名称、span 名称或状态码。当多个可视化使用相同的分组维度时,通常可以由同一个 materialized view 提供支持。
例如 (对于链路追踪) :
- 按服务名称统计随时间变化的平均耗时 -
SELECT avg(Duration), toStartOfMinute(Timestamp) as time, ServiceName FROM otel_traces GROUP BY ServiceName, time - 按服务名称统计随时间变化的请求数 -
SELECT count() count, toStartOfMinute(Timestamp) as time, ServiceName FROM otel_traces GROUP BY ServiceName, time - 按状态码统计随时间变化的平均耗时 -
SELECT avg(Duration), toStartOfMinute(Timestamp) as time, StatusCode FROM otel_traces GROUP BY StatusCode, time - 按状态码统计随时间变化的请求数 -
SELECT count() count, toStartOfMinute(Timestamp) as time, StatusCode FROM otel_traces GROUP BY StatusCode, time
创建 materialized view
如果 HyperDX 中某个组件没有调试面板,用户可以查看浏览器控制台,其中会记录所有查询。
AggregatingMergeTree:
你可以在下面的视频中查看如何使用 AggregatingMergeTree 和聚合函数的示例:
示例 materialized view
otel_traces_1m,用于存储相应的聚合状态:
otel_traces_1m_mv - 的定义会在插入新数据时计算这些状态并将其写入:
- 目标表:它定义了用于存储中间结果的 schema 和聚合状态类型。需要使用 AggregatingMergeTree 引擎,以确保这些状态能在后台被正确合并。
- materialized view 查询会在插入时自动执行。与原始查询相比,它使用
avgState和quantilesState这类状态函数,而不是最终聚合函数。
在 ClickStack 中使用 materialized view
注册 materialized view 以便使用
1
编辑 source
在 HyperDX 中找到相应的 source,然后打开 编辑配置 对话框。滚动到 materialized view 部分。2
添加 materialized view
选择 添加 materialized view,然后选择作为该 materialized view 后端的数据库和目标表。3
选择指标
大多数情况下,timestamp、维度和指标列都会自动推断出来。若未自动推断,请手动指定。对于指标,你必须映射:- 原始列名,例如
Duration,到 - materialized view 中对应的聚合列,例如
avg__Duration
4
选择时间粒度
选择 materialized view 的时间粒度,例如 1 分钟。5
6
保存 source
保存 source 配置。在仪表盘和可视化中验证加速效果
只有当 materialized view 的最小时间戳小于或等于查询时间范围的起始时间时,ClickStack 才会使用该视图,以确保视图包含所需的全部数据。尽管查询在内部会按时间拆分为子查询,但 materialized view 要么应用于整个查询,要么完全不应用。未来的改进可能会支持仅对符合条件的子查询选择性使用视图。
- 检查优化状态 查看仪表盘或可视化时,查找闪电或
Accelerated图标:
- 绿色闪电表示该查询已由 materialized view 加速。
- 橙色闪电表示该查询是在源表上执行的。
- 查看优化详情 点击闪电图标,打开详情面板,其中会显示:
- 活动 materialized view:为该查询选中的视图,包括其预估行数。
- 已跳过的 materialized views:兼容但未被选中的视图,以及它们的预估扫描大小。
- 不兼容的 materialized views:无法使用的视图,以及具体原因。
- 了解常见的不兼容原因 在以下情况下,materialized view 可能不会被使用:
- 查询时间范围起始于该视图最小时间戳之前。
- 可视化粒度不是该视图粒度的整数倍。
- 查询请求的聚合函数不存在于该视图中。
- 查询使用了自定义 count 表达式,例如
count(if(...)),无法从该视图的聚合状态中推导出来。
如何为可视化选择 materialized view
EXPLAIN ESTIMATE 机制,自动评估并选择效率最高的选项。
选择过程遵循一套明确的流程:
-
验证兼容性
ClickStack 首先会通过检查以下条件,判断某个 materialized view 是否可用于该查询:
- 时间覆盖范围:查询的时间范围必须完全落在该 materialized view 的可用数据范围内。
- 粒度:可视化的时间桶必须等于或粗于该视图的粒度。
- 聚合:请求的指标必须存在于该视图中,并且能够基于其聚合状态计算出来。
-
转换查询
对于兼容的视图,ClickStack 会重写查询,使其指向 materialized view 对应的表:
- 将聚合函数映射到对应的物化列。
- 对聚合状态应用
-Merge组合器。 - 调整时间分桶,使其与该视图的粒度保持一致。
-
选择最佳候选项
如果有多个兼容的 materialized view,ClickStack 会为每个候选项运行一次
EXPLAIN ESTIMATE查询,并比较预估扫描的行数和粒度。最终会选择预估扫描成本最低的视图。 - 优雅回退 如果没有兼容的 materialized view,ClickStack 会自动回退为查询源表。
选择 materialized view 的示例
otel_traces_1m,按分钟、ServiceName和StatusCode分组otel_traces_1m_v2,按分钟、ServiceName、StatusCode和SpanName分组
EXPLAIN ESTIMATE 查询,并比较预估的粒度数量,即:
otel_traces_1m 更小、扫描的粒度也更少,因此会被自动选中。
这两个 materialized view 的性能仍然优于直接查询基表,但选择能够满足需求的最小视图,性能最佳。
告警
回填 materialized view
回填方式
避免使用 POPULATE除了在已暂停摄取的小型数据集场景外,不建议使用 POPULATE 命令回填 materialized view。该操作符可能会漏掉插入到其源表中的行,因为 materialized view 是在 POPULATE 完成后才创建的。此外,POPULATE 会对全部数据执行处理,在大型数据集上容易受到中断或内存限制的影响。
使用 INSERT INTO SELECT 直接回填
1
确定视图当前的覆盖范围
在尝试任何回填之前,首先需要确认 materialized view 已经包含了哪些数据。这可以通过查询目标表中的最小时间戳来完成:2
判断是否需要回填
在大多数 ClickStack 部署中,查询通常聚焦于最近的数据,例如过去 24 小时。在这种情况下,新创建的视图在创建后不久就能完全投入使用,因此无需回填。如果上一步返回的时间戳对你的使用场景来说已经足够早,就不需要回填。只有在以下情况下才应考虑回填:- 查询经常覆盖较长的历史时间范围。
- 该视图对这些时间范围内的性能至关重要。
- 数据集规模和聚合成本使回填具备可行性。
3
回填缺失的历史数据
如果需要回填,请基于该视图的查询进行修改,使其只读取早于上述时间戳的数据,从而为 materialized view 的目标表补齐当前最小时间戳之前的历史数据。由于目标表使用的是 AggregatingMergeTree,回填查询必须插入聚合状态,而不是最终值。请注意,下面的查询添加了一个WHERE 子句,将聚合限制为早于视图中最早时间戳的数据:使用 Null 表进行增量回填
INSERT INTO SELECT 直接回填可能并不现实,甚至存在风险。这种情况下,建议采用增量回填。这种方法更接近增量materialized view 的常规工作方式:不是一次聚合整个历史数据集,而是按可控的数据块逐步处理。
这种方法适用于以下情况:
- 回填查询否则需要运行数小时。
- 完整聚合的峰值内存占用过高。
- 你希望在回填期间严格控制 CPU 和内存消耗。
- 你需要一种更稳健的流程,以便在中断后也能安全重启。
1
为回填创建一个 Null 表
创建一个轻量级的 Null 表,只包含 materialized view 聚合所需的列。这样可以将 I/O 和内存占用降到最低。2
将 materialized view 挂接到 Null 表
接下来,在 Null 表上创建一个 materialized view,并让它指向与你的主 materialized view 相同的聚合表。3
以增量方式回填数据
最后,将历史数据插入 Null 表。materialized view 会按数据块逐步处理这些数据,将聚合状态写入目标表,而不会持久化原始行。为进一步提升安全性,可以考虑让回填 materialized view 指向一个临时目标表 (例如
otel_traces_1m_v2) 。回填成功完成后,可以将分区移动到主目标表,例如 ALTER TABLE otel_traces_1m_v2 MOVE PARTITION '2026-01-02' TO otel_traces_1m。这样一来,如果回填因中断或资源限制而失败,就能更方便地恢复。建议
粒度选择与对齐
- 时间图表 (x 轴为时间的折线图或柱状图) : 图表显式指定的粒度必须是 materialized view 粒度的整数倍。 例如,10 分钟图表可以使用粒度为 10、5、2 或 1 分钟的 materialized view,但不能使用 20 分钟或 3 分钟粒度的视图。
-
非时间图表 (数值、表格或摘要图表) :
有效粒度通过
(time range / 80)计算得出,并向上取整到 HyperDX 支持的最近粒度。该派生粒度也必须是 materialized view 粒度的整数倍。
- 不要创建 10 分钟粒度的 materialized view。 ClickStack 支持图表和告警使用 15 分钟粒度,但不支持 10 分钟粒度。因此,10 分钟的 materialized view 将无法兼容常见的 15 分钟图表和告警。
- 优先选择 1 分钟或1 小时粒度,它们与大多数图表和告警配置都能良好适配。
限制并整合 materialized view
- 每个 source 最多不超过 20 个 materialized view。
- 通常以 10 个左右的 materialized view 为最佳。
- 当多个可视化基于相同维度时,应将其整合到单个视图中。
谨慎选择维度
- 每增加一个分组列,视图的体积都会增大。
- 需要在查询灵活性与存储、插入时成本之间做好权衡。
- 如果对视图中不存在的列使用过滤器,ClickStack 会回退到源表。
提示一种常见且几乎总是有用的基础做法是:创建一个按 服务名 分组、并包含 count 指标的 materialized view,这样可以在搜索和仪表盘中快速生成直方图并提供服务级概览。
聚合列的命名约定
- 模式:
<aggFn>__<sourceColumn> - 示例:
avg__Durationmax__Durationcount__用于行计数
分位数与草图选择
quantiles会在磁盘上生成更大的草图,但在写入时计算开销更低。quantileTDigest在写入时计算开销更高,但生成的草图更小,因此视图查询通常也会更快。
quantile(0.5)) 。之后仍可基于生成的草图查询其他分位数值,例如 quantile(0.95)。建议通过实验找到最适合你的工作负载的平衡点。
持续验证效果
- 通过 UI 中的加速指示器确认其使用情况。
- 比较启用该视图前后的查询性能。
- 监控资源使用情况和合并行为。
高级配置
- 高分辨率的近期数据,配合较粗粒度的历史视图
- 用于概览的服务级视图,以及用于深度诊断的端点级视图
局限性
常见不兼容原因
- 查询时间范围 查询时间范围的起始时间早于 materialized view 的最小时间戳。由于视图不会自动回填,因此只能满足其时间范围被完全覆盖的查询。
-
粒度不匹配
可视化的实际粒度必须是 materialized view 粒度的整数倍。具体而言:
- 对于时间图表 (x 轴为时间的折线图或柱状图) ,图表选择的粒度必须是视图粒度的倍数。例如,10 分钟图表可以使用粒度为 10、5、2 或 1 分钟的 materialized view,但不能使用 20 分钟或 3 分钟的视图。
- 对于非时间图表 (数值图表或表格图表) ,实际粒度按
(time range / 80)计算,向上取整到最接近的 HyperDX 支持粒度,并且同样必须是视图粒度的倍数。
- 不支持的聚合函数 查询所需的聚合在 materialized view 中不存在。只有在视图中显式计算并存储的聚合才能使用。
-
自定义计数表达式
使用
count(if(...))或其他条件计数表达式的查询,无法从标准聚合状态中推导出来,因此不能使用 materialized view。
设计和运维约束
- 不会自动回填 增量materialized view 只包含创建后插入的数据。若要加速历史数据,必须显式执行回填;而对于大型数据集,这样做的成本可能很高,甚至并不现实。
- 粒度权衡 粒度过细的视图会增大存储占用和插入时开销,而粒度过粗的视图则会降低灵活性。必须谨慎选择粒度,以匹配预期的查询模式。
- 维度爆炸 增加大量分组维度会显著扩大视图规模,并可能降低其效果。视图应只包含常用的分组列和过滤列。
- 视图数量的可扩展性有限 每个 materialized view 都会带来额外的插入时开销,并增加合并压力。创建过多视图会对摄取和后台合并造成负面影响。
故障排查
materialized view 未生效
- 打开优化弹窗,查看是否显示“Date range not supported.”
- 确保查询的日期范围晚于 materialized view 的最小日期。
- 如果 materialized view 包含全部历史数据,请移除最小日期。
- 确认图表粒度是 MV 粒度的整数倍。
- 尝试将图表设置为“Auto”,或手动选择兼容的粒度。
- 检查图表使用的聚合是否包含在 MV 中。
- 在优化弹窗中查看“Available aggregated columns”。
- 确保 group by 的列包含在 MV 的维度列中。
- 在优化弹窗中查看“Available group/filter columns”。
materialized view 查询缓慢
- 由于粒度太细 (例如 1 秒) ,MV 的行数过多。
- 解决方案:创建一个粒度更粗的 MV (例如 1 分钟或 1 小时) 。
- 由于维度列过多,MV 的基数过高。
- 解决方案:将维度列精简为最常用的几个。
- 系统会对每个 MV 运行
EXPLAIN。 - 解决方案:删除很少使用或总是被跳过的 MV。
配置错误
- 在 MV 配置中至少添加一个聚合列。
- 请指定要聚合的列 (只有 count 可以省略源列) 。
- 请使用下拉列表中的预设粒度之一。
- 格式必须是有效的 SQL 时间间隔 (例如
1 hour,而不是1 h) 。