跳转到主要内容

简介

ClickStack 可以利用增量materialized view (IMV)来加速依赖重度聚合查询的可视化,例如计算随时间变化的每分钟平均请求耗时。该功能可显著提升查询性能,通常在规模较大的部署中收益最明显——大约从每日 10 TB 及以上开始——并支持进一步扩展到每日 PB 级别。增量materialized view 目前处于 Beta 阶段,应谨慎使用。
告警也能从 materialized views 中受益,并会自动利用它们。 这可以降低运行大量告警的计算开销,尤其是这类任务通常执行得非常频繁。 缩短执行时间有助于提升响应速度并降低资源消耗。

什么是增量materialized view

增量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 都会带来额外的写入时开销,因此应谨慎使用。
仅为最常用的仪表盘和可视化创建视图。 当该功能处于 Beta 阶段时,将使用量限制在少于 20 个视图。 预计这一阈值会在未来版本中提高。
单个 materialized view 可以针对不同分组计算多个指标,例如按 1 分钟桶统计每个服务名称的最小值、最大值和 p95 耗时。这样,一个视图就可以服务于多个可视化,而不只是一个。因此,将指标整合到共享视图中非常重要,这样才能最大化每个视图的价值,并确保它能在各个仪表盘和工作流中复用。
继续之前,建议先更深入地了解 ClickHouse 中的 materialized view。 更多细节请参阅我们的增量materialized view指南。

选择需要加速的可视化

在创建任何 materialized view 之前,务必先明确你希望加速哪些可视化,以及哪些工作流对用户最关键。 在 ClickStack 中,materialized view 旨在加速以聚合为主的可视化,也就是那些按时间计算一个或多个指标的查询。例如,每分钟平均请求耗时每个服务的请求数随时间变化的错误率。materialized view 必须始终包含聚合以及基于时间的分组,因为它本就是为时序可视化服务的。 通常,建议如下:

识别高价值可视化项

最值得加速的对象通常属于以下几类之一:
  • 刷新频繁且长期持续展示的仪表盘图表,例如显示在墙上大屏上的高层监控仪表盘。
  • runbook 中使用的诊断工作流:在事件响应期间需要反复查看特定图表,并且要求快速返回结果。
  • HyperDX 的核心体验,包括:
    • 搜索页面中的直方图视图。
    • 预设仪表盘中使用的可视化项,例如 APM、Services 或 Kubernetes 视图。
这些可视化项通常会在不同用户和时间范围下被反复执行,因此非常适合将计算从查询时转移到写入时。

权衡收益与写入时成本

materialized view 会在写入时增加额外负担,因此应有选择地谨慎创建。并非每个可视化都能从预聚合中受益,而为使用频率很低的图表提速,通常并不值得这部分开销。materialized view 的总数应控制在 20 个以内。
在投入生产环境之前,务必验证 materialized view 带来的资源开销,尤其是 CPU 使用率、磁盘 I/O 和合并活动。每个 materialized view 都会增加写入时的工作量,并产生额外的 parts,因此必须确保合并能够跟上,且 part 数量保持稳定。你可以通过开源 ClickHouse 中的系统表内置可观测性仪表盘进行监控,也可以使用内置指标以及 ClickHouse Cloud 中的监控仪表盘。有关如何诊断和缓解 part 数量过多的问题,请参见 parts 过多
一旦你确定了最重要的可视化,下一步就是归并。

将可视化整合到共享视图中

ClickStack 中的所有 materialized view 都应使用诸如 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,不如将它们合并为一个按服务名称和状态码聚合的视图。这个视图可以计算多个指标,例如计数、平均耗时、最大耗时以及百分位数,随后复用于多个可视化。下面展示了一个将上述内容合并后的查询示例:
SELECT avg(Duration), max(Duration), count(), quantiles(0.95,0.99)(Duration), toStartOfMinute(Timestamp) as time, ServiceName, StatusCode
FROM otel_traces
GROUP BY time, ServiceName, StatusCode
以这种方式整合视图,可以减少插入时的开销,控制 materialized view 的总数量,减少 parts 数量相关问题,并简化后续维护。 在这个阶段,重点关注你想要加速的可视化所发起的查询。下一节将通过一个示例说明,如何将多个聚合查询合并为一个 materialized view。

