KaiwuDB 落地实践KaiwuDB 征文多模数据库时序数据库

KWDB 3.2.2 高级特性实战:即时流计算与稀疏表在智能工厂的应用

原创shunwah2026-10-31
0

告警出来了,站点名丢了: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。

这件事有三个地方让人不安:

  1. SELECT 里明明写了 station AS station,建流的时候类型校验也过了;
  2. 运行时没有任何报错,没有日志,SHOW STREAMS 的 error_message 列是空的;
  3. 丢的是站点名,运维收到「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 条更值得写:崩库你看得见,静默丢字段你看不见。


十二、能拿走的九条

  1. 时序表加列是在线的,11 个列,2 到 97 毫秒,查询没中断,历史数据不回填。这条我实测了,可以放心用。
  2. BOOL 类型支持,door_open BOOL 建列和插入都通过。
  3. INSERT ... SELECT 里 UNION ALL 可用,时序表不支持 VALUES 里的表达式,绕道 SELECT 就行。
  4. 设备型号别靠「有没有值」反推,写成标签列。
  5. 流建完要 START,SHOW STREAMS 里 Enable 不代表在跑。
  6. 稳态延迟 3 到 8 秒,按秒级设计告警链路,别按毫秒。
  7. 告警表只用主标签,非主标签会静默丢(第 9 条),站点信息另想办法。
  8. 容器重启后流是僵尸状态,要 DROP 重建,纳入巡检。
  9. 性能数字一概没测。本文所有毫秒数是空表十来行的值,只能说明功能通了。

十三、没做的部分

按老规矩交代清楚:

  • 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)

Me