兼容
是对前人努力的尊重
是确保业务平稳过渡的基石
然而
这仅仅是故事的起点

说个真实经历。

去年年底接了个工业物联网的项目,客户是做设备监控的,前端接了大概三千多个传感器点位,温度、振动、电流什么的都有。刚上线那会儿一切正常,跑了三个月之后,问题就来了——写入开始变慢,查询更慢,运维那边天天跟我抱怨说数据量上来之后整个系统跟老牛拉破车似的。

我当时第一反应是加索引、调参数,折腾了一轮发现没啥用。后来仔细一查,好家伙,底层分区表已经膨胀到上千个分片了,光查询规划器遍历元数据就得吃掉大几十毫秒。再加上分片策略是初期拍脑袋定的,有些分片数据量差了好几倍,典型的热点倾斜。

最后那个项目花了大力气重新做数据迁移,才算把坑填上。但这段经历让我对时序数据库的分区管理这件事有了很深的警惕。

今年有机会深入体验了金仓数据库的时序场景性能增强包,里面有个核心概念叫"超表"(Hypertable),说白了就是针对我上面说的这些痛点设计的一套东西。这篇文章算是我的体验笔记,把超表的架构逻辑拆开来讲讲。

先搞明白一个问题:为什么传统分区表搞不定时序数据?

用金仓那份官方文档里的话说,时序场景的核心需求就四条:海量存储(设备多、指标多、频率高)、持续写入(数据不停产生)、特定时间段的聚合查询优化、以及数据生命周期管理(过期的数据要能自动处理)。

传统分区表在这几个方面都有短板。拿分片管理来说,你得自己提前规划好分区策略——分多少片、每片多大、什么时候创建新分区。这些全得手动搞,写脚本、配定时任务。一旦脚本出了问题,比如某个时间段的分区没创建出来,数据写入就直接报错。

更麻烦的是扩容。设备增加了、采样频率提高了,当初设定的分区策略可能就不合适了。但改分区策略这件事,在傳統方案里基本等于做一次小型数据迁移。

还有一点容易被忽略的——元数据膨胀。每个分区都有自己的索引、统计信息和路由信息。当分区数从几十个涨到几百个、上千个之后,数据库在做查询规划的时候光遍历这些元数据就得花不少时间。我前面说的那个项目就是这个情况,查个最近一小时的数据,还没开始真正扫描数据呢,光看元数据就慢了一拍。

超表的第一层:把物理分片藏起来

金仓时序数据库的超表,最核心的设计思路就一句话——让开发者只面对一张逻辑表,底下的物理分片全交给系统自己管。

这话说起来简单,做起来涉及的东西还挺多的。我从使用手册里梳理了一下,超表的分区机制大概是这么运作的:

每个超表底下由很多叫"Chunk"的子表组成。每个Chunk对应一个时间范围,只包含这个时间范围内的数据。关键的是,当插入的数据时间戳不在已有Chunk的时间范围内时,系统会自动创建一个新的Chunk来存储这些数据。你不需要写脚本,不需要配定时任务,系统自己就干了。

默认情况下,每个Chunk覆盖的时间周期是7天。但这个值可以通过chunk_time_interval参数来调整。使用手册里给了个很实际的建议:如果每日写入量大概2GB、内存64GB,那7天的间隔就合适;如果每日写入量到了10GB,那间隔就应该设成1天。

这个设计的好处在哪呢?拿我前面那个项目来说,如果当时用的是超表,分区数量膨胀的问题就不会出现。因为Chunk的创建是自动的,而且每个Chunk的时间范围是受控的,不会无限制膨胀。

来看个具体的创建过程:

-- 第一步:先建一张普通表,跟平时建表一模一样
CREATE TABLE sensor_data (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT NOT NULL,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    vibration   DOUBLE PRECISION
);

-- 第二步:一行SQL把它变成超表
SELECT create_hypertable('sensor_data', 'time');

就这两步。第一步行是标准的CREATE TABLE,没有任何特殊语法。第二步调用create_hypertable函数,告诉系统这张表按time列做时间分区。完事了。

如果你想调整分区间隔,创建的时候直接指定就行:

-- 创建超表时设置分区间隔为1天
SELECT create_hypertable('sensor_data', 'time', 
    chunk_time_interval => INTERVAL '1 day');

已经存在的超表也能改间隔,不过要注意——改了之后只对新建的Chunk生效,老的Chunk不受影响。所以初期选个合理的值还是挺重要的:

-- 调整已有超表的块间隔
SELECT set_chunk_time_interval('sensor_data', INTERVAL '24 hours');

从应用层的角度看,超表就是一张普通表。INSERT的时候不需要指定数据写到哪个Chunk里,系统根据时间戳自动路由。SELECT的时候也不需要关心数据分布在哪些Chunk上,该JOIN就JOIN,该GROUP BY就GROUP BY。

这一点特别重要。因为很多时序库搞了一套自己的专有API或者查询语言,开发团队得重新学,原来的代码也不能复用。

超表的第二层:二维分区——不只是按时间切

金仓的做法是再加一个空间轴。用设备ID或者区域编号做哈希分区,把同一时间段内的数据均匀分散到不同的物理节点上。这样一来,写入压力就被分摊了,整体吞吐量跟节点数成正比,可以线性扩展。

从使用手册里看,加空间维度是用add_dimension函数:

-- 先创建按时间分区的超表
SELECT create_hypertable('device_metrics', 'time');

-- 再加上空间维度,按device_id做4路哈希分区
SELECT add_dimension('device_metrics', 'device_id', 
    number_partitions => 4);

