ThingsBoard 核心优化:通过聚合表提升物联网遥测数据统计性能

本文针对物联网海量遥测数据的查询性能瓶颈,系统阐述通用聚合表的设计与实现路径,为企业级自定义报表统计提供一套高性能、可扩展的优化解决方案。

问题描述

前期基于遥测数据开发统计报表时,即发现查询性能存在严重短板;究其原因,设备以 1 分钟频次上报心跳数据,导致ks_tv表年数据量高达 5 亿条。当查询一个支局的整体功率因素时,由于设备数量多,常规查询已无法完成计算。而电量尖峰平谷的时段化统计,又必须依赖全量明细数据,无法通过粗粒度聚合规避性能问题。

  1. 当前设备数:实际物理设备数170个。
设备 类型 个数
网关 逻辑 2
用电主机 物理 17
传感器 逻辑 441
用电精灵 物理 153
用电精灵通道 逻辑 306
合计 逻辑 919
  1. 当前遥测表数据量统计(12月第二批设备接入)
月份 SQL 记录条数
2025.10 select count(*) from ts_kv_2025_10 3259,6974
2025.11 select count(*) from ts_kv_2025_11 3040,1911
2025.12 select count(*) from ts_kv_2025_12 8784,3354
  1. 数据量推算

当前按1000个设备计算,每个设备只上传一种遥测数据,1次/分钟,年数据量5亿。

bash 复制代码
日:1000 * 60 * 24 = 1,440,000 (144万)
月:1,440,000 * 30 = 43,200,000 (4千万)
年:43,200,000 * 12 = 518,400,000 (5亿)

问题分析

  1. 该类统计需求在业务场景中具备高度普遍性,且凸显三大核心特征:
  • 指标计算逻辑多元,有功电量需统计累计值、功率因素需计算平均值,各类指标均需按需适配对应的统计方法;
  • 统计维度灵活,可按资产目录或组织维度开展核算,因此需以设备作为基础统计单元,借助资产与设备的关联关系,实现设备数据的层级聚合;
  • 报表需支撑历史数据回溯统计,需要纵向缩短时间维度的数据,规则链更倾向于资产间汇总,故此方案不可采用。
    基于上述特征,本需求的核心优化目标明确为:通过对遥测数据实施按日汇总预处理,实现报表统计性能的质效提升
  1. 数据汇总结构
  • 既有遥测数据ts_tv的数据结构

    字段:entity_id,key,ts,bool_v,str_v,long_v,dbl_v,json_v

  • 新增通用遥测汇总数据ts_kv_daily的结构为

    字段:entity_id,key,date, sum_value,count_value,avg_value,min_value,max_value

  • 只有数值型才涉及到聚合,因而只针对数值型字段,进行记录。在ts_tv增加时,使用触发器同步添加到本表。哪些指标要汇总,直接在触发器中定义,不定义参数表。

问题解决

  1. 建表语句
sql 复制代码
CREATE TABLE ts_kv_daily (
    entity_id UUID NOT NULL,
    key INT NOT NULL,
    ts BIGINT NOT NULL,
    sum_value DOUBLE PRECISION DEFAULT 0,
    count_value BIGINT DEFAULT 0,
    avg_value DOUBLE PRECISION DEFAULT 0,
    min_value DOUBLE PRECISION DEFAULT NULL,
    max_value DOUBLE PRECISION DEFAULT NULL,
    UNIQUE (entity_id, key, ts)
);
  1. 创建触发器
sql 复制代码
CREATE OR REPLACE FUNCTION update_ts_kv_daily()
RETURNS TRIGGER AS $$
DECLARE
    v_date DATE;
    v_ts BIGINT;  -- 改为 BIGINT
    v_value DOUBLE PRECISION;