创建 materialized view

确定要加速的一个或一组可视化后,下一步就是找出其底层查询。实际操作中,这意味着检查可视化配置并审查生成的 SQL,重点关注所用的聚合指标和应用的函数。
如果 HyperDX 中某个组件没有调试面板,用户可以查看浏览器控制台,其中会记录所有查询。
梳理出所需查询后,你应该先熟悉 ClickHouse 中的 聚合状态函数。materialized view 依赖这些函数将计算从查询时转移到写入时。materialized view 不会存储最终聚合值,而是计算并存储中间聚合状态,然后在查询时再对其进行合并并完成最终聚合。与原始表相比,这些状态通常会小得多。这些状态有对应的专用数据类型,必须在目标表的 schema 中显式定义。 作为参考,ClickHouse 文档中提供了聚合状态函数的详细概述和示例,以及用于存储这些状态的表引擎 AggregatingMergeTree 你可以在下面的视频中查看如何使用 AggregatingMergeTree 和聚合函数的示例:
强烈建议你在继续之前先熟悉这些概念。

示例 materialized view

请看下面这个原始查询:它按分钟、服务名称和状态码分组,计算平均耗时、最大耗时、事件数以及各百分位数:
SELECT
    toStartOfMinute(Timestamp),
    ServiceName,
    StatusCode,
    count() AS count,
    avg(Duration),
    max(Duration),
    quantiles(0.95, 0.99)(Duration)
FROM otel_traces
GROUP BY
    time,
    ServiceName,
    StatusCode
为加快此查询,请创建一个目标表 otel_traces_1m,用于存储相应的聚合状态:
CREATE TABLE otel_traces_1m
(
    `Timestamp` DateTime,
    `ServiceName` LowCardinality(String),
    `StatusCode` LowCardinality(String),
    `count` SimpleAggregateFunction(sum, UInt64),
    `avg__Duration` AggregateFunction(avg, UInt64),
    `max__Duration` SimpleAggregateFunction(max, Int64),
    `quantiles__Duration` AggregateFunction(quantiles(0.95, 0.99), Int64)
)
ENGINE = AggregatingMergeTree
ORDER BY (Timestamp, ServiceName, StatusCode);
然后,materialized view - otel_traces_1m_mv - 的定义会在插入新数据时计算这些状态并将其写入:
CREATE MATERIALIZED VIEW otel_traces_1m_mv TO otel_traces_1m
AS
SELECT
    toStartOfMinute(Timestamp) AS Timestamp,
    ServiceName,
    StatusCode,
    count() AS count,
    avgState(Duration) AS avg__Duration,
    maxSimpleState(Duration) AS max__Duration,
    quantilesState(0.95, 0.99)(Duration) AS quantiles__Duration
FROM otel_v2.otel_traces
GROUP BY
    Timestamp,
    ServiceName,
    StatusCode;
这个 materialized view 由两部分组成:
  1. 目标表:它定义了用于存储中间结果的 schema 和聚合状态类型。需要使用 AggregatingMergeTree 引擎,以确保这些状态能在后台被正确合并。
  2. materialized view 查询会在插入时自动执行。与原始查询相比,它使用 avgStatequantilesState 这类状态函数,而不是最终聚合函数。
最终得到的是一张紧凑的表,按分钟为每个服务名和状态码存储聚合状态。它的大小会随时间和基数以可预测的方式增长;经过后台合并后,它表示的结果与直接对原始数据执行原始聚合查询的结果相同。查询这张表的成本远低于直接从源链路追踪表进行聚合,因此能够在大规模场景下实现快速且稳定一致的可视化性能。

在 ClickStack 中使用 materialized view

在 ClickHouse 中创建 materialized view 后,必须将其注册到 ClickStack 中,以便可视化、仪表盘和告警自动使用。

注册 materialized view 以便使用

materialized view 应注册到 HyperDX 中与其所派生自的原始源表对应的 source 上。
1

编辑 source