这个设计在金仓官方的那篇工业时序数据的文章里也有详细说明。原文是这么说的:金仓时序数据库自研创新"二维分区"算法,通过分布式超表和块级数据管理机制,结合时间和空间两个维度的内核级精密路由。时间轴主分区用于控制数据块和索引规模,避免热数据范围无限扩大;空间轴次分区针对设备ID等标签执行哈希分区,将高并发写入压力均匀分散至各数据节点。

我用测试环境做了个简单的对比。在只按时间分区的情况下,模拟10000个设备并发写入,写入延迟波动比较大,大概50到200毫秒之间跳动。加上4路空间哈希之后,写入延迟基本稳定在10到30毫秒。差距还是很明显的。

而且这个差距会随着设备数量增加而放大。设备越多,空间哈希的分散效果越好。

查询这边:分区裁剪和标准SQL的化学反应

超表的查询体验,我觉得可以用"无感"两个字来形容。

你写SQL的时候完全不需要考虑底下有多少个Chunk、数据分布在哪些节点上。带时间范围的查询,系统自动做分区裁剪,只扫相关的Chunk。

-- 查最近1小时的设备平均温度
-- 系统自动跳过不相关的分区,你什么都不用管
SELECT device_id, AVG(temperature) AS avg_temp
FROM device_metrics
WHERE time >= NOW() - INTERVAL '1 hour'
GROUP BY device_id
ORDER BY avg_temp DESC;

想看执行计划是不是真的做了分区裁剪?用EXPLAIN就行:

EXPLAIN (ANALYZE, BUFFERS) 
SELECT device_id, AVG(temperature) 
FROM device_metrics
WHERE time >= '2026-08-20 00:00:00'
  AND time <  '2026-08-20 01:00:00'
GROUP BY device_id;

执行计划里如果只出现了20260820相关的那个子分区,就说明裁剪生效了。如果你发现所有分片都在被扫描,那八成是查询条件里没有直接带上时间列,或者用了函数包裹时间列导致优化器推不出来范围。

时间桶聚合是时序场景里的刚需操作。金仓内置了time_bucket函数,可以把时间戳按任意间隔截断,配合GROUP BY做降采样:

-- 按分钟降采样,算每分钟的平均温度
SELECT 
    time_bucket('1 minute', time) AS minute_bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    MAX(vibration) AS max_vib
FROM device_metrics
WHERE time >= '2026-08-20 00:00:00'
  AND time <  '2026-08-21 00:00:00'
GROUP BY minute_bucket, device_id
ORDER BY minute_bucket, device_id;

如果你的业务需要特殊对齐方式,可以通过origin参数调整。

-- 计算5小时间隔的平均值,并把所有桶推迟1小时
SELECT time_bucket('5 hours', time, '1 hour'::INTERVAL) AS bucket,
    AVG(cpu)
FROM metrics
GROUP BY bucket
ORDER BY bucket DESC;

时序表和关系表的JOIN也很自然。超表在应用层就是一张普通表,所以它可以跟任何其他表做JOIN,不需要额外的数据同步或者中间层。

-- 时序数据直接JOIN设备档案表
SELECT d.device_name, d.location, d.maintainer,
       m.time, m.temperature, m.pressure
FROM device_metrics m
JOIN device_archive d ON m.device_id = d.device_id
WHERE m.time >= '2026-08-20 00:00:00'
  AND m.time <  '2026-08-20 01:00:00'
  AND m.temperature > d.alert_threshold
ORDER BY m.temperature DESC;

查询的时候,系统会把已经预计算好的历史结果和最新的未聚合数据组合起来返回,你得到的始终是最新的分析结果,但不需要每次都去扫描海量原始数据。

-- 创建一个按小时聚合的连续聚合视图
CREATE MATERIALIZED VIEW hourly_temp_stats
WITH (timescaledb.continuous) AS
SELECT 
    time_bucket('1 hour', time) AS hour_bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    MAX(temperature) AS max_temp,
    MIN(temperature) AS min_temp
FROM device_metrics
GROUP BY hour_bucket, device_id;

连续聚合还支持分层。比如你可以先建一个按分钟聚合的视图,再在上面建一个按小时聚合的视图,层层递进。这样不同粒度的分析需求都有预计算的结果可以用。

使用手册里说,在典型的分钟级滑动窗口分析中,连续聚合可以实现毫秒级响应。这对状态监测和故障识别这类需要持续获得最新分析结果的应用来说非常有用。

自动刷新策略也可以配:

-- 添加自动刷新策略,每1小时刷新一次,覆盖最近2小时的数据
SELECT add_continuous_aggregate_policy('hourly_temp_stats',
    start_offset => INTERVAL '2 hours',
    end_offset => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour');

上篇小结

写到这,上篇差不多了。总结一下我对超表架构的理解:

超表本质上做了一件事——把分片管理的复杂度从开发者和运维身上移到了数据库内核里。

传统的分区方案,分片的创建、路由、裁剪、归档全得人手动搞。超表把这些全自动化了。开发者看到的是一张表,写的是标准SQL;运维看到是一组自动化策略;底下的物理Chunk怎么创建、怎么路由、怎么裁剪,那是内核的事。

而二维分区的设计又在这个基础上解决了高并发写入的分散问题。时间轴控制数据块规模,空间轴分摊写入压力,两个维度配合起来才能撑住真正的大规模场景。

连续聚合则进一步解决了"查得快"的问题——常用的分析结果提前算好,查询的时候直接用,不用每次都扫原始数据。

下篇我打算聊聊压缩、数据保留策略、分布式超表,还有几个实际落地项目的情况。北京轨交TCC、泰兴交通、机场预测性维护,这些案例里都有不少干货。

先写到这。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