BEGIN
    -- 根据key决定是否处理,按需增加
    IF NEW.key NOT IN (96, 98) THEN
        RETURN NEW;
    END IF;
    
    -- 只处理数值型数据
    IF (NEW.dbl_v IS NOT NULL OR NEW.long_v IS NOT NULL) THEN
        v_value := COALESCE(NEW.dbl_v, NEW.long_v::DOUBLE PRECISION);
        
        -- 计算日期和时间戳
        v_date := DATE(
            (TO_TIMESTAMP(NEW.ts/1000) AT TIME ZONE 'UTC') 
            AT TIME ZONE 'Asia/Shanghai'
        );
        v_ts := (EXTRACT(EPOCH FROM 
            (v_date || ' 00:00:00 Asia/Shanghai')::TIMESTAMPTZ
        ) * 1000)::BIGINT;
		
        -- 插入或更新 ts_kv_daily 表
        INSERT INTO ts_kv_daily 
            (entity_id, key, ts, sum_value, count_value, avg_value, min_value, max_value)
        VALUES 
            (NEW.entity_id, NEW.key, v_ts, v_value, 1, v_value, v_value, v_value)
        ON CONFLICT (entity_id, key, ts) 
        DO UPDATE SET
            sum_value = ts_kv_daily.sum_value + EXCLUDED.sum_value,
            count_value = ts_kv_daily.count_value + 1,
            avg_value = (ts_kv_daily.sum_value + EXCLUDED.sum_value) / (ts_kv_daily.count_value + 1),
            min_value = LEAST(ts_kv_daily.min_value, EXCLUDED.min_value),
            max_value = GREATEST(ts_kv_daily.max_value, EXCLUDED.max_value);
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;


-- 删除存在的触发器
DROP TRIGGER IF EXISTS trg_update_daily_stats ON ts_kv;

-- 创建正确的触发器
CREATE TRIGGER trg_update_daily_stats
AFTER INSERT ON ts_kv
FOR EACH ROW
EXECUTE FUNCTION update_ts_kv_daily();
  1. 可以随时重新计算的存储过程,按天和指标计算,避免数据量太大造成耗时太长
sql 复制代码
CREATE OR REPLACE FUNCTION put_ts_kv_daily(
    p_date DATE,
    p_key INT
)
RETURNS VOID AS $$
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')::timestamp AT TIME ZONE 'Asia/Shanghai'
    ) * 1000)::BIGINT;
    
    v_end_ts := (EXTRACT(EPOCH FROM 
        (p_date::text || ' 23:59:59.999')::timestamp AT TIME ZONE 'Asia/Shanghai'
    ) * 1000)::BIGINT;
    
    -- 计算每日聚合的时间戳(北京时区当天的零点)
    v_daily_ts := (EXTRACT(EPOCH FROM 
        (p_date::text || ' 00:00:00 Asia/Shanghai')::timestamptz
    ) * 1000)::BIGINT;
    
    -- 删除旧数据(使用 ts 字段)
    DELETE FROM ts_kv_daily 
    WHERE ts = v_daily_ts 
      AND key = p_key;
    
    -- 聚合插入(使用 ts 字段)
    INSERT INTO ts_kv_daily (
        entity_id, key, ts, sum_value, count_value, avg_value, min_value, max_value
    )
    SELECT 
        entity_id,
        p_key,
        v_daily_ts,  -- 使用计算出的每日时间戳
        SUM(COALESCE(dbl_v, long_v::DOUBLE PRECISION)),
        COUNT(*),
        AVG(COALESCE(dbl_v, long_v::DOUBLE PRECISION)),
        MIN(COALESCE(dbl_v, long_v::DOUBLE PRECISION)),
        MAX(COALESCE(dbl_v, long_v::DOUBLE PRECISION))
    FROM ts_kv
    WHERE key = p_key
      AND ts >= v_start_ts
      AND ts <= v_end_ts
      AND (dbl_v IS NOT NULL OR long_v IS NOT NULL)
    GROUP BY entity_id;
    
    RAISE NOTICE '刷新完成: 日期=%, key=%, 时间戳=%', p_date, p_key, v_daily_ts;
END;
$$ LANGUAGE plpgsql;
  1. 调用一次过程刷新数据
