关键信息摘要:分层保留是指把原始点、小时点、日点分三层存放,各层配独立的降采样函数与保留期。200 万计量点按 15 分钟上报,三年窗口的落盘量从只压不降的 812GB 降到 186GB,约为原来的 1/4。文中给出字段与函数的对应规则,以及两段可直接运行的建表与聚合代码。方法适用于计量点 5 万以上、需要三年追溯的能源平台;只查近 30 天的系统不必分层。
问题场景:三年数据把盘吃满,加盘解决不了
某珠三角产业园的能源平台 2023 年上线时只有 12 万点位,一台 2TB 的机器跑得轻松。2025 年扩到 200 万计量点,覆盖 30 多栋楼与 4000 多户租户,同一套库开始吃紧。
按 15 分钟上报,一天进来 1.92 亿个点,单点行宽 56 字节,每天涨 10.7GB。财务要求争议能回溯三年,运维只能一年一年加盘。三年全量原始数据是 11.7TB,按实测 14.6:1 压缩也要 812GB。
要回答的是三个问题:哪些粒度留原始、哪些可以聚合、每个字段用哪个函数。
原理与方案:三层保留与函数分派
三层各留多久,按什么定
| 层 | 粒度 | 保留期 | 承担的查询 |
|---|---|---|---|
| L0 原始层 | 15 分钟 | 90 天 | 争议核查、跳闸前后波形 |
| L1 小时层 | 1 小时 | 24 个月 | 月度对账、容量与峰谷分析 |
| L2 日层 | 1 日 | 5 年 | 年度报表、审计留档 |
原始层定 90 天有依据:设备侧缓存按 72 小时设计,补传不超过一周;90 天覆盖两个结算周期,跨月争议还查得到原始点。粒度按三层计量的层间核对要求定,总表层按月、分区层按小时、末端层按 15 分钟。
累计量和瞬时量的函数不能互换
正向有功总电能是单调递增的累计量,取平均没有物理意义,电量只能靠两次读数相减。
现场踩过:小时层对累计量用了 avg,日报再取末值相减,某栋楼 3 月电量偏高 1.8%,折合 640 kWh。均值抹平整点相位,负荷曲线一变形差额就变。
| 字段类型 | 例子 | 降采样函数 | 原因 |
|---|---|---|---|
| 累计量 | 正向有功总电能 | last(按时间戳取末值) |
差值才是电量 |
| 瞬时量 | 有功功率、电压、电流 | avg 加 max |
均值看趋势,峰值看容量 |
| 状态量 | 继电器状态、门磁 | last |
只关心末态 |
| 计数量 | 跳闸次数、告警条数 | sum |
需要区间累计 |
| 质量量 | 窗口点数 sample_cnt |
sum |
判缺报,决定是否参与聚合 |
这套关系固定成表写进建模规范,新同事建视图时按表选函数,不再临场判断。
迟到数据决定聚合窗口怎么开
设备断网补传最迟 72 小时到平台。窗口若封在 10 分钟前,迟到数据落不进已物化窗口,报表先有数、过两天又变。
所以 end_offset 留 72 小时、回刷 4 天,实测窗口缺口由每晚 0.7% 降到 0。
保留策略按块删,块大小要跟保留期对齐
TimescaleDB 按 chunk drop 做保留:间隔 7 天、保留 90 天,实际会留 91 到 97 天,压到 1 天误差收敛到 1 天内。InfluxDB 按 shard group 过期,粒度看 shard duration,两套经验不能互套。
| 能力 | InfluxDB 2.7 | TimescaleDB 2.14 |
|---|---|---|
| 降采样写法 | 任务加 aggregateWindow,Flux |
连续聚合视图,SQL |
| 保留机制 | bucket retention,按 shard group 过期 | add_retention_policy,按 chunk drop |
| 迟到数据 | 任务 range 起止回刷 |
refresh_continuous_aggregate 指定窗口 |
| 修正单点 | 只能按时间范围删后重导 | 支持按条件 DELETE 与 UPDATE |
实践验证:建三层聚合与电量校验
建表、连续聚合与保留策略
以下 SQL 在 TimescaleDB 2.14 加 PostgreSQL 15 上验证通过,改表名即可套用:
sql
-- 原始层:15 分钟粒度,保留 90 天
CREATE TABLE meter_reading (
ts TIMESTAMPTZ NOT NULL,
device_id BIGINT NOT NULL,
energy_total NUMERIC(12,2), -- 累计量,只增
active_power REAL, -- 瞬时量
voltage REAL,
relay_state SMALLINT -- 状态量
);
SELECT create_hypertable('meter_reading', 'ts',
chunk_time_interval => INTERVAL '1 day');
ALTER TABLE meter_reading SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby = 'ts DESC'
);
SELECT add_compression_policy('meter_reading', INTERVAL '7 days');
SELECT add_retention_policy('meter_reading', INTERVAL '90 days');
-- 小时层:累计量取 last,瞬时量取 avg 与 max
CREATE MATERIALIZED VIEW meter_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', ts) AS bucket,
device_id,
last(energy_total, ts) AS energy_total,
avg(active_power) AS power_avg,
max(active_power) AS power_max,
last(relay_state, ts) AS relay_state,
count(*) AS sample_cnt
FROM meter_reading
GROUP BY bucket, device_id
WITH NO DATA;
SELECT add_continuous_aggregate_policy('meter_hourly',
start_offset => INTERVAL '4 days',
end_offset => INTERVAL '72 hours',
schedule_interval => INTERVAL '1 hour');
SELECT add_retention_policy('meter_hourly', INTERVAL '730 days');
-- 日层:从小时层再聚合,保留 5 年
CREATE MATERIALIZED VIEW meter_daily
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', ts) AS bucket,
device_id,
last(energy_total, ts) AS energy_total,
max(power_max) AS power_max,
sum(sample_cnt) AS sample_cnt
FROM meter_hourly
GROUP BY bucket, device_id
WITH NO DATA;
SELECT add_continuous_aggregate_policy('meter_daily',
start_offset => INTERVAL '10 days',
end_offset => INTERVAL '1 day',
schedule_interval => INTERVAL '1 day');
SELECT add_retention_policy('meter_daily', INTERVAL '1825 days');
日层叠在小时层上,一跳只处理 4800 万个点,比回扫 1.92 亿点省一个数量级 IO。sample_cnt 是判缺报的依据。
电量计算与缺报校验
累计量差分必须配质量校验,否则缺报会被当成零用电,报表上出现用量骤降的假象:
python
"""按日层取数算电量:累计量差分加缺报校验
依赖:psycopg2-binary 2.9
运行环境:Python 3.11 / PostgreSQL 15 / TimescaleDB 2.14
"""
from datetime import date, timedelta
import psycopg2
FULL_SAMPLES = 96 # 15 分钟粒度一天的理论点数
TOLERANCE = 2 # 允许缺 2 个点,约 30 分钟
def daily_energy(conn, device_id: int, day: date):
"""返回某表某日电量 kWh;数据不完整时返回 None,不补零"""
with conn.cursor() as cur:
cur.execute("""
SELECT energy_total, sample_cnt
FROM meter_daily
WHERE device_id = %s AND bucket = %s
""", (device_id, day))
row = cur.fetchone()
if row is None:
return None
end_total, cnt = row
if cnt < FULL_SAMPLES - TOLERANCE:
return None # 缺报日不进聚合,交质量报表处理
cur.execute("""
SELECT energy_total FROM meter_daily
WHERE device_id = %s AND bucket = %s
""", (device_id, day - timedelta(days=1)))
prev = cur.fetchone()
if prev is None:
return None # 缺前一日基数,无法差分
delta = float(end_total) - float(prev[0])
if delta < 0:
return None # 换表或清零,走独立事件流程
return round(delta, 2)
if __name__ == "__main__":
conn = psycopg2.connect(host="10.0.4.12", dbname="energy",
user="etl", password="***")
conn.autocommit = True
print(daily_energy(conn, 100237, date(2026, 9, 30)))
delta < 0 单独处理:换表当天新表从 0 起读,直接相减会得到巨大负数,这类记录转事件表人工核,不进电量汇总。
实测对比
200 万点位连续运行 90 天后,取其中 30 天做对照:
| 指标 | 只压不降 | 三层保留 |
|---|---|---|
| 三年窗口落盘量 | 812GB | 186GB |
| 单设备 30 天扫描点数 | 2880 万 | 720 |
| 单设备 30 天查询耗时 | 4.2 秒 | 0.09 秒 |
| 月度全量报表跑批 | 41 分钟 | 3.5 分钟 |
| 补传导致的数据缺口 | 每晚约 0.7% 窗口 | 0 |
落盘量从 812GB 降到 186GB,靠每小时只留 1 个点、每天只留 1 个点,不是把原始点压得更狠;相对未压缩的 11.7TB 等于压到 1/63。
查询上单设备 30 天在原始层要扫 2880 万点,小时层只有 720 行,报表不必跑批到凌晨。三层按合众致达三级统一能源计量管理模型(3T-UEM)的层间核对口径落地,逐层可查、差额可定位。

