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

时序表加 6 列,老数据一个没动:KWDB 3.2.2 上机实测

原创shunwah2026-10-31
0

时序表加 6 列,老数据一个没动:KWDB 3.2.2 上机实测

之前我看过一张表,我记到现在。

同一批储能柜,前后两个厂家、两个年份。老柜子只上报温度和 SOC,三个字段;新柜子多出单体电压极值、充放电电流、绝缘阻值、环境温度,加起来十几个测点。这些柜子要进同一张表,还要按柜号一起查。

传统时序库碰到这事,通常两条路:按最全的那款柜子建宽表,老柜子每行空着七八列;或者一个批次一张表,查询时再拼。两条路都别扭。

KWDB 3.2.2 把稀疏表列进了新特性,单表支持到 20000 列。数字我先记下,真正想知道的是另一件事:表已经建好、老数据也进去了,这时候加列,历史数据要不要回填?

在 openEuler 上装了一个单机版,从头跑到尾。命令和输出都在下面,没删改。


一、环境

项 值
操作系统 openEuler
部署方式 Docker 单机
镜像 kwdb/kwdb:3.2.2(拉取后 1.2GB)
数据目录 /data/kaiwudb
端口 26257(SQL)、8080(HTTP)

单节点非安全模式,只用来验证功能。生产部署请照官方文档走裸机方式,这篇不涉及性能。


二、拉镜像、建目录、起容器

cd /data/
mkdir -p /data/kaiwudb
docker pull kwdb/kwdb:3.2.2

拉取完成后的输出:

Digest: sha256:a27980970298cba1dbfc6f02051a43fd5b4f603f22c814054c2d63ed2af99ed4
Status: Downloaded newer image for kwdb/kwdb:3.2.2
docker.io/kwdb/kwdb:3.2.2

启动容器。官方文档里的挂载路径是 /var/lib/kaiwudb,我换成了自己建的 /data/kaiwudb,方便找:

docker run -d --privileged --name kwdb \
  -p 26257:26257 -p 8080:8080 \
  -v /data/kaiwudb:/kaiwudb/deploy/kaiwudb-container \
  --ipc shareable \
  -w /kaiwudb/bin \
  kwdb/kwdb:3.2.2 \
  ./kwbase start-single-node --insecure \
  --listen-addr=0.0.0.0:26257 \
  --http-addr=0.0.0.0:8080 \
  --store=/kaiwudb/deploy/kaiwudb-container

真实输出里有这么一行,不影响本机使用,跨主机访问要处理:

WARNING: IPv4 forwarding is disabled. Networking will not work.
acf44bdb45b71b970623954e03456dcffed5d239159ad8b95fa4863562c6aedf

容器起来了:

CONTAINER ID   IMAGE             COMMAND                  STATUS              PORTS                                  NAMES
acf44bdb45b7   kwdb/kwdb:3.2.2   "./kwbase start-sing…"   Up About a minute   0.0.0.0:8080->8080/tcp, ... 26257/tcp   kwdb

三、看日志,确认节点真就绪

容器 Up 不等于库能用。看一眼日志尾巴:

docker logs kwdb 2>&1 | tail -5
storage engine:      rocksdb
ts storage engine:   kwtsdb
status:              initialized new cluster
clusterID:           a3670f0e-3534-4241-8ce5-aacf78c97aa7
nodeID:              1

关键的两行是 ts storage engine: kwtsdb 和 status: initialized new cluster。时序引擎起来了,集群初始化完成。

数据目录也写进东西了:

000003.log  kwbase.advertise-addr      kwbase.listen-addr     LOCK             temp-dirs-record.txt
auxiliary   kwbase.advertise-sql-addr  kwbase.sql-addr        logs             tsdb
CURRENT     KWBASEDB_VERSION           kwbase.start_mode      MANIFEST-000001
IDENTITY    kwbase.http-addr           kwbase-temp1613256973  OPTIONS-000005

四、连上去,先看版本

docker exec -it kwdb ./kwbase sql --insecure --host=127.0.0.1
# Server version: KaiwuDB 3.2.2 (x86_64-pc-linux-gnu, built 2026/08/05 01:31:25, go1.21.13, gcc 7.4.0)
# Cluster ID: a3670f0e-3534-4241-8ce5-aacf78c97aa7
root@127.0.0.1:26257/defaultdb>
root@127.0.0.1:26257/defaultdb> SELECT version();
                                        version
