1. 项目背景与需求分析
在物联网平台ThingsBoard的实际应用中,电能量数据统计是一个关键功能。传统方案使用触发器实时计算功率因素平均值,但对于用电量这种需要分时段统计的场景存在明显不足。核心痛点在于:
- 尖峰平谷时段定义存储在客户JSON配置中,触发器实时解析会极大影响性能
- 设备注册流程中存在"先上报数据,后分配客户"的时间差,导致触发器无法获取时段配置
- 月报统计通常只需截至昨日数据,实时性并非必要需求
实测表明,当设备数量超过5000台时,触发器方案会导致数据库CPU负载长期超过80%,严重影响系统稳定性。这促使我们转向更高效的定时批处理方案。
2. 数据库结构优化方案
2.1 表结构扩展设计
为支持分时段统计,我们对ts_kv_daily表进行了垂直分片扩展:
ALTER TABLE ts_kv_daily ADD COLUMN sum_value_a DOUBLE PRECISION DEFAULT 0; ALTER TABLE ts_kv_daily ADD COLUMN sum_value_b DOUBLE PRECISION DEFAULT 0; ALTER TABLE ts_kv_daily ADD COLUMN sum_value_c DOUBLE PRECISION DEFAULT 0; ALTER TABLE ts_kv_daily ADD COLUMN sum_value_d DOUBLE PRECISION DEFAULT 0;字段设计考虑:
- 保留原sum_value作为总量汇总
- 新增a/b/c/d四个时段字段对应尖/峰/平/谷
- 采用DOUBLE PRECISION确保高精度计算
- 默认值0避免NULL值计算问题
2.2 时段规则解析函数
创建专用函数解析客户配置的JSON规则:
CREATE OR REPLACE FUNCTION public.parse_electric_quantity_rule( p_rule jsonb ) RETURNS TABLE( month integer, time_type text, start_time time, end_time time ) LANGUAGE 'sql' AS $BODY$ SELECT (e->>'month')::integer, e->>'timeType', (e->>'startTime')::time, (e->>'endTime')::time FROM jsonb_array_elements(p_rule->'rules') e $BODY$;该函数处理形如以下的JSON配置:
{ "rules": [ { "month": 7, "timeType": "times_a", "startTime": "08:00:00", "endTime": "11:00:00" } ] }3. 核心聚合函数实现
3.1 函数主体结构
CREATE OR REPLACE FUNCTION public.put_ts_kv_daily_b( p_date date, p_key integer ) RETURNS void LANGUAGE 'plpgsql' AS $BODY$ DECLARE v_start_ts BIGINT; v_end_ts BIGINT; v_daily_ts BIGINT; BEGIN -- 时间戳计算 v_start_ts := (EXTRACT(EPOCH FROM (p_date::text || ' 00:00:00 Asia/Shanghai')::timestamptz ) * 1000)::BIGINT; v_end_ts := v_start_ts + (24 * 60 * 60 * 1000) - 1; v_daily_ts := v_start_ts; -- 主聚合逻辑 WITH customer_parsed_rules AS ( -- 规则解析子查询 ) INSERT INTO ts_kv_daily ( -- 字段列表 ) SELECT -- 聚合计算 FROM ts_kv a -- 多表关联 WHERE a.key = p_key AND a.ts >= v_start_ts AND a.ts <= v_end_ts GROUP BY a.entity_id ON CONFLICT (entity_id, key, ts) DO UPDATE SET -- 冲突更新 RAISE NOTICE '日统计完成: 日期=%, key=%', p_date, p_key; END; $BODY$;3.2 关键实现细节
时间范围计算:
- 使用EXTRACT(EPOCH FROM...)获取Unix时间戳
- 乘以1000转换为毫秒级时间戳
- 考虑时区转换(Asia/Shanghai)
规则解析CTE:
SELECT ak.entity_id as customer_id, per.month, per.time_type, per.start_time, per.end_time FROM attribute_kv ak INNER JOIN key_dictionary kd ON kd.key_id = ak.attribute_key JOIN public.parse_electric_quantity_rule(ak.json_v::jsonb) per ON TRUE WHERE kd.key = '尖峰平谷'分时段聚合逻辑:
SUM(CASE WHEN cpr.time_type = 'times_a' THEN COALESCE(a.dbl_v, a.long_v::DOUBLE PRECISION) ELSE 0 END)::NUMERIC(22,2) as sum_value_a冲突处理:
ON CONFLICT (entity_id, key, ts) DO UPDATE SET sum_value = EXCLUDED.sum_value, -- 其他字段更新
4. 定时任务配置
4.1 pgAgent作业设置
-- 每天凌晨2点执行 SELECT pgagent.pga_job_add( '每日电能量汇总', '00:02:00'::interval, true, 'SELECT put_ts_kv_daily_b(CURRENT_DATE - 1, 98);' );4.2 执行策略优化
- 错峰执行:选择业务低峰期(凌晨2点)
- 分批处理:超过10万设备时按customer_id分片
- 失败重试:配置最多3次重试机制
5. 性能对比测试
测试环境配置:
- CPU: 16核 Intel Xeon
- 内存: 32GB
- PostgreSQL 12.5
- 测试数据量: 1000万条ts_kv记录
| 方案 | 执行时间 | CPU峰值 | 锁等待时间 |
|---|---|---|---|
| 触发器方案 | 持续高负载 | 85% | 1200ms |
| 批处理方案 | 23分钟 | 62% | 50ms |
优化效果:
- 查询性能提升4倍
- 数据库负载降低30%
- 锁争用减少98%
6. 常见问题处理
6.1 时区不一致问题
现象:统计结果时间范围偏移 解决方案:
-- 确保函数内时区与系统一致 SET TIME ZONE 'Asia/Shanghai';6.2 规则解析失败
排查步骤:
- 检查attribute_kv表中key='尖峰平谷'的记录
- 验证JSON格式是否符合规范
- 测试parse_electric_quantity_rule函数
6.3 数据重复计算
预防措施:
-- 执行前清除当日已有数据 DELETE FROM ts_kv_daily WHERE key = p_key AND ts = v_daily_ts;7. 扩展应用场景
本方案同样适用于:
- 分时段计费的水气数据统计
- 生产设备分班次运行时长统计
- 交通流量高峰时段分析
实际案例:某电网项目应用后,月报生成时间从3小时缩短到15分钟,同时支持了更灵活的分时电价统计需求。