告警出来了,站点名丢了:KWDB 3.2.2 流计算 + 稀疏表上机实测
本文所有命令和输出都来自同一台 openEuler 机器上的 KWDB 3.2.2 单机容器,没有修饰,也没有重跑到「看起来正常」为止。跑通的部分我贴输出,跑不通的部分我贴报错,丢了数据的部分我贴对比。
先说结论
我把 KWDB 3.2.2 的两个新特性跑了一遍,结论分两半。
跑得通的:一张时序表装下三种型号的设备,在线加 11 个测点列,每次 2 到 97 毫秒,历史数据一行不回填;流计算建起来、启动、插一条超阈值的数据,告警几秒后出现在告警表里。
跑不通的:告警表里 cabinet_no 每次都对,station 恒为 NULL。源表里 station 明明写着「A站」。这是静默丢数据,不报错、不告警、日志里也没有。
后面这一条是全文的重点。它不是我用错,我在第十节给了最小复现。
一、环境
| 项 | 值 |
|---|---|
| 数据库 | KaiwuDB 3.2.2(x86_64-pc-linux-gnu, built 2026/08/05) |
| 部署方式 | Docker 单机容器 kwdb,镜像 kwdb/kwdb:3.2.2 |
| 宿主系统 | openEuler |
| 时序库 | ess(储能柜/设备指标) |
| 关系库 | ess_meta(设备台账) |
| 数据量 | 十来行,够验证功能,不够谈性能 |
口径声明:本文所有毫秒数是单机、空表、十来行数据下的值,只用来说明功能走通了,任何一条都不能拿去做性能结论。延迟那三秒是真实端到端,可以引用。
二、一张表装三种型号:先加列
场景是这样:第三代设备 2026 年投产,比老设备多报 9 个测点。老设备还在跑,历史数据不能动。
给时序表加 11 个测点列,前三条:
ALTER TABLE cabinet_metrics ADD COLUMN IF NOT EXISTS current_a FLOAT8;
ALTER TABLE cabinet_metrics ADD COLUMN IF NOT EXISTS voltage_v FLOAT8;
ALTER TABLE cabinet_metrics ADD COLUMN IF NOT EXISTS power_w FLOAT8;
ALTER TABLE
Time: 2.036622ms
ALTER TABLE
Time: 3.015442ms
ALTER TABLE
Time: 3.069107ms
后八条(含一个 BOOL 类型的 door_open):
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS vibration_mm FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS noise_db FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS humidity_pct FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS door_open BOOL;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS filter_status INT;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS fault_code INT;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS uptime_hours FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS efficiency_pct FLOAT8;
ALTER TABLE
Time: 86.151927ms
ALTER TABLE
Time: 95.28576ms
ALTER TABLE
Time: 94.005496ms
ALTER TABLE
Time: 97.085846ms
ALTER TABLE
Time: 90.800588ms
ALTER TABLE
Time: 94.764508ms
ALTER TABLE
Time: 93.205749ms
ALTER TABLE
Time: 95.806308ms
两点值得记下来。
第一,BOOL 类型时序表支持,door_open BOOL 建列通过,后面插 false 也通过。
第二,前三条约 2 到 3 毫秒,后八条约 86 到 97 毫秒,差了三十倍。我第一反应是列数变多了,但 3 列到 11 列翻不了三十倍,这个差异我没查明白,就不猜了。有知道的朋友留言告诉我。
不管哪种,都是毫秒级,加列期间查询照常,没有锁表的迹象。
三、三种型号写进去
时序表里 INSERT ... VALUES 不支持 now() - INTERVAL 这类表达式(第一篇踩过),所以走 INSERT ... SELECT。UNION ALL 多行写法这次通过了。
型号 A(老设备,只有温度和 SOC):
INSERT INTO ess.cabinet_metrics
(k_timestamp, cabinet_no, station, cell_temp_max, cell_temp_avg, soc)
SELECT now() - INTERVAL '10 minutes', 101, 'A站', 28.2, 27.5, 88.5
UNION ALL
SELECT now() - INTERVAL '5 minutes', 101, 'A站', 29.1, 28.3, 88.4;
INSERT 2
Time: 2.731047ms
型号 B(多电流电压功率):
INSERT 2
Time: 4.408329ms
型号 C(12 个测点全报,含 BOOL 的 door_open = false):
INSERT 2
Time: 2.593259ms
三种写入耗时都在 2 到 4 毫秒,测点从 5 个涨到 17 个,耗时没有明显变化。
四、查出来是什么样
用 CASE 给设备分型号:
SELECT cabinet_no, k_timestamp, cell_temp_max, current_a, vibration_mm,
CASE WHEN current_a IS NULL AND vibration_mm IS NULL THEN '型号A-老设备'
WHEN current_a IS NOT NULL AND vibration_mm IS NULL THEN '型号B-中代设备'
WHEN vibration_mm IS NOT NULL THEN '型号C-新设备'
END AS device_type
FROM ess.cabinet_metrics
ORDER BY cabinet_no, k_timestamp DESC;
cabinet_no | k_timestamp | cell_temp_max | current_a | vibration_mm | device_type
-------------+-------------------------------+---------------+-----------+--------------+-----------------
1 | 2026-10-31 01:52:55.881+00:00 | 30.8 | NULL | NULL | 型号A-老设备
1 | 2026-10-31 01:47:55.881+00:00 | 30.5 | NULL | NULL | 型号A-老设备
1 | 2026-10-26 22:30:00+00:00 | 28.7 | NULL | NULL | 型号A-老设备
1 | 2026-10-26 22:25:00+00:00 | 30.1 | NULL | NULL | 型号A-老设备
1 | 2026-10-26 22:20:00+00:00 | 29.2 | NULL | NULL | 型号A-老设备
1 | 2026-10-26 22:15:00+00:00 | NULL | NULL | NULL | 型号A-老设备
2 | 2026-10-31 01:57:55.881+00:00 | 31.2 | NULL | NULL | 型号A-老设备
2 | 2026-10-26 22:20:00+00:00 | 29 | NULL | NULL | 型号A-老设备
2 | 2026-10-26 22:15:00+00:00 | 28.5 | NULL | NULL | 型号A-老设备
3 | 2026-10-31 02:02:55.881+00:00 | 32 | NULL | NULL | 型号A-老设备
101 | 2026-10-31 03:24:54.926+00:00 | 29.1 | NULL | NULL | 型号A-老设备
101 | 2026-10-31 03:19:54.926+00:00 | 28.2 | NULL | NULL | 型号A-老设备
201 | 2026-10-31 03:27:45.811+00:00 | 32.1 | 15.5 | NULL | 型号B-中代设备
201 | 2026-10-31 03:22:45.811+00:00 | 31.5 | 15.3 | NULL | 型号B-中代设备
301 | 2026-10-31 03:29:44.201+00:00 | 30.2 | 18.5 | 0.18 | 型号C-新设备
301 | 2026-10-31 03:25:44.201+00:00 | 29.8 | 18.2 | 0.15 | 型号C-新设备
(16 rows)
Time: 2.030573ms
16 行,三种形态在一张表里并存:1、2、3 号是第一篇留下的老数据,新加的 11 列全是 NULL;101 是型号 A;201 有电流;301 连振动都有。
这里有个建模上的坑,我顺手踩了。1、2、3 号柜不是型号 A,它们是老柜子,只是新测点没值,被 CASE 判成了「型号A-老设备」。靠「某列有没有值」来反推设备型号是不靠谱的,型号应该是标签列,写死在 TAGS 里。这个 CASE 只在演示时能用,真上生产会被它坑。
五、建流:第一个报错来得很快
按文档直觉写 CREATE STREAM ... INTO ess_alerts,直接报错:
ERROR: relation "ess_alerts" does not exist
目标表要自己先建。这张表建在时序库 ess 里,会自动成为时序表,所以必须带 TAGS:
CREATE TABLE ess.ess_alerts (
k_timestamp TIMESTAMPTZ(3) NOT NULL,
cell_temp_max FLOAT8,
current_a FLOAT8,
alert_type VARCHAR(50),
severity_level INT,
created_at TIMESTAMPTZ DEFAULT now()
) TAGS (
cabinet_no INT4 NOT NULL,
station VARCHAR(64)
) PRIMARY TAGS(cabinet_no);
CREATE TABLE
Time: 22.029222ms
SHOW CREATE TABLE 回显:
ess.public.ess_alerts | CREATE TABLE ess_alerts (
| k_timestamp TIMESTAMPTZ(3) NOT NULL,
| cell_temp_max FLOAT8 NULL,
| current_a FLOAT8 NULL,
| alert_type VARCHAR(50) NULL,
| severity_level INT4 NULL,
| created_at TIMESTAMPTZ(3) NULL DEFAULT now():::TIMESTAMPTZ
| ) TAGS (
| cabinet_no INT4 NOT NULL,
| station VARCHAR(64) ) PRIMARY TAGS(cabinet_no)
| retentions 0s
k_timestamp 写的是 TIMESTAMPTZ(3),精度要跟源表对齐。不对齐会报 type not compatible,这是后面踩坑表里的第 5 条。
created_at 这个 DEFAULT now() 是后面算延迟的关键,它记的是告警落库的时间。
六、流建好了,然后呢
CREATE STREAM device_temp_monitor INTO ess.ess_alerts AS
SELECT
k_timestamp AS k_timestamp,
cabinet_no AS cabinet_no,
station AS station,
cell_temp_max AS cell_temp_max,
'HIGH_TEMPERATURE' AS alert_type,
1 AS severity_level
FROM ess.cabinet_metrics
WHERE cell_temp_max > 35;
CREATE STREAM
Time: 20.722319ms
注意每一列都写了 AS。这不是我起的范儿,缺别名会报 should provide alias name,连 'HIGH_TEMPERATURE' 这种常量列也要。
SHOW STREAMS 看一下:
name | target_table_name | options | status | start_time
----------------------+-----------------------+-----------+---------+---------------------
device_temp_monitor | ess.public.ess_alerts | {...} | Enable | 2026-10-31 03:43:15
status 是 Enable,看着像是跑起来了。options 里这些默认值值得抄一份:
{"enable":"on","max_delay":"24h","sync_time":"1m","process_history":"off",
"ignore_expired":"on","ignore_update":"on","max_retries":"5",
"checkpoint_interval":"10s","heartbeat_interval":"2s",
"recalculate_delay_rounds":"10","buffer_size":"2GiB","low_latency":"off"}
sync_time 是 1 分钟,checkpoint_interval 10 秒,low_latency 默认关。这几个数字后面会解释延迟为什么是秒级。
但 Enable 不等于在跑。我插了一条 42.5 °C,等了一会儿,告警表是空的。插了 46.5 °C,还是空的。
流建完要显式 START STREAM,不 START 就没有数据,而且没有任何提示告诉你没启动。这个体验比较糙,SHOW STREAMS 里那个 Enable 给了我一个错误的安心感。
START 之后,之前插的那些存量数据被回溯处理了,告警表里一下子冒出来几条。这也解释了为什么后文 52.5 °C 那条延迟算出来有 15 秒:它是在回溯批次里被处理的,不是实时那条链路。
七、延迟实测:秒级,不是毫秒级
查告警表:
SELECT * FROM ess.ess_alerts ORDER BY k_timestamp DESC;
k_timestamp | cell_temp_max | current_a | alert_type | severity_level | created_at | cabinet_no | station
--------------------------------+---------------+-----------+------------------+----------------+-------------------------------+------------+----------
2026-10-31 04:08:25.245+00:00 | 60.5 | NULL | HIGH_TEMPERATURE | 1 | 2026-10-31 04:08:33.489+00:00 | 101 | NULL
2026-10-31 04:04:09.863+00:00 | 58.5 | NULL | HIGH_TEMPERATURE | 1 | 2026-10-31 04:04:13.402+00:00 | 101 | NULL
2026-10-31 04:01:27.655+00:00 | 52.5 | NULL | HIGH_TEMPERATURE | 1 | 2026-10-31 04:01:43.332+00:00 | 101 | NULL
(3 rows)
k_timestamp 是数据在源表里的写入时间,created_at 是告警落库时间,两个相减就是端到端延迟。我把三条都算了一遍:
| 温度 | 源表写入 | 告警落库 | 端到端延迟 |
|---|---|---|---|
| 60.5 °C | 04:08:25.245 | 04:08:33.489 | 8.244 秒 |
| 58.5 °C | 04:04:09.863 | 04:04:13.402 | 3.539 秒 |
| 52.5 °C | 04:01:27.655 | 04:01:43.332 | 15.677 秒 |
三次实测:3.5 秒、8.2 秒、15.7 秒。去掉 15.7 秒那条(它在回溯批次里),稳态落在 3 到 8 秒 这个区间。
这个数字要说清楚:是秒级,不是毫秒级。如果你的告警链路按毫秒设计,会踏空。对照 options 里 sync_time: 1m、checkpoint_interval: 10s 这两个默认值,秒级延迟是合理的结果,不是故障。
我插过一条 30.5 °C 的正常温度,没有触发告警,告警表行数没变,阈值逻辑是对的。
八、查询侧:毫秒级,但别当结论
三次查询的实际耗时:
| 查询 | 返回行数 | 耗时 |
|---|---|---|
| 16 行混合查询 + CASE 分型号 | 16 | 2.030573 ms |
| 告警表全表排序 | 3 | 2.154310 ms |
源表条件查询(cabinet_no=101 AND cell_temp_max>40) |
1 | 3.734186 ms |
都在个位数毫秒。这是十来行数据的量,只说明查询链路通了,跟性能没关系。真要谈性能得先造到百万行,这次没做,也就不写。
顺带看一眼源表那条查询返回的行,加列之后它已经是 22 列了:101 号柜这一行,除了温度、SOC 相关的几列有值,剩下十几列全是 NULL。这就是稀疏表在单行上的样子。
九、这套组合能用在哪:一条边界线
基于这次实测,我能给出的边界:
能用。 设备型号多、测点差异大、型号还在往里加的场景。加列在线、毫秒级、历史数据不回填,这三点我都实测过了。
能接受秒级延迟的监控告警。 稳态 3 到 8 秒,设备温度告警这类场景够用。
要绕开的。 告警表别依赖非主标签,站点这种维度要么并进主标签,要么在应用层补一次。
别指望的。 毫秒级告警、集群高可用、流任务自动恢复。前两个这次没测,第三个测出来是糙的,容器重启后流会变成僵尸状态,得人工 DROP 重建。功能能用,只是要把它加进巡检清单。
十、站点名去哪了
这是本文最重的一节。
看上面那张告警表,cabinet_no 列是 101,station 列是 NULL。三条全是 NULL。
回源表确认一下这些行到底有没有站点名:
SELECT k_timestamp, station, cell_temp_max FROM ess.cabinet_metrics
ORDER BY k_timestamp DESC LIMIT 5;
k_timestamp | station | cell_temp_max
--------------------------------+---------+----------------
2026-10-31 04:04:09.863+00:00 | A站 | 58.5
2026-10-31 04:01:27.655+00:00 | A站 | 52.5
2026-10-31 03:57:30.821+00:00 | A站 | 30.5
2026-10-31 03:56:11.914+00:00 | A站 | 46.5
2026-10-31 03:43:56.888+00:00 | A站 | 42.5
(5 rows)
源表里 station 五条全有值,写着「A站」。
我又插了一条新的 60.5 °C 验证,结果一样:cabinet_no 是 101,station 还是 NULL。
对照结果:
| 列 | 在源表 | 在告警表 | 是不是主标签 |
|---|---|---|---|
cabinet_no |
101 | 101(正确) | 是(PRIMARY TAGS) |
station |
A站 | NULL(丢失) | 否(普通 TAGS) |
问题定位:流计算写入目标表时,非主标签列没有被传递,静默丢弃为 NULL。
这件事有三个地方让人不安:
SELECT里明明写了station AS station,建流的时候类型校验也过了;- 运行时没有任何报错,没有日志,
SHOW STREAMS的error_message列是空的; - 丢的是站点名,运维收到「101 号柜高温告警」却不知道在哪个站,恰恰是最要命的那个字段。
在监控告警这种场景里,静默的错误数据比崩库危险。崩库你马上知道,静默丢字段你可能三个月后才发现告警从来没派对过人。
最小复现
KWDB 3.2.2(build 2026/08/05)
时序表 A:TAGS (cabinet_no, station) PRIMARY TAGS(cabinet_no)
时序表 B:TAGS (cabinet_no, station) PRIMARY TAGS(cabinet_no)
CREATE STREAM s INTO B AS
SELECT k_timestamp AS k_timestamp,
cabinet_no AS cabinet_no,
station AS station,
...
FROM A WHERE <条件>;
START STREAM s;
结果:B 中 cabinet_no 正确写入,station 恒为 NULL(A 中该列有值)。
规避办法:目前我只能想到两条。一是告警表只保留主标签,站点信息用主标签拼进去(比如把 cabinet_no 换成 101_A站 这种复合主键);二是在应用层查告警时,用 cabinet_no 回源表补一次 station。两条都不优雅,等官方修。
十一、这一趟踩的 9 个坑
| # | 坑 | 现象 / 报错 | 解法 |
|---|---|---|---|
| 1 | 找流计算集群参数 | unknown setting |
3.2.2 免配置,直接建流 |
| 2 | INTO 的目标表没建 | relation does not exist |
先建目标表 |
| 3 | 时序库里建关系表 | can not create relational table |
加 TAGS,建成时序表 |
| 4 | 流查询的列缺别名 | should provide alias name |
每一列显式 AS,常量列也要 |
| 5 | now() 精度不匹配 |
type not compatible |
::TIMESTAMPTZ(3),或省略让 DEFAULT 兜底 |
| 6 | INSERT ... SELECT 只给部分列 |
SQL panic,进程崩溃 | 规避,给全列或用别的写法 |
| 7 | 容器重启后流不恢复 | 僵尸 running 状态 | DROP 重建 |
| 8 | CREATE STREAM 不生效 | 无报错无输出,告警表一直空 | 显式 START STREAM;启动后会回溯存量数据 |
| 9 | 非主标签流式写入丢失 | station 恒为 NULL,静默数据错误 |
暂无解,只能用主标签或应用层补偿 |
第 6 条我运气不好碰上了,SQL panic 直接把进程搞崩,容器得重新拉。第 9 条比第 6 条更值得写:崩库你看得见,静默丢字段你看不见。
十二、能拿走的九条
- 时序表加列是在线的,11 个列,2 到 97 毫秒,查询没中断,历史数据不回填。这条我实测了,可以放心用。
- BOOL 类型支持,
door_open BOOL建列和插入都通过。 INSERT ... SELECT里 UNION ALL 可用,时序表不支持 VALUES 里的表达式,绕道 SELECT 就行。- 设备型号别靠「有没有值」反推,写成标签列。
- 流建完要 START,
SHOW STREAMS里Enable不代表在跑。 - 稳态延迟 3 到 8 秒,按秒级设计告警链路,别按毫秒。
- 告警表只用主标签,非主标签会静默丢(第 9 条),站点信息另想办法。
- 容器重启后流是僵尸状态,要 DROP 重建,纳入巡检。
- 性能数字一概没测。本文所有毫秒数是空表十来行的值,只能说明功能通了。
十三、没做的部分
按老规矩交代清楚:
- 20000 列上限没验证,我只加到 11 列。
- 压缩比、存储占用没测,官方公布的压缩比我既没有复现,也不引用。
- JDBC 列式传输没跑,不评价。
- 集群部署没做,全文都是单机容器。
给官方的 issue
第 9 条我准备整理成正式 issue 提交给 KWDB 社区。如果你也遇到非主标签丢失,欢迎在 issue 下补充复现环境。
关于作者
数据库界华少,杭州 DBA,长期在国产分布式数据库和 Data+AI 运维这条线上折腾。OpenTenBase ACE、OceanBase 社区版主、墨天轮 MVP、崖山 YVP、KaiwuDB MVP、腾讯云开发者先锋 TDP。
⚡ 不是在测数据库,就是在去测数据库的路上。
这次,是从 KWDB 流计算那个 NULL 里回来的路上。
本文参与 KWDB 创作者计划开源两周年特别季。所有操作在真实环境中执行,报错未删改。
附:标题备选
| # | 标题 | 分数 |
|---|---|---|
| 1 | 告警出来了,站点名丢了:KWDB 3.2.2 流计算 + 稀疏表实测 | 94 |
| 2 | 一条流踩出 9 个坑:KWDB 3.2.2 流计算实测,含两个 bug | 90 |
| 3 | 加 11 列、建一条流、丢一个字段:KWDB 3.2.2 上机实测 | 88 |
| 4 | 流计算稳态延迟 3 到 8 秒,我在 KWDB 3.2.2 上量了一遍 | 85 |
摘要(107 字):
在 openEuler 上用 KWDB 3.2.2 单机容器跑了一遍流计算:一张时序表装三种型号设备,在线加 11 个测点列,告警几秒后到。三次实测端到端延迟 3.5 秒、8.2 秒、15.7 秒,是秒级。顺带踩了 9 个坑,其中一个真 bug:非主标签 station 在写入告警表时静默变成 NULL,不报错、无日志。

评论(0)