----------------------------------------------------------------------------------------
  KaiwuDB 3.2.2 (x86_64-pc-linux-gnu, built 2026/08/05 01:31:25, go1.21.13, gcc 7.4.0)
(1 row)

Time: 943.12µs

版本号对上,开干。


五、一个实例,两种库

建一个时序库放指标,再建一个关系库放台账:

CREATE TS DATABASE ess;
CREATE DATABASE ess_meta;
CREATE TS DATABASE
Time: 9.84273ms

CREATE DATABASE
Time: 9.57444ms

一条 CREATE TS DATABASE,一条 CREATE DATABASE,差的就是 TS 两个字。查一下:

SHOW DATABASES;
  database_name | engine_type
----------------+--------------
  defaultdb     | RELATIONAL
  ess           | TIME SERIES
  ess_meta      | RELATIONAL
  postgres      | RELATIONAL
  system        | RELATIONAL
(5 rows)

engine_type 这一列把事情说清楚了:同一个实例里,ess 是 TIME SERIES,ess_meta 是 RELATIONAL。所谓多模,落到操作上就是建库时的一个关键字。


六、建表,只给老柜子留三个测点

按最老的那一批柜子来建:

USE ess;

CREATE TABLE cabinet_metrics (
    k_timestamp    TIMESTAMPTZ NOT NULL,
    cell_temp_max  FLOAT,
    cell_temp_avg  FLOAT,
    soc            FLOAT
) TAGS (
    cabinet_no INT NOT NULL,
    station    VARCHAR(64)
) PRIMARY TAGS(cabinet_no);
CREATE TABLE
Time: 15.155936ms

这张表的列分三类,KWDB 时序表都是这个结构:

类型 本表中的列 说明
时间戳列 k_timestamp 每张时序表必须有,且非空
标签列 cabinet_no、station 写在 TAGS() 里,描述设备的静态属性,不随时间变
字段列 cell_temp_max、cell_temp_avg、soc 随时间变化的采集值

PRIMARY TAGS(cabinet_no) 指定柜号为主标签,同一台柜子的数据在物理上聚在一起,按柜号查的时候省掉一轮扫描。站名 station 作为普通标签,可以参与过滤。

SHOW CREATE TABLE 回显,注意最后一行:

SHOW CREATE TABLE ess.cabinet_metrics;
  ess.public.cabinet_metrics | CREATE TABLE cabinet_metrics (
                             |     k_timestamp TIMESTAMPTZ(3) NOT NULL,
                             |     cell_temp_max FLOAT8 NULL,
                             |     cell_temp_avg FLOAT8 NULL,
                             |     soc FLOAT8 NULL
                             | ) TAGS (
                             |     cabinet_no INT4 NOT NULL,
                             |     station VARCHAR(64) ) PRIMARY TAGS(cabinet_no)
                             |     retentions 0s

retentions 0s 表示没设保留策略,数据不过期。生产环境要设,我这里是验证功能,先不动。

插一条老柜子的数据,只给温度和 SOC:

INSERT INTO cabinet_metrics (k_timestamp, cabinet_no, station, cell_temp_avg, soc)
  VALUES ('2026-10-26 22:15:00', 1, 'A站', 28.9, 86.6);
INSERT 1
Time: 1.361871ms

查回来:

         k_timestamp        | cell_temp_max | cell_temp_avg | soc  | cabinet_no | station
----------------------------+---------------+---------------+------+------------+----------
  2026-10-26 22:15:00+00:00 | NULL          |          28.9 | 86.6 |          1 | A站
(1 row)

没给的 cell_temp_max 是 NULL。这在关系库里是常识,在时序库里值得记一下,因为很多时序产品的空值处理有自己的一套。


七、加 6 列,然后看老数据

新柜子要进来了,补列:

ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS cell_volt_max FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS cell_volt_min FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS charge_current FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS discharge_current FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS ins_resistance FLOAT8;
ALTER TABLE ess.cabinet_metrics ADD COLUMN IF NOT EXISTS ambient_temp FLOAT8;

六条都返回 ALTER TABLE,每列耗时在 90ms 上下:

ALTER TABLE
Time: 93.985148ms
...
ALTER TABLE
Time: 93.649592ms

新柜子的完整数据,一次插两条:

INSERT INTO ess.cabinet_metrics VALUES
('2026-10-26 22:15:00', 28.5, 27.2, 86.5, 3.35, 3.31, 50.0, 45.0, 550.0, 31.0, 2, 'A站'),
('2026-10-26 22:20:00', 29.0, 27.5, 86.4, 3.36, 3.32, 48.0, 43.0, 552.0, 31.1, 2, 'A站');
INSERT 2
Time: 567.778µs

现在把 10 月 26 日当天的数据全查出来:

SELECT * FROM ess.cabinet_metrics
WHERE k_timestamp >= '2026-10-26 00:00:00'
  AND k_timestamp < '2026-10-27 00:00:00'
  AND station = 'A站'
ORDER BY k_timestamp DESC;
         k_timestamp        | cell_temp_max | cell_temp_avg | soc  | cell_volt_max | cell_volt_min | charge_current | discharge_current | ins_resistance | ambient_temp | cabinet_no | station
----------------------------+---------------+---------------+------+---------------+---------------+----------------+-------------------+----------------+--------------+------------+----------
  2026-10-26 22:30:00+00:00 |          28.7 |          27.9 | 86.3 | NULL          | NULL          | NULL           | NULL              | NULL           | NULL         |          1 | A站
  2026-10-26 22:25:00+00:00 |          30.1 |          28.8 | 86.4 | NULL          | NULL          | NULL           | NULL              | NULL           | NULL         |          1 | A站
  2026-10-26 22:20:00+00:00 |          29.2 |          28.1 | 86.5 | NULL          | NULL          | NULL           | NULL              | NULL           | NULL         |          1 | A站
  2026-10-26 22:20:00+00:00 |            29 |          27.5 | 86.4 |          3.36 |          3.32 |             48 |                43 |            552 |         31.1 |          2 | A站
  2026-10-26 22:15:00+00:00 | NULL          |          28.9 | 86.6 | NULL          | NULL          | NULL           | NULL              | NULL           | NULL         |          1 | A站
  2026-10-26 22:15:00+00:00 |          28.5 |          27.2 | 86.5 |          3.35 |          3.31 |             50 |                45 |            550 |           31 |          2 | A站
(6 rows)

这就是我这次最想看的一屏。

1 号柜是加列前进来的,新加的六列全是 NULL;2 号柜是加列后进来的,六列有值。同一个查询里两种形态并存,加列没有回头去改老数据,也没有要求我回填。

对储能这种场景,这一条比「支持两万列」那个数字有用得多:设备分批进场、测点逐年增加是常态,每次加测点都要停服回填,这事没人干得下去。


八、四个坑

坑一:查不到数据,因为时间对不上

第一次按相对时间查,返回 0 行:

SELECT * FROM ess.cabinet_metrics
WHERE k_timestamp > now() - INTERVAL '30 minutes'
ORDER BY k_timestamp DESC;
(0 rows)

原因不在 SQL,在时间。我插的数据时间戳是 2026-10-26,服务器系统时间是 2026-10-31,now() - INTERVAL '30 minutes' 算出来是 10 月 31 日凌晨,跟 5 天前的数据没有任何交集。

做时序的人迟早会踩这个:造测试数据时习惯写死一个日期,写查询时习惯用 now(),两边一错开就是 0 行,而且没有任何报错提示。

改法有两个,一是查的时候用固定时间范围(上面第七节那句),二是插的时候就用相对时间。

坑二:插入数据有两个限制

限制一,VALUES 的个数必须和表的列数对上。

我按新柜子的完整测点写了 13 个值,而当时表只有 6 个目标列(1 个时间戳 + 3 个字段 + 2 个标签),插入直接失败。时序表不会因为多给了值就自动补列,缺的列得先 ALTER TABLE 加上,这也是第七节那 6 条 ALTER 的由来。

限制二,VALUES 里写 now() - INTERVAL 会报错。

想插一批贴近当前时间的数据,第一反应这么写:

INSERT INTO ess.cabinet_metrics (k_timestamp, cabinet_no, station, cell_temp_max, cell_temp_avg, soc)
VALUES
(now() - INTERVAL '20 minutes', 1, 'A站', 30.5, 29.8, 86.2),
(now() - INTERVAL '15 minutes', 1, 'A站', 30.8, 30.1, 86.1);

报错:unsupported input type BinaryOperator。

时序表的 VALUES 子句对表达式的支持有限,now() - INTERVAL 这种二元运算解析不了。把 VALUES 换成 SELECT 就能过:

INSERT INTO ess.cabinet_metrics (k_timestamp, cabinet_no, station, cell_temp_max, cell_temp_avg, soc)
SELECT now() - INTERVAL '20 minutes', 1, 'A站', 30.5, 29.8, 86.2
UNION ALL SELECT now() - INTERVAL '15 minutes', 1, 'A站', 30.8, 30.1, 86.1
UNION ALL SELECT now() - INTERVAL '10 minutes', 2, 'A站', 31.2, 30.5, 86.0
UNION ALL SELECT now() - INTERVAL '5 minutes',  3, 'B站', 32.0, 31.2, 85.9;

插进去之后再查相对时间,这次有数据了:

           k_timestamp          | cell_temp_max | cell_temp_avg | soc  | ... | cabinet_no | station
--------------------------------+---------------+---------------+------+ ... +------------+----------
  2026-10-31 02:02:55.881+00:00 |            32 |          31.2 | 85.9 | ... |          3 | B站
  2026-10-31 01:57:55.881+00:00 |          31.2 |          30.5 |   86 | ... |          2 | A站
  2026-10-31 01:52:55.881+00:00 |          30.8 |          30.1 | 86.1 | ... |          1 | A站
  2026-10-31 01:47:55.881+00:00 |          30.5 |          29.8 | 86.2 | ... |          1 | A站
(4 rows)

坑三:time_bucket 不是 KWDB 的函数

按 5 分钟窗口降采样,第一反应写:

SELECT time_bucket('5 minutes', k_timestamp) AS time_window, ...

报错,函数不存在。time_bucket 是另一些时序库提供的函数,KWDB 没实现。

用 date_trunc 对齐窗口,效果一样:

SELECT
    date_trunc('minute', k_timestamp) AS time_window,
    cabinet_no,
    AVG(cell_temp_avg) AS avg_temp,
    MAX(cell_temp_max) AS max_temp
FROM ess.cabinet_metrics
WHERE k_timestamp > now() - INTERVAL '30 minutes'
GROUP BY time_window, cabinet_no
ORDER BY time_window, cabinet_no;
         time_window        | cabinet_no | avg_temp | max_temp
----------------------------+------------+----------+-----------
  2026-10-31 01:47:00+00:00 |          1 |     29.8 |     30.5
  2026-10-31 01:52:00+00:00 |          1 |     30.1 |     30.8
  2026-10-31 01:57:00+00:00 |          2 |     30.5 |     31.2
  2026-10-31 02:02:00+00:00 |          3 |     31.2 |       32
(4 rows)

顺带说一句,从别的时序库迁过来的人,time_bucket 这种函数是肌肉记忆,撞一次很正常。


九、跨模 JOIN:时序表 JOIN 关系表

在关系库里建台账:

CREATE TABLE IF NOT EXISTS ess_meta.cabinet_info (
    cabinet_no      INT PRIMARY KEY,
    model           VARCHAR(50),
    vendor          VARCHAR(50),
    commission_date DATE
);
CREATE TABLE
Time: 9.906211ms
INSERT INTO ess_meta.cabinet_info VALUES
(1, 'ESS-215', '品牌A', '2024-03-15'),
(2, 'ESS-100', '品牌B', '2024-11-20'),
(3, 'ESS-215', '品牌A', '2024-03-15');
INSERT 3
Time: 3.648302ms

然后跨库 JOIN。一个库是 TIME SERIES 引擎,一个是 RELATIONAL 引擎:

SELECT
    ts.cabinet_no,
    ci.model,
    ci.vendor,
    ts.k_timestamp,
    ts.cell_temp_avg,
    ts.soc
FROM ess.cabinet_metrics ts
JOIN ess_meta.cabinet_info ci ON ts.cabinet_no = ci.cabinet_no
WHERE ts.station = 'A站'
ORDER BY ts.cabinet_no, ts.k_timestamp DESC;
  cabinet_no |  model  | vendor |        k_timestamp        | cell_temp_avg | soc
-------------+---------+--------+---------------------------+---------------+-------
           1 | ESS-215 | 品牌A  | 2026-10-26 22:30:00+00:00 |          27.9 | 86.3
           1 | ESS-215 | 品牌A  | 2026-10-26 22:25:00+00:00 |          28.8 | 86.4
           1 | ESS-215 | 品牌A  | 2026-10-26 22:20:00+00:00 |          28.1 | 86.5
           1 | ESS-215 | 品牌A  | 2026-10-26 22:15:00+00:00 |          28.9 | 86.6
           2 | ESS-100 | 品牌B  | 2026-10-26 22:20:00+00:00 |          27.5 | 86.4
           2 | ESS-100 | 品牌B  | 2026-10-26 22:15:00+00:00 |          27.2 | 86.5