在 HyperDX 中找到相应的 source,然后打开 编辑配置 对话框。滚动到 materialized view 部分。
2

添加 materialized view

选择 添加 materialized view,然后选择作为该 materialized view 后端的数据库和目标表。
3

选择指标

大多数情况下,timestamp、维度和指标列都会自动推断出来。若未自动推断,请手动指定。对于指标,你必须映射:
  • 原始列名,例如 Duration,到
  • materialized view 中对应的聚合列,例如 avg__Duration
对于维度,请指定除 timestamp 之外,该视图用于分组的所有列。
4

选择时间粒度

选择 materialized view 的时间粒度,例如 1 分钟。
5

选择最小日期

指定 materialized view 包含数据的最早日期。这表示该视图中可用的最早 timestamp;如果摄取始终持续进行,通常就是该视图的创建时间。
materialized view 在创建时不会自动回填,因此它们只会包含创建后插入的数据所生成的行。 有关回填 materialized view 的完整指南,请参阅“回填数据”
如果无法确定确切的开始时间,可以通过查询目标表中的最小 timestamp 来确定,例如:
SELECT min(Timestamp) FROM otel_traces_1m
6

保存 source

保存 source 配置。
注册 materialized view 后,只要查询符合条件,ClickStack 就会自动使用它,无需修改仪表盘、可视化或告警。ClickStack 会在执行时评估每个查询,并确定是否可以应用 materialized view。

在仪表盘和可视化中验证加速效果

请务必记住,增量materialized view 仅包含在视图创建之后插入的数据。它们不会自动回填,因此能够保持轻量且维护成本较低。因此,用户在注册视图时必须显式指定其有效时间范围。
只有当 materialized view 的最小时间戳小于或等于查询时间范围的起始时间时,ClickStack 才会使用该视图,以确保视图包含所需的全部数据。尽管查询在内部会按时间拆分为子查询,但 materialized view 要么应用于整个查询,要么完全不应用。未来的改进可能会支持仅对符合条件的子查询选择性使用视图。
ClickStack 提供了清晰的可视化标识,用于确认是否正在使用 materialized view。
  1. 检查优化状态 查看仪表盘或可视化时,查找闪电或 Accelerated 图标:
  • 绿色闪电表示该查询已由 materialized view 加速。
  • 橙色闪电表示该查询是在源表上执行的。
  1. 查看优化详情 点击闪电图标,打开详情面板,其中会显示:
  • 活动 materialized view:为该查询选中的视图,包括其预估行数。
  • 已跳过的 materialized views:兼容但未被选中的视图,以及它们的预估扫描大小。
  • 不兼容的 materialized views:无法使用的视图,以及具体原因。
  1. 了解常见的不兼容原因 在以下情况下,materialized view 可能不会被使用:
  • 查询时间范围起始于该视图最小时间戳之前。
  • 可视化粒度不是该视图粒度的整数倍。
  • 查询请求的聚合函数不存在于该视图中。
  • 查询使用了自定义 count 表达式,例如 count(if(...)),无法从该视图的聚合状态中推导出来。
这些标识可以帮助你轻松确认某个可视化是否已加速,了解为何选择了特定视图,并诊断某个视图为何不符合使用条件。

如何为可视化选择 materialized view

执行可视化时,ClickStack 可能有多个可选对象,包括基表和多个 materialized view。为确保获得最佳性能,ClickStack 会借助 ClickHouse 的 EXPLAIN ESTIMATE 机制,自动评估并选择效率最高的选项。 选择过程遵循一套明确的流程:
  1. 验证兼容性 ClickStack 首先会通过检查以下条件,判断某个 materialized view 是否可用于该查询:
    • 时间覆盖范围:查询的时间范围必须完全落在该 materialized view 的可用数据范围内。
    • 粒度:可视化的时间桶必须等于或粗于该视图的粒度。
    • 聚合:请求的指标必须存在于该视图中,并且能够基于其聚合状态计算出来。
  2. 转换查询 对于兼容的视图,ClickStack 会重写查询,使其指向 materialized view 对应的表:
    • 将聚合函数映射到对应的物化列。
    • 对聚合状态应用 -Merge 组合器。
    • 调整时间分桶,使其与该视图的粒度保持一致。
  3. 选择最佳候选项 如果有多个兼容的 materialized view,ClickStack 会为每个候选项运行一次 EXPLAIN ESTIMATE 查询,并比较预估扫描的行数和粒度。最终会选择预估扫描成本最低的视图。
  4. 优雅回退 如果没有兼容的 materialized view,ClickStack 会自动回退为查询源表。