sql 复制代码
-- 创建索引(包含查询所需的所有字段),否则执行put_ts_kv_daily会很慢
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_ts_kv_2025_12_key_ts_covering 
ON ts_kv_2025_12(key, ts, entity_id, dbl_v, long_v);

//若是要刷新多天,或者多个指标,则调用多次
SELECT put_ts_kv_daily('2025-12-31', 96);
SELECT put_ts_kv_daily('2025-12-30', 96);
SELECT put_ts_kv_daily('2025-12-29', 96);
SELECT put_ts_kv_daily('2025-12-28', 96);
SELECT put_ts_kv_daily('2025-12-27', 96);
SELECT put_ts_kv_daily('2025-12-26', 96);
SELECT put_ts_kv_daily('2025-12-25', 96);
SELECT put_ts_kv_daily('2025-12-24', 96);
SELECT put_ts_kv_daily('2025-12-23', 96);
SELECT put_ts_kv_daily('2025-12-22', 96);
SELECT put_ts_kv_daily('2025-12-21', 96);
SELECT put_ts_kv_daily('2025-12-20', 96);
SELECT put_ts_kv_daily('2025-12-19', 96);
SELECT put_ts_kv_daily('2025-12-18', 96);
SELECT put_ts_kv_daily('2025-12-17', 96);
SELECT put_ts_kv_daily('2025-12-16', 96);
SELECT put_ts_kv_daily('2025-12-15', 96);
SELECT put_ts_kv_daily('2025-12-14', 96);
SELECT put_ts_kv_daily('2025-12-13', 96);
SELECT put_ts_kv_daily('2025-12-12', 96);
SELECT put_ts_kv_daily('2025-12-11', 96);
SELECT put_ts_kv_daily('2025-12-10', 96);
SELECT put_ts_kv_daily('2025-12-09', 96);
SELECT put_ts_kv_daily('2025-12-08', 96);
SELECT put_ts_kv_daily('2025-12-07', 96);
SELECT put_ts_kv_daily('2025-12-06', 96);
SELECT put_ts_kv_daily('2025-12-05', 96);
SELECT put_ts_kv_daily('2025-12-04', 96);
SELECT put_ts_kv_daily('2025-12-03', 96);
SELECT put_ts_kv_daily('2025-12-02', 96);
SELECT put_ts_kv_daily('2025-12-01', 96);

-- 聚合完成后删除这个索引
DROP INDEX CONCURRENTLY IF EXISTS idx_ts_kv_2025_12_key_ts_covering;
相关推荐
q_30238195563 小时前
RK3588 + YOLOv8 田块分割实战指南:从环境搭建到部署落地全流程
人工智能·单片机·深度学习·神经网络·物联网·yolo
安科瑞刘鸿鹏173 小时前
企业配电系统中开关柜“可视化运行管理”的实现路径
大数据·运维·网络·物联网
一路往蓝-Anbo3 小时前
C语言从句柄到对象 (五) —— 虚函数表 (V-Table) 与 RAM 的救赎
c语言·开发语言·stm32·单片机·物联网
专业开发者4 小时前
蓝牙技术如何在不可靠的基础上构建可靠性
物联网
SmartRadio4 小时前
计算 CH584M-SX1262-W25Q16 组合最低功耗 (1)
c语言·开发语言·物联网·lora·lorawan
广州灵眸科技有限公司7 小时前
瑞芯微(EASY EAI)RV1126B RTC使用
单片机·物联网·实时音视频
三佛科技-134163842127 小时前
SM7055-18 输出18V 250mA低成本非隔离BUCK、 BUCK-BOOST方案典型应用电路(电磁炉方案)
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
Sui_Network7 小时前
Sui 2025 年终回顾:支付、BTC 与机构采用篇
大数据·人工智能·物联网·web3·去中心化·区块链
TDengine (老段)7 小时前
TDengine GROUP BY 与 PARTITION BY 使用及区别深度分析
大数据·开发语言·数据库·物联网·时序数据库·tdengine·涛思数据