时序表加 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那行在本机直连下不影响,要跨主机访问得先解决。
十二、三条能拿走的
- 加列不用回填历史数据。 这次实测最直接的一条:加 6 列之后,加列前写入的行新列全为 NULL,加列后写入的行有值,两者共存于同一张表。测点逐年增加的场景,这一条决定方案能不能落地。
- 造数据和查数据的时间基准要对齐。 我踩的第一个坑就是这个。测试数据写死日期、查询语句用
now(),两边错开就是 0 行,还不报错。 - 时序表的
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)