这种方式能够持续将扫描数据量降到最低,并且无需修改可视化定义,即可提供可预测的低延迟性能。 即使可视化包含过滤器、搜索约束或时间分桶,只要视图中包含所有必需的维度,materialized view 仍可用于查询加速。这样一来,无需修改可视化定义,也能利用这些视图加速仪表盘、直方图和带过滤条件的图表。

选择 materialized view 的示例

考虑在同一个链路追踪数据源上创建的两个 materialized view:
  • otel_traces_1m,按分钟、ServiceNameStatusCode 分组
  • otel_traces_1m_v2,按分钟、ServiceNameStatusCodeSpanName 分组
第二个视图包含额外的分组键,因此会产生更多的行,并扫描更多的数据。 如果某个可视化请求按服务查看随时间变化的平均耗时,那么这两个视图在技术上都可用。ClickStack 会对每个候选项发出一个 EXPLAIN ESTIMATE 查询,并比较预估的粒度数量,即:
EXPLAIN ESTIMATE
SELECT
    toStartOfHour(Timestamp) AS hour,
    ServiceName,
    avgMerge(avg__Duration) AS avg__Duration
FROM otel_v2.otel_traces_1m
GROUP BY
    hour,
    ServiceName
ORDER BY hour DESC
┌─database─┬─table──────────┬─parts─┬──rows─┬─marks─┐
│ otel_v2  │ otel_traces_1m │     1 │ 49385 │     6 │
└──────────┴────────────────┴───────┴───────┴───────┘

1 row in set. Elapsed: 0.009 sec.
EXPLAIN ESTIMATE
SELECT
    toStartOfHour(Timestamp) AS hour,
    ServiceName,
    avgMerge(avg__Duration) AS avg__Duration
FROM otel_v2.otel_traces_1m_v2
GROUP BY
    hour,
    ServiceName
ORDER BY hour DESC
┌─database─┬─table─────────────┬─parts─┬───rows─┬─marks─┐
│ otel_v2  │ otel_traces_1m_v2 │     1 │ 212519 │    26 │
└──────────┴───────────────────┴───────┴────────┴───────┘

1 row in set. Elapsed: 0.004 sec.
由于 otel_traces_1m 更小、扫描的粒度也更少,因此会被自动选中。 这两个 materialized view 的性能仍然优于直接查询基表,但选择能够满足需求的最小视图,性能最佳。

告警

兼容时,告警查询会自动使用 materialized view。这里同样适用相同的优化逻辑,从而加快告警评估速度。

回填 materialized view

如前所述,增量materialized view 仅包含在视图创建后插入的数据,不会自动回填。这种设计使视图保持轻量、维护成本较低,但也意味着:如果查询需要早于该视图最小时间戳的数据,就无法使用该视图。 在大多数情况下,这是可以接受的。常见的 ClickStack 工作负载都聚焦于最近的数据,例如过去 24 小时,这意味着新创建的视图会在创建一天后变得完全可用。不过,对于跨越更长时间范围的查询,在经过足够长的时间之前,该视图可能仍然无法使用。 在这些情况下,用户可以考虑使用历史数据对 materialized view 进行回填 回填的计算开销可能非常高。在正常运行时,materialized view 会随着数据到达而增量填充,从而将计算成本均匀分摊到一段时间内。 而回填会将这些工作压缩到更短的时间内,显著增加单位时间内的 CPU 和内存使用量。 根据数据集大小和保留窗口,可能需要临时对集群进行扩缩容——无论是纵向扩缩容,还是在 ClickHouse Cloud 中进行横向扩缩容——才能在合理时间内完成回填。 如果没有配置额外资源,回填可能会对生产工作负载造成负面影响,包括查询延迟和摄取吞吐量。对于非常大的数据集或较长的历史时间范围,回填可能并不现实,甚至完全不可行。 总之,回填通常并不值得付出这样的成本和运维风险。只有在历史数据加速至关重要的特殊情况下,才应考虑这样做。如果你决定继续,建议遵循下文概述的受控方法,以在性能、成本和对生产环境的影响之间取得平衡。