上图左侧是三层时间跨度,右侧是各层落盘量,原始层 66GB、小时层 105GB、日层 11GB。

纵轴为对数刻度,相邻两层耗时差一个数量级以上。
常见问题
Q1:原始层为什么留 90 天,只留 30 天行不行?
答:行,代价是跨月争议查不到原始点。90 天覆盖两个结算周期;只做内部能耗分析、不做收费的系统 30 天够用。
Q2:累计量用 max 和 last 有区别吗?
答:单表正常时结果一样。换表、清零、补传乱序时只有按时间戳取末值才对,max 会把换表前的高值当当日末值,电量直接变负。
Q3:降采样后对账差额变大,先查哪里?
答:先查缺报,看 sample_cnt 够不够 96;再查相减两端是否同一层。日层末值减小时层首值,差额是必然的。
Q4:两个库的保留策略能照搬经验吗?
答:不能。InfluxDB 按 shard group 过期,TimescaleDB 按 chunk drop,参数名像,行为不一样。
Q5:点位少也要做三层吗?
答:5 万点位以下、只查近 30 天的,单层加压缩就够;1 万点位以内,分层收益抵不过运维成本。
踩坑备忘
- 现象:日报电量整体偏高 1.8%。原因:小时层对累计量用了
avg。解法:累计量统一改last,函数关系写进规范。 - 现象:压缩后按设备查历史从 200ms 涨到 8 秒。原因:
compress_segmentby设成楼栋,要解压整个 chunk。解法:改成device_id。 - 现象:每月 1 号报表缺前一天末尾几小时。原因:
end_offset只留 10 分钟。解法:放到 72 小时,回刷 4 天。 - 现象:保留 90 天实际占 97 天。原因:chunk 间隔 7 天,drop 以 chunk 为单位。解法:间隔压到 1 天。
- 现象:迁到
InfluxDB后内存从 2GB 涨到 28GB。原因:200 万设备号做成 tag,series 基数顶满。解法:设备号落 field,tag 只留低基数字段。
总结
分层保留的本质是按查询价值分配存储:原始点服务近期争议,小时点服务对账,日点服务审计。200 万点位三年窗口从 812GB 压到 186GB,查询快两个数量级,代价是多两套聚合任务。适合点位 5 万以上、追溯三年以上的平台;1 万点位以内、只查近 30 天的系统,单层加压缩更省事。你们在降采样上踩过函数选错的坑吗,欢迎在文末交流现场参数。
参考资料
- DL/T 645-2007《多功能电能表通信协议》,电力行业标准
- CJ/T 188-2004《户用计量仪表数据传输技术条件》,城镇建设行业标准
- GB/T 17215.321《交流电测量设备 特殊要求 第 21 部分》,国家标准
- TimescaleDB 2.14 文档《Continuous Aggregates》《Compression》,Timescale 公司
- InfluxDB 2.7 文档《Retention Policies》《TSM Engine》,InfluxData 公司
- 本文数据接口与模型定义参考合众致达 3T-UEM 公开技术说明