网络运维中心(NOC) 本实验已运行
现在哪里拥挤、哪里信号弱、哪里刚出了故障
图由 JavaScript 绘制。
- 用户最多的小区
- 信号最弱的小区
- 刚刚信号下降的区域
- 测量条数
详情
在一个屏幕上画出过去 1 小时用户最多的小区(蓝)、信号最弱的小区(红),以及刚刚 1 分钟内信号突然下降的区域(黄)。
做法:只把屏幕需要的答案每分钟预先算好,所以屏幕立即打开。小区边界由 ClickHouse 的 H3 函数返回。
案例研究 · 运营商 OSS 数据湖
从一块很慢的 NOC 屏幕开始,最终把数据汇集到一处,由多个团队和 AI 智能体共同使用。
这是一个合成场景:以虚构的运营商为对象,按常见的电信 OSS 做法(3GPP PM/CM/trace、奖章分层)构建;名称为虚构,数字为本实验实测。
用户最多的 10 个小区
信号最弱的 10 个小区
一家运营商的 OSS 团队在 NOC 屏幕上显示“过去 1 小时用户最多的小区和信号最弱的小区”。终端(UE:手机、IoT 模块、车辆、路由器等)的测量每次都从源头重新聚合,屏幕因此很慢。
运营商的系统大致分为两类。BSS 管客户和钱(开户、计费、账单),OSS 管网络本身(设备、故障、质量)。这个数据湖属于 OSS 一侧。
不再把数据复制到每个系统,而是在一个数据层之上叠加语义和访问控制,让人和 AI 智能体使用同一份数据。以下各层根据公开资料整理。
Managed Postgres 使用方的使用模式和工作记录(订阅、API 密钥、水位、工单、智能体的操作记录与审批)属于事务数据,放在 ClickHouse 的 Managed Postgres 中,并通过 pg_clickhouse 与数据湖连接。 演示中没有
典型电信 OSS 的整个数据湖。实线部分在本实验中实际运行,虚线部分遵循同样的模式,只在图中展示。
终端测量轨迹 (UE)
配置 · 资源 (CM)
RAN · 核心网性能计数器 (PM)
客户投诉 (VoC)
Kafka本实验中由生成器代替
原样保存实际中每个来源和设备软件版本各一张表
类型 · 清洗 · 哈希
从资源数据补上区域名称
小区 × 1 分钟
小区 × 10 分钟
区域 × 1 分钟
时间 × 地理两个维度
现成答案(每分钟)
REST · HTTP
订阅 · 告警
批量导出
NOC 屏幕
阈值告警
仪表盘
AI 智能体(MCP,只读)
数据目录
每个使用方单独的用户和限额
Schema 契约(Git): 列名、说明和类型在 Git 的一份契约文件中定义,各阶段的 schema 和 DDL 都由它自动生成。
控制平面 API: 主题、流作业、连接器、schema 和表都通过 API 而不是手工来声明、部署和观测。人和 AI 智能体使用同一个 API。
很多团队以各自的方式使用同一份数据。分为四组:查看、取用、运营平台、AI。
网络运维中心(NOC) 本实验已运行
图由 JavaScript 绘制。
在一个屏幕上画出过去 1 小时用户最多的小区(蓝)、信号最弱的小区(红),以及刚刚 1 分钟内信号突然下降的区域(黄)。
做法:只把屏幕需要的答案每分钟预先算好,所以屏幕立即打开。小区边界由 ClickHouse 的 H3 函数返回。
运营仪表盘 · BI 本实验已运行
图由 JavaScript 绘制。
仪表盘工具每分钟发出同一个查询并画线。下面是过去 1 小时各城市的弱信号比例;突起的点表示那一分钟某个区域发生了故障。
做法:仪表盘工具直接连接 ClickHouse。像用户数这样需要去重的值,也通过合并 1 分钟汇总中保存的状态来精确计算。
-- ---------------------------------------------------------------- 2 · dashboard
-- One point per city per minute for a time-series panel (Grafana, Superset, ...):
-- distinct users (uniq states merged across the city's cells) and weak-signal share.
CREATE OR REPLACE VIEW oss_lake.uc_dashboard AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
k.bucket AS minute,
g.city AS city,
uniqMerge(k.users_st) AS users,
sum(k.samples_n) AS samples,
round(100 * sum(k.poor_n) / sum(k.rsrp_n), 2) AS poor_pct
FROM oss_lake.cell_kpi_1m AS k
INNER JOIN oss_lake.grid AS g ON g.h3 = k.cell
WHERE k.bucket >= t - INTERVAL 1 HOUR AND k.bucket < t
GROUP BY minute, city
ORDER BY minute, city;质量分析师 本实验已运行
| 城市 | 区域 | 弱信号比例 | |
|---|---|---|---|
| A 市 | 市中心 | 0% | |
| A 市 | 中间 | 2.6% | |
| A 市 | 郊区 | 23.1% | |
| B 市 | 市中心 | 0% | |
| B 市 | 中间 | 2.5% | |
| B 市 | 郊区 | 23.1% | |
| C 市 | 市中心 | 0.1% | |
| C 市 | 中间 | 2.6% | |
| C 市 | 郊区 | 23.1% |
弱信号多在市中心还是郊区?这是没人做过报表的问题,当场用 SQL 来问。
做法:直接查询 1 分钟汇总并与区域信息关联。这类问题在基于文件的数据湖上很难达到交互式的响应时间。
-- ---------------------------------------------------------------- 1 · ad-hoc analysis
-- A quality analyst's question: where is weak signal concentrated, downtown or at the edge?
CREATE OR REPLACE VIEW oss_lake.uc_analysis AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
g.city AS city,
multiIf(g.ring <= 10, 'downtown', g.ring <= 30, 'middle', 'edge') AS zone,
uniqExact(k.cell) AS cells,
sum(k.samples_n) AS samples,
round(sum(k.rsrp_sum) / sum(k.rsrp_n), 1) AS avg_rsrp,
round(100 * sum(k.poor_n) / sum(k.rsrp_n), 1) AS poor_pct
FROM oss_lake.cell_kpi_1m AS k
INNER JOIN oss_lake.grid AS g ON g.h3 = k.cell
WHERE k.bucket >= t - INTERVAL 1 HOUR AND k.bucket < t
GROUP BY city, zone
ORDER BY city, indexOf(['downtown', 'middle', 'edge'], zone);值班人员 · 下游系统 本实验已运行
某小区最近 1 分钟的信号比它自己过去 1 小时明显下降,就形成告警。小区按区域合并成人能读懂的一行。下面是本实验中实际产生的告警。
做法:通常在新数据就绪时通过消息队列发出“现在来取”的事件,订阅方随即取走。也可以把结果直接推送到 Kafka 主题。
在生产中,同样的行通过 URL 表引擎推送到 webhook。本实验不使用:无法访问的地址会让源 INSERT 失败。
示例,本实验不运行(需要 Kafka broker)。
-- Example, not run in this lab (it needs a Kafka broker): publish each minute's alerts to a
-- topic that an on-call app or chat bot subscribes to. Inserting into a Kafka-engine table
-- produces messages; a refreshable MV with APPEND does that once a minute.
CREATE TABLE oss_lake.alerts_topic
(
area String, city UInt8, minute DateTime, cells UInt64, drop_db Float64, rsrp_now Float64
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'broker:9092',
kafka_topic_list = 'noc-alerts',
kafka_group_name = 'oss-lake',
kafka_format = 'JSONEachRow';
CREATE MATERIALIZED VIEW oss_lake.alerts_publish
REFRESH EVERY 1 MINUTE APPEND TO oss_lake.alerts_topic AS
SELECT area, city, minute, cells, drop_db, rsrp_now FROM oss_lake.uc_alert_areas;业务应用 · 服务 · AI 智能体 本实验已运行
curl 'http://localhost:18123/?user=oss_reader' \
--data-binary 'SELECT area, city, minute, cells, drop_db, rsrp_now FROM oss_lake.uc_alert_areas FORMAT JSONEachRow'{"area":"8730c19a5ffffff","city":2,"minute":"2026-10-18 10:20:00","cells":48,"drop_db":21.2,"rsrp_now":-114.1}
使用方用一个 HTTP 请求取走所需数据。通常由 ClickHouse 前面的 API 服务负责认证和目录,并允许选择原始格式或行业标准模型格式。下面是直接发给 ClickHouse HTTP 接口的请求及响应的开头。
做法:ClickHouse 通过 HTTP 接收查询,结果边生成边逐行流式返回。无需驱动。
服务层 · 业务应用(未来扩展) 构成示例
服务层总伴随着一些小的业务数据:订阅列表、API 密钥、每个使用方读到了哪里(水位),通常放在 Postgres。使用 ClickHouse 的 Managed Postgres,这些业务数据和数据湖就在同一个云中,借助 pg_clickhouse 扩展,用普通的 Postgres SQL 就能把数据湖的告警与负责团队、未关闭工单关联起来。
依据:同样的方式已在 clickhouse-managed-postgres-hols 仓库的 pg_clickhouse 实验中验证(2026-10-01)。本实验未包含。
扩展示例,本实验不运行。主机和密码为占位符。
-- Expansion example, not run in this lab. Runs on (Managed) Postgres, where an operations app
-- keeps tickets and area owners; pg_clickhouse reads the lake's alert view in place.
-- Same statements as clickhouse-managed-postgres-hols extensions/pg-clickhouse-lab
-- (verified there 2026-10-01 on pg_clickhouse 0.11.0, PostgreSQL 18.6); host and password are placeholders.
CREATE EXTENSION IF NOT EXISTS pg_clickhouse;
CREATE SERVER lake FOREIGN DATA WRAPPER clickhouse_fdw
OPTIONS (driver 'binary', host '<clickhouse host>', port '9000', dbname 'oss_lake');
CREATE USER MAPPING FOR CURRENT_USER SERVER lake
OPTIONS (user 'oss_reader', password '<password>');
CREATE SCHEMA lake;
IMPORT FOREIGN SCHEMA oss_lake LIMIT TO (uc_alert_areas) FROM SERVER lake INTO lake;
-- who to call: this minute's alert areas with their owner team and open tickets
SELECT a.area, a.cells, a.drop_db, o.team, count(t.id) AS open_tickets
FROM lake.uc_alert_areas AS a
JOIN ops.area_owner AS o ON o.area = a.area
LEFT JOIN ops.ticket AS t ON t.area = a.area AND t.status = 'open'
GROUP BY a.area, a.cells, a.drop_db, o.team
ORDER BY a.cells DESC;AI 智能体 构成示例
运维人员问“过去 30 分钟 B 市哪些区域信号下降了?”,智能体查看目录和 schema 说明,生成 SQL 去问数据湖。schema 契约里的说明直接成为智能体的上下文。
做法:智能体通过 MCP 或 HTTP 连接 ClickHouse,以只读用户查询。即使问题无法预测,汇总表也能让回答保持快速。
数据平台团队 构成示例
每个阶段各自定义 schema 时,名称和含义会逐渐走样,每次修改都要经过多个团队。常见的做法是以 Git 中的一份数据契约文件为准,在 CI 中自动生成消息 schema、数据湖表定义和 ClickHouse DDL。
做法:ClickHouse 直接接收生成的 DDL。表和 MV 都是 SQL,像代码一样评审和部署。
数据质量负责人 本实验已运行
没有小区或用户的测量在整理阶段丢弃,没有信号值的测量保留为“缺失”而不是 0。每个阶段丢弃和保留了什么,可以用行数核对。
做法:清洗规则集中在物化视图的 SQL 一处,规则和结果一起评审。原始数据原样保留,规则变化时可以重建。
平台运维(SRE) 构成示例
采集器、主题、流作业和连接器一多,看清哪里变慢、哪里失败本身就成了工作。它们的日志、指标和链路也可以汇集到同一个 ClickHouse。
做法:用基于 ClickHouse 的可观测性栈 ClickStack 接收 OpenTelemetry 数据,用于搜索和仪表盘。
分析师 · 数据科学家 构成示例
长期历史放在开放表格式的数据湖(如 Iceberg),最近的数据在 ClickHouse 中快速使用。用一个查询把两者连起来,重处理和长期趋势分析都更容易。
做法:ClickHouse 可以直接读取对象存储上的 Iceberg 表,无需搬迁数据湖即可与黄金层表关联。
数据工程师 · 其他分析平台 构成示例
当需要大块数据,例如训练模型或导入其他系统时,按时间段一次性导出。
做法:ClickHouse 可以把查询结果直接以文件写入对象存储,或通过 HTTP 流式返回。这是即时查询之后常见的下一种服务方式。
AI 开发团队 构成示例
只有记录并评估智能体生成的 SQL 和答案是否正确、成本和延迟如何,才能放心使用。
做法:用以 ClickHouse 为存储的 Langfuse,在同一平台上查看智能体的链路、评估和成本。
在这个 PoC 中,生产环境里的消息总线和流处理阶段由 ClickHouse 内的物化视图代替,下面每项功能都用本实验的数据逐一核实。核验以检查的形式写在实验的 SQL 中,每次 tools/hol run 都会重新核验。 其中“刷新期间仍有 20 行”和“写入被拒绝”两项为人工核验(2026-10-18)。
原样保存
不做修改直接存储;规则变化时从这里重建。
h3 '8930e1d8c0bffff' · imsi '/ZM4BSO2…'
整理
统一类型,把用户号码换成哈希;不能用的行丢弃。
h3 617853093946327039 · imsi → 624750…
1 分钟汇总
每个小区每分钟折成一行;任何时间段都能再合并。
一个小区一分钟 441 行 → 1 行
现成的答案
只把屏幕要读的答案每分钟重新算好。
NOC 地图需要的 20 行
小区 × 1 分钟
区域 × 1 分钟
小区 × 10 分钟承接 1 分钟汇总
| 区域 | 弱信号比例 | |
|---|---|---|
| C North-West | 13.3% | |
| B South-West | 13.3% | |
| A North-East | 13.1% | |
| B South-East | 12.5% | |
| A West | 12.5% |
四个阶段是数据库 oss_lake 中的 ue_trace_raw、ue_trace、cell_kpi_1m 和 cell_rank_now 表。前两段由物化视图连接,最后一段由每分钟运行的可刷新物化视图连接。用例是 04-usecases.sql 中的视图。
| 原样 | 整理 | 变化 | |
|---|---|---|---|
event_ts2026-10-18 10:20:00.011 | event_ts2026-10-18 10:20:00.011 | 不变 | |
h3_res098930e1d8c0bffff | h3617853093946327039 | 字符串 → 数字(stringToH3) | |
imsi/ZM4BSO2N5g8Ovcpun3PtidkglHPB22vffMxteYPT5w= | imsi_hash6247503676043346188 | 密文 → 哈希(cityHash64),原号码不进入数据湖 | |
rsrp-69 | rsrp-69 | Float32;缺失时保持 NULL | |
rsrq-4.5 | rsrq-4.5 | Float32 | |
sinr20 | sinr20 | Float32 | |
cqi13 | cqi13 | 字符串 → 数字 | |
pci290 | — | 不使用,丢弃 | |
probeprobe-a | — | 不使用,丢弃 | |
— | region1City A | 从资源数据补上的区域名称 | |
— | region2A Central | 从资源数据补上的区域名称 |
bucket | 2026-10-18 10:20:00 |
|---|---|
cell | 617853093946327039 |
users_st | uniqState(…) → 133 |
samples_n | 441 |
rsrp_n | 399 |
rsrp_sum | -31229 |
poor_n | 0 |
| # | cell | users |
|---|---|---|
| 1 | 8930e1d8c0bffff | 5,475 |
| 2 | 8930c1903cbffff | 5,449 |
| 3 | 8930c1619d7ffff | 5,385 |
| 4 | 8930e1d8c03ffff | 2,596 |
| 5 | 8930c190227ffff | 2,565 |
| 6 | 8930c16198bffff | 2,545 |
| 7 | 8930e1d8c0fffff | 2,016 |
| 8 | 8930c16199bffff | 1,969 |
| 9 | 8930c190237ffff | 1,910 |
| 10 | 8930c190353ffff | 1,777 |
| # | cell | avg_rsrp |
|---|---|---|
| 1 | 8930c16ad73ffff | -117.9 dBm |
| 2 | 8930e1c302fffff | -117.5 dBm |
| 3 | 8930c16ad3bffff | -117.5 dBm |
| 4 | 8930c19221bffff | -117.4 dBm |
| 5 | 8930c16ad03ffff | -117.4 dBm |
| 6 | 8930c16ad07ffff | -117.4 dBm |
| 7 | 8930e1c3397ffff | -117.3 dBm |
| 8 | 8930c1922cfffff | -117.2 dBm |
| 9 | 8930e1c339bffff | -117.2 dBm |
| 10 | 8930c16ad0fffff | -117.2 dBm |
-- ---------------------------------------------------------------- the inventory (CM)
-- Every cell gets a site and a fictional district: 'Central' within 10 rings, otherwise one
-- of six 60-degree sectors around the city centre. Coverage holes are known weak spots.
INSERT INTO oss_lake.cell_inventory
WITH
[(37.5665, 126.9780), (35.1796, 129.0756), (35.8714, 128.6014)] AS centres,
['North-East', 'East', 'South-East', 'South-West', 'West', 'North-West'] AS sectors,
centres[city + 1] AS c,
h3ToGeo(h3) AS p,
(degrees(atan2((p.2 - c.2) * cos(radians(c.1)), p.1 - c.1)) + 360) % 360 AS bearing
SELECT
h3 AS cell,
concat(['A', 'B', 'C'][city + 1], '-', leftPad(toString(intDiv(idx, 7)), 4, '0')) AS site_id,
['City A', 'City B', 'City C'][city + 1] AS region1,
concat(['A', 'B', 'C'][city + 1], ' ',
if(ring <= 10, 'Central', sectors[1 + toUInt8(floor(bearing / 60)) % 6])) AS region2,
is_hole AS known_weak
FROM oss_lake.grid;
-- Rows without a cell or a subscriber cannot be placed or counted: silver drops them.
CREATE MATERIALIZED VIEW oss_lake.ue_trace_mv TO oss_lake.ue_trace AS
SELECT
event_ts,
stringToH3(assumeNotNull(h3_res09)) AS h3,
cityHash64(assumeNotNull(imsi)) AS imsi_hash,
CAST(rsrp AS Nullable(Float32)) AS rsrp,
toFloat32(ifNull(rsrq, 0)) AS rsrq,
toFloat32(ifNull(sinr, 0)) AS sinr,
toUInt8OrZero(ifNull(cqi, '')) AS cqi,
-- a measurement gets a place name from the inventory
dictGet('oss_lake.cell_dict', 'region1', h3) AS region1,
dictGet('oss_lake.cell_dict', 'region2', h3) AS region2
FROM oss_lake.ue_trace_raw
WHERE h3_res09 IS NOT NULL AND imsi IS NOT NULL;
CREATE MATERIALIZED VIEW oss_lake.cell_kpi_1m_mv TO oss_lake.cell_kpi_1m AS
SELECT
toStartOfMinute(event_ts) AS bucket,
h3 AS cell,
uniqState(imsi_hash) AS users_st,
count() AS samples_n,
count(rsrp) AS rsrp_n,
sum(ifNull(rsrp, 0)) AS rsrp_sum,
countIf(rsrp < -100) AS poor_n
FROM oss_lake.ue_trace
GROUP BY bucket, cell;
CREATE MATERIALIZED VIEW oss_lake.region_kpi_1m_mv TO oss_lake.region_kpi_1m AS
SELECT
toStartOfMinute(event_ts) AS bucket,
region1, region2,
uniqState(imsi_hash) AS users_st,
count() AS samples_n,
count(rsrp) AS rsrp_n,
sum(ifNull(rsrp, 0)) AS rsrp_sum,
countIf(rsrp < -100) AS poor_n
FROM oss_lake.ue_trace
GROUP BY bucket, region1, region2;
[[sql:gold10]]
-- No APPEND: every refresh replaces the 20 rows.
CREATE MATERIALIZED VIEW oss_lake.cell_rank_rmv
REFRESH EVERY 1 MINUTE TO oss_lake.cell_rank_now AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
lr.1 AS list, lr.2 AS rnk, cell, users, samples, avg_rsrp, poor_pct, t AS window_end
FROM
(
SELECT
cell,
uniqMerge(users_st) AS users,
sum(samples_n) AS samples,
round(sum(rsrp_sum) / sum(rsrp_n), 1) AS avg_rsrp,
round(100 * sum(poor_n) / sum(rsrp_n), 1) AS poor_pct,
sum(rsrp_n) >= 30 AS eligible,
row_number() OVER (ORDER BY users DESC, cell) AS r_users,
row_number() OVER (PARTITION BY eligible ORDER BY sum(rsrp_sum) / sum(rsrp_n), cell) AS r_signal
FROM oss_lake.cell_kpi_1m
WHERE bucket >= t - INTERVAL 1 HOUR AND bucket < t
GROUP BY cell
)
ARRAY JOIN [('users', r_users), ('signal', if(eligible, r_signal, 999))] AS lr
WHERE lr.2 <= 10;
-- ---------------------------------------------------------------- 3 · alerts
-- Cells whose signal in the latest minute is at least 12 dB below their own last hour.
-- Subscribers (an on-call app, a chat bot) pick these up every minute; see 4 and
-- subscribe-kafka.sql for how they are delivered.
CREATE OR REPLACE VIEW oss_lake.uc_alert_cells AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
cell, city, area, minute, rsrp_now, rsrp_hour,
round(rsrp_hour - rsrp_now, 1) AS drop_db,
samples_now
FROM
(
SELECT
k.cell AS cell,
any(g.city) AS city,
h3ToParent(k.cell, 7) AS area,
t - INTERVAL 1 MINUTE AS minute,
sumIf(k.rsrp_sum, k.bucket = minute) / sumIf(k.rsrp_n, k.bucket = minute) AS rsrp_now,
sum(k.rsrp_sum) / sum(k.rsrp_n) AS rsrp_hour,
sumIf(k.rsrp_n, k.bucket = minute) AS samples_now
FROM oss_lake.cell_kpi_1m AS k
INNER JOIN oss_lake.grid AS g ON g.h3 = k.cell
WHERE k.bucket >= t - INTERVAL 1 HOUR AND k.bucket < t
GROUP BY cell
)
WHERE samples_now >= 3 AND rsrp_hour - rsrp_now >= 12;
把同一个 NOC 问题分别发给四个阶段,返回的 20 行完全相同(由 03-queries.sql 检查)。每个阶段的行数和读取量都在减少。
测量于 2026-10-18 10:22 UTC,ClickHouse 26.9.13.15,Apple M4 Pro (macOS arm64),容器 12 个 CPU · 内存 7.75 GiB。预热 1 次后 5 次运行的中位数,服务器端耗时,查询缓存关闭。仅供参考。
构建时发现的一个陷阱。 聚合状态的类型必须与源列是否 Nullable 一致。不一致的 MV 在 CREATE 时就被拒绝;但如果之后用 ALTER 把源列改成 Nullable,此后的 INSERT 全部失败,而源表照样增长(README 中有复现记录)。
同一个 NOC 查询分别写在青铜层和白银层上。标出的 NULL 检查、类型转换和哈希在白银层都消失了。
SELECT
stringToH3(assumeNotNull(h3_res09)) AS cell,
uniq(cityHash64(assumeNotNull(imsi))) AS users,
count() AS samples,
round(avg(rsrp), 1) AS avg_rsrp,
round(100 * countIf(rsrp < -100) / count(rsrp), 1) AS poor_pct,
count(rsrp) >= 30 AS eligible,
row_number() OVER (ORDER BY users DESC, cell) AS r_users,
row_number() OVER (PARTITION BY eligible ORDER BY avg(rsrp), cell) AS r_signal
FROM oss_lake.ue_trace_raw
WHERE event_ts >= t - INTERVAL 1 HOUR AND event_ts < t
AND h3_res09 IS NOT NULL AND imsi IS NOT NULL
GROUP BY cellSELECT
h3 AS cell,
uniq(imsi_hash) AS users,
count() AS samples,
round(avg(rsrp), 1) AS avg_rsrp,
round(100 * countIf(rsrp < -100) / count(rsrp), 1) AS poor_pct,
count(rsrp) >= 30 AS eligible,
row_number() OVER (ORDER BY users DESC, cell) AS r_users,
row_number() OVER (PARTITION BY eligible ORDER BY avg(rsrp), cell) AS r_signal
FROM oss_lake.ue_trace
WHERE event_ts >= t - INTERVAL 1 HOUR AND event_ts < t
GROUP BY cell与其指望换类型来变小,不如选好用户数放在哪个粒度(1 分钟、10 分钟)。
单个状态的大小:按输入类型、按不同值数量
本实验中全部用户数状态:1 分钟粒度与 10 分钟粒度
10 分钟汇总由一个承接 1 分钟汇总新行的 MV 填充,原始数据只聚合一次。用带 APPEND 的可刷新 MV 时,若窗口比刷新间隔宽,数据会被插入两次。
分桶和 TTL 会偏移 9 小时。请用明确时区的类型保存时刻。
09:00≠UTC 00:00同一时刻,只是标签不同仪表盘、通过 MCP 的 AI 智能体、数据目录和 Kafka Connect 批量写入,各自以不同方式施加负载。
测量:2026-10-18,ClickHouse 26.9.13.15,基于本实验的数据与 05-lessons.sql。
在实时演示中每点一次“发送下一分钟”,一分钟的测量就会穿过四个阶段,NOC 地图和告警随之刷新。
cd usecase/oss-network-quality-lake
ui/up.sh
在本地运行只需要 Docker 和 Python 3。本站的演示回放在本地录制的几分钟。