回填方式

避免使用 POPULATE除了在已暂停摄取的小型数据集场景外,不建议使用 POPULATE 命令回填 materialized view。该操作符可能会漏掉插入到其源表中的行,因为 materialized view 是在 POPULATE 完成后才创建的。此外,POPULATE 会对全部数据执行处理,在大型数据集上容易受到中断或内存限制的影响。
假设你想回填与下述聚合对应的 materialized view;该聚合按服务名称和状态码分组,计算每分钟的指标:
SELECT
    toStartOfMinute(Timestamp),
    ServiceName,
    StatusCode,
    count() AS count,
    avg(Duration),
    max(Duration),
    quantiles(0.95, 0.99)(Duration)
FROM otel_traces
GROUP BY
    time,
    ServiceName,
    StatusCode
如前所述,增量materialized view 不会自动回填。建议采用以下流程,在安全回填历史数据的同时,保留对新数据的增量处理行为。

使用 INSERT INTO SELECT 直接回填

这种方法最适合较小的数据集相对较轻量的聚合查询:能够在合理时间内完成整个回填,同时不会耗尽集群资源。通常适用于回填查询可在几分钟内完成,最多不超过几小时,且可以接受 CPU 和 I/O 使用量的临时上升。对于更大的数据集或成本更高的聚合,建议改用下面介绍的增量回填或基于块的回填方法。
1
确定视图当前的覆盖范围
在尝试任何回填之前,首先需要确认 materialized view 已经包含了哪些数据。这可以通过查询目标表中的最小时间戳来完成:
SELECT min(Timestamp)
FROM otel_traces_1m;
该时间戳表示此视图能够满足查询的最早时间点。ClickStack 中任何请求早于该时间戳数据的查询,都会回退到基表。
2
判断是否需要回填
在大多数 ClickStack 部署中,查询通常聚焦于最近的数据,例如过去 24 小时。在这种情况下,新创建的视图在创建后不久就能完全投入使用,因此无需回填。如果上一步返回的时间戳对你的使用场景来说已经足够早,就不需要回填。只有在以下情况下才应考虑回填:
  • 查询经常覆盖较长的历史时间范围。
  • 该视图对这些时间范围内的性能至关重要。
  • 数据集规模和聚合成本使回填具备可行性。
3
回填缺失的历史数据
如果需要回填,请基于该视图的查询进行修改,使其只读取早于上述时间戳的数据,从而为 materialized view 的目标表补齐当前最小时间戳之前的历史数据。由于目标表使用的是 AggregatingMergeTree,回填查询必须插入聚合状态,而不是最终值
此查询可能会处理大量数据,因此资源开销较高。运行回填前,务必确认可用的 CPU、内存和 I/O 容量。一个实用技巧是先使用 FORMAT Null 执行该查询,以估算运行时间和资源占用。如果预计该查询本身需要运行很多小时,则不建议采用这种方法。
请注意,下面的查询添加了一个 WHERE 子句,将聚合限制为早于视图中最早时间戳的数据:
INSERT INTO otel_traces_1m
SELECT
    toStartOfMinute(Timestamp) AS Timestamp,
    ServiceName,
    StatusCode,
    count() AS count,
    avgState(Duration) AS avg__Duration,
    maxSimpleState(Duration) AS max__Duration,
    quantilesState(0.95, 0.99)(Duration) AS quantiles__Duration
FROM otel_traces
WHERE Timestamp < (
    SELECT min(Timestamp) FROM otel_traces_1m
)
GROUP BY
    Timestamp,
    ServiceName,
    StatusCode;

使用 Null 表进行增量回填