(6 rows)

Time: 4.385522ms

6 行,柜号、型号、厂商、温度、SOC 一次出来。标准 SQL,没有专门的函数,也没做数据搬运。

这一步的意义在运维而不是性能:台账在关系库、指标在时序库,两边各自按自己的模型维护,查询的时候才合到一起。以前这类需求要么 ETL 同步一份,要么在应用里拼两次查询。


十、另外两个常用查询

每个柜子的最新一条

SELECT DISTINCT ON (cabinet_no)
    cabinet_no, station, k_timestamp, cell_temp_max, cell_temp_avg, soc
FROM ess.cabinet_metrics
ORDER BY cabinet_no, k_timestamp DESC;
  cabinet_no | station |          k_timestamp          | cell_temp_max | cell_temp_avg | soc
-------------+---------+-------------------------------+---------------+---------------+-------
           1 | A站     | 2026-10-31 01:52:55.881+00:00 |          30.8 |          30.1 | 86.1
           2 | A站     | 2026-10-31 01:57:55.881+00:00 |          31.2 |          30.5 |   86
           3 | B站     | 2026-10-31 02:02:55.881+00:00 |            32 |          31.2 | 85.9
(3 rows)

DISTINCT ON 这种写法在时序场景里很好用,省掉一层子查询。

按柜子分组聚合

SELECT
    cabinet_no, station,
    AVG(cell_temp_avg) AS avg_temp,
    AVG(soc) AS avg_soc,
    COUNT(*) AS data_points
FROM ess.cabinet_metrics
WHERE k_timestamp > now() - INTERVAL '30 minutes'
GROUP BY cabinet_no, station
ORDER BY cabinet_no;
  cabinet_no | station |      avg_temp      | avg_soc | data_points
-------------+---------+--------------------+---------+--------------
           1 | A站     | 29.950000000000003 |   86.15 |           2
           2 | A站     |               30.5 |      86 |           1
           3 | B站     |               31.2 |    85.9 |           1
(3 rows)

十一、这次没测到的

得说清楚,免得被当成结论用:

  • 流计算没跑。 CREATE STREAM 这次一行都没执行,文章里不写它的行为。下一篇单独上。
  • 性能一概没测。 上面贴的那些毫秒数是单条语句的返回耗时,发生在单机、非安全模式、几行数据的环境里,只用来说明功能通了,不代表任何性能结论。真要出性能数字,得单独搭压测环境。
  • 20000 列这个上限没验证。 官方文档写的数字,我这次只加了 6 列。
  • 容器告警没处理。 IPv4 forwarding is disabled 那行在本机直连下不影响,要跨主机访问得先解决。

十二、三条能拿走的

  1. 加列不用回填历史数据。 这次实测最直接的一条:加 6 列之后,加列前写入的行新列全为 NULL,加列后写入的行有值,两者共存于同一张表。测点逐年增加的场景,这一条决定方案能不能落地。
  2. 造数据和查数据的时间基准要对齐。 我踩的第一个坑就是这个。测试数据写死日期、查询语句用 now(),两边错开就是 0 行,还不报错。
  3. 时序表的 VALUES 支持有限。 需要 now() - INTERVAL 这类表达式时,改用 INSERT ... SELECT。time_bucket 不存在,窗口对齐用 date_trunc。

关于作者

数据库界华少,杭州 DBA,常年跟国产数据库打交道。OpenTenBase ACE、OceanBase 社区版主、墨天轮 MVP、崖山 YVP、KaiwuDB MVP、金仓 KVA、TiDB MVA、NebulaGraph 合作伙伴、IFClub 专家、GBase 8a 资深专家、ITPUB 专家、腾讯云架构师上海同盟成员、腾讯云开发者先锋 TDP、腾讯云创作之星。

⚡ 不是在测数据库,就是在去测数据库的路上。

这次,是从一个储能柜的测点表回来的路上。

文中命令与输出均来自 openEuler + Docker 单机环境的实际执行,未做修饰。官方特性以上游文档为准,实测结论随版本变化可能不同。

-- 坚持原创,转载请注明出处 --

评论(0)

Me