对于更大的数据集,或资源开销更高的聚合查询,使用单条 INSERT INTO SELECT 直接回填可能并不现实,甚至存在风险。这种情况下,建议采用增量回填。这种方法更接近增量materialized view 的常规工作方式:不是一次聚合整个历史数据集,而是按可控的数据块逐步处理。 这种方法适用于以下情况:
  • 回填查询否则需要运行数小时。
  • 完整聚合的峰值内存占用过高。
  • 你希望在回填期间严格控制 CPU 和内存消耗。
  • 你需要一种更稳健的流程,以便在中断后也能安全重启。
核心思路是使用 Null 表 作为摄取缓冲区。虽然 Null 表本身不存储数据,但挂接到它的 materialized view 仍然会执行,因此可以在数据流经时增量计算聚合状态。
1
为回填创建一个 Null 表
创建一个轻量级的 Null 表,只包含 materialized view 聚合所需的列。这样可以将 I/O 和内存占用降到最低。
CREATE TABLE otel_traces_backfill
(
    Timestamp DateTime64(9),
    ServiceName LowCardinality(String),
    StatusCode LowCardinality(String),
    Duration UInt64
)
ENGINE = Null;
2
将 materialized view 挂接到 Null 表
接下来,在 Null 表上创建一个 materialized view,并让它指向与你的主 materialized view 相同的聚合表。
CREATE MATERIALIZED VIEW otel_traces_1m_mv_backfill
TO otel_traces_1m
AS
SELECT
    toStartOfMinute(Timestamp) AS Timestamp,
    ServiceName,
    StatusCode,
    count() AS count,
    avgState(Duration) AS avg__Duration,
    maxSimpleState(Duration) AS max__Duration,
    quantilesState(0.95, 0.99)(Duration) AS quantiles__Duration
FROM otel_traces_backfill
GROUP BY
    Timestamp,
    ServiceName,
    StatusCode;
当行被插入 Null 表时,这个 materialized view 会以增量方式执行,并以小数据块生成聚合状态。
3
以增量方式回填数据
最后,将历史数据插入 Null 表。materialized view 会按数据块逐步处理这些数据,将聚合状态写入目标表,而不会持久化原始行。
INSERT INTO otel_traces_backfill
SELECT
    Timestamp,
    ServiceName,
    StatusCode,
    Duration
FROM otel_traces
WHERE Timestamp < (
    SELECT min(Timestamp) FROM otel_traces_1m
);
由于数据是增量处理的,因此内存占用会保持在可控且可预测的范围内,整体行为也更接近正常摄取流程。
为进一步提升安全性,可以考虑让回填 materialized view 指向一个临时目标表 (例如 otel_traces_1m_v2) 。回填成功完成后,可以将分区移动到主目标表,例如 ALTER TABLE otel_traces_1m_v2 MOVE PARTITION '2026-01-02' TO otel_traces_1m。这样一来,如果回填因中断或资源限制而失败,就能更方便地恢复。
有关此流程调优的更多细节,包括如何提升插入性能,以及如何减少并控制资源消耗,请参阅 “Backfilling.”

建议

以下建议概述了在 ClickStack 中设计和运维 materialized views 的最佳实践。遵循这些建议有助于确保 materialized views 高效、可预测,并具备良好的成本效益。

粒度选择与对齐

只有当图表或告警的粒度是视图粒度的精确整数倍时,才会使用 materialized view。具体如何确定该粒度,取决于图表类型:
  • 时间图表 (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 小时粒度,它们与大多数图表和告警配置都能良好适配。
更高的粒度 (例如 1 小时) 会生成更小的视图,并降低存储开销;更低的粒度 (例如 1 分钟) 则能为细粒度分析提供更大的灵活性。请选择能够支持关键工作流的最小粒度。

限制并整合 materialized view

每个 materialized view 都会带来额外的插入时开销,并增加分片与合并压力。 建议遵循以下准则:
  • 每个 source 最多不超过 20 个 materialized view
  • 通常以 10 个左右的 materialized view 为最佳。
  • 当多个可视化基于相同维度时,应将其整合到单个视图中。
在可能的情况下,应通过同一个 materialized view 计算多个指标,并支撑多个图表。

谨慎选择维度

只纳入那些经常用于分组或过滤的维度:
  • 每增加一个分组列,视图的体积都会增大。
  • 需要在查询灵活性与存储、插入时成本之间做好权衡。
  • 如果对视图中不存在的列使用过滤器,ClickStack 会回退到源表。
提示一种常见且几乎总是有用的基础做法是:创建一个按 服务名 分组、并包含 count 指标的 materialized view,这样可以在搜索和仪表盘中快速生成直方图并提供服务级概览。

聚合列的命名约定

materialized view 的聚合列必须遵循严格的命名约定,才能启用自动推断:
  • 模式:<aggFn>__<sourceColumn>
  • 示例:
    • avg__Duration
    • max__Duration
    • count__ 用于行计数
ClickStack 依赖这一约定,才能将查询正确映射到 materialized view 列。

分位数与草图选择

不同的分位数函数在性能和存储方面各有特点:
  • quantiles 会在磁盘上生成更大的草图,但在写入时计算开销更低。
  • quantileTDigest 在写入时计算开销更高,但生成的草图更小,因此视图查询通常也会更快。
你也可以指定草图大小 (例如,对这两个函数都在写入时使用 quantile(0.5)) 。之后仍可基于生成的草图查询其他分位数值,例如 quantile(0.95)。建议通过实验找到最适合你的工作负载的平衡点。

持续验证效果

始终要验证 materialized view 是否确实带来了实际收益:
  • 通过 UI 中的加速指示器确认其使用情况。
  • 比较启用该视图前后的查询性能。
  • 监控资源使用情况和合并行为。
应将 materialized view 视为一种性能优化手段;随着查询模式的演变,需要定期审查和调整。

高级配置

对于更复杂的工作负载,可以使用多个 materialized view 来适配不同的访问模式。示例包括:
  • 高分辨率的近期数据,配合较粗粒度的历史视图
  • 用于概览的服务级视图,以及用于深度诊断的端点级视图
如果有针对性地应用,这些模式可以显著提升性能,但只有在验证过更简单的配置后,才应引入。 遵循这些建议,有助于确保 materialized view 持续保持高效、易于维护,并与 ClickStack 的执行模型一致。

局限性

常见不兼容原因

如果出现以下任一情况,将不会使用 materialized view:
  • 查询时间范围 查询时间范围的起始时间早于 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 只用在确实能带来实际收益的场景中,并避免采用那些会在无提示的情况下回退到更慢的源表查询的配置。

故障排查

materialized view 未生效

检查 1:日期范围
  • 打开优化弹窗,查看是否显示“Date range not supported.”
  • 确保查询的日期范围晚于 materialized view 的最小日期。
  • 如果 materialized view 包含全部历史数据,请移除最小日期。
检查 2:粒度
  • 确认图表粒度是 MV 粒度的整数倍。
  • 尝试将图表设置为“Auto”,或手动选择兼容的粒度。
检查 3:聚合
  • 检查图表使用的聚合是否包含在 MV 中。
  • 在优化弹窗中查看“Available aggregated columns”。
检查 4:维度
  • 确保 group by 的列包含在 MV 的维度列中。
  • 在优化弹窗中查看“Available group/filter columns”。

materialized view 查询缓慢

问题 1:materialized view 粒度过细
  • 由于粒度太细 (例如 1 秒) ,MV 的行数过多。
  • 解决方案:创建一个粒度更粗的 MV (例如 1 分钟或 1 小时) 。
问题 2:维度过多
  • 由于维度列过多,MV 的基数过高。
  • 解决方案:将维度列精简为最常用的几个。
问题 3:存在多个行数较多的 MV
  • 系统会对每个 MV 运行 EXPLAIN
  • 解决方案:删除很少使用或总是被跳过的 MV。

配置错误

错误:“At least one aggregated column is required”
  • 在 MV 配置中至少添加一个聚合列。
错误:“Source column is required for non-count aggregations”
  • 请指定要聚合的列 (只有 count 可以省略源列) 。
错误:“Invalid granularity format”
  • 请使用下拉列表中的预设粒度之一。
  • 格式必须是有效的 SQL 时间间隔 (例如 1 hour,而不是 1 h) 。
最后修改于 2026年6月10日