尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【系列:TDengine 工业物联网实战:从零搭起可运行系统 · 第 3 篇】

【系列:TDengine 工业物联网实战:从零搭起可运行系统 · 第 3 篇】 窗口聚合、每设备最新值、离线检测是时序查询三个高频场景。本文用 TDengine 实战代码讲 INTERVAL _wstart、LAST_ROW()、HAVING LAST(ts) 三种写法解释为什么聚合列敢拼 SQL、条件却必须参数绑定以及 31 天/5000 行边界背后的资源保护逻辑。设备看板前你盯着三块屏心里反复冒出三个问题这 10 秒的平均温度是多少每台设备的最新值是什么哪些设备已经掉线了这三个问题恰好对应时序查询的三个高频场景窗口聚合、每设备最新值、离线检测。上一篇我们用超级表 TAG 完成了建模没看过的读者可以回看第 2 篇今天进入查询层用TelemetryRepository.java的实战代码把这三种写法过一遍。第一板斧窗口聚合把时间切成桶先看最常用的场景按固定时间窗统计趋势。TelemetryRepository.aggregate方法的核心 SQL 如下Stringsql SELECT _wstart AS window_start, AVG(%s) AS average_value, MIN(%s) AS minimum_value, MAX(%s) AS maximum_value, COUNT(%s) AS samples FROM iot.telemetry WHERE device_id ? AND ts ? AND ts ? INTERVAL(%s) .formatted(safeMetric,safeMetric,safeMetric,safeMetric,safeInterval);INTERVAL(10s/30s/1m/5m/15m/1h/1d)是 TDengine 的分桶语法——把时间轴切成固定宽度的窗口每个窗口返回一行聚合函数只在该窗口内计算。关键在_wstart这个伪列它代表窗口的起始时间。配合_wend窗口结束和_wduration窗口时长能精确知道每个桶落在哪个时间段。前端画趋势图时横轴直接用_wstart不用在应用层自己换算。对比关系型 SQLINTERVAL 分桶 ≈ 手写GROUP BY 时间片但窗口是自动对齐的不用自己算区间边界。比如INTERVAL(10s)所有桶都对齐到整 10 秒。结果集每行是一个窗口包含window_start、平均值、最小值、最大值和样本数。AVG/MIN/MAX/COUNT 四个指标加 samples 样本数够渲染一张迷你趋势卡片了。requireMetric和requireInterval是白名单校验不合法直接抛IllegalArgumentException——这一点后面专门讲。第二板斧一条 SQL 拿所有设备最新值设备看板上「当前值」是最直观的模块。有两种写法对应不同场景。写法 A查单台设备最新一条SELECTts,temperature,...,status_code,sequence_noFROMiot.telemetryWHEREdevice_id?ORDERBYtsDESCLIMIT1按时间戳倒序取第一条逻辑简单直白。写法 B所有设备一次拿完SELECTLAST_ROW(*)FROMtelemetryPARTITIONBYdevice_id;LAST_ROW()返回每个分区的最后一行整行PARTITION BY device_id按设备分组。一条 SQL 就把全部设备的最新值拿回来应用层不用写循环。你熟悉的ROW_NUMBER() OVER (PARTITION BY ...)在这里完全不需要。有了最新值在线判定顺理成章。TelemetryService里的逻辑point.timestamp().isAfter(Instant.now().minus(ONLINE_THRESHOLD))其中ONLINE_THRESHOLD Duration.ofMinutes(5)——最新一条数据在 5 分钟内判定为在线。这是物联网里典型的「心跳」思路设备持续上报只要你还在说话就当你活着。第三板斧用 HAVING 揪出离线设备最新值回答「现在什么状态」离线检测要回答「谁消失了」。两者本质不同前者是查存在后者是查缺席。offlineDeviceIds的核心 SQLStringsql SELECT device_id FROM iot.telemetry GROUP BY device_id HAVING LAST(ts) ? LIMIT ? ;拆解GROUP BY device_id按设备分组LAST(ts)取出组内最后一条记录的时间戳HAVING LAST(ts) ?过滤出最后上报时间早于 cutoff 的设备——这些就是离线的。注意LAST(ts)是聚合函数不是伪列。它返回分组内最新一条记录的时间戳和_wstart有本质区别——一个是计算结果一个是窗口元数据。002_examples.sql里还有一个变体把最后上报时间也查出来排查起来更方便SELECTdevice_id,LAST(ts)ASlast_seenFROMtelemetryGROUPBYdevice_idHAVINGLAST(ts)NOW-5mNOW - 5m是 TDengine 的时间表达式等价于应用层传入 cutoff。HAVING 对聚合结果过滤语义和关系型 SQL 一致只是LAST()这个聚合函数是时序库特有的。换成关系型 SQL得自连接或窗口函数才能实现「取每组最后一条再过滤」TDengine 一行搞定。cutoff 的计算也有讲究cutoff now - minutesminutes 被限制在1~4320030 天防止误传一个天荒地老的时间把全表扫穿。为什么敢拼 SQL白名单 参数绑定双保险细心的读者会发现aggregate 方法里用了.formatted()拼 SQL而其他查询全部用的?参数绑定。这是不是 SQL 注入的隐患拼接的每个值都先过了白名单校验。参数绑定?占位device_id、start、end、offset、limit、cutoff——所有用户传入的原始值全部走? jdbc 参数绑定拼接.formatted()只有metric和interval——它们先经过requireMetric/requireInterval的Set.contains校验不合法直接抛异常METRICS白名单有 9 个指标名INTERVALS白名单有 7 个窗口值都是代码写死的枚举用户输入绕不过去。为什么不干脆全参数绑定因为INTERVAL(?) 和聚合函数列名不能参数化——TDengine 的驱动不支持列名占位符INTERVAL(?)也会被当成字面量。所以只能白名单校验后拼接。这是「受限拼接」的正确姿势先验证再拼接不信任任何原始输入。31 天、5000 行查询边界的资源保护逻辑时序库最怕什么大范围无 LIMIT 扫全表。一台设备每秒上报一条365 天就是 3153 万行全量查询直接拖垮集群。QueryRangeValidator用几个硬边界兜底。看application.yml的默认值tdengine:query-timeout-seconds:${TDENGINE_QUERY_TIMEOUT:30}max-range-days:${TDENGINE_MAX_RANGE_DAYS:31}max-page-size:${TDENGINE_MAX_PAGE_SIZE:5000}validate(start, end)三个条件——非空、start end、跨度 ≤ 31 天validateLimit1 ≤ limit ≤ 5000单页上限防止有人一把梭LIMIT 1000000validateOffsetoffset ≥ 0负数直接拒绝还有两个实战技巧。第一个是limit1 翻页repository.findTelemetry(...,safeLimit1)多查一行判断「是否还有下一页」PageResponse再截断回safeLimit。一次查询同时拿到数据和分页信息不用额外 COUNT。第二个是30 秒查询超时。query-timeout-seconds从环境变量注入默认 30 秒。慢查询直接掐掉避免拖垮连接池——生产环境里超时是比报错更重要的保护机制。小结三个问题三种套路回到开头的三个问题现在都有标准答案问题核心写法关键词这 10 秒平均温度多少INTERVAL 聚合函数_wstart、AVG/MIN/MAX/COUNT每台设备最新值是什么LAST_ROW(*) PARTITION BY在线判定 5 分钟阈值哪些设备掉线了GROUP BY HAVING LAST(ts)缺席检测、cutoff聚合看趋势窗口、最新看状态LAST_ROW、缺席看离线HAVING。再加上白名单拼接和边界控制一个完整的时序查询层就立起来了。下一篇文章进入写入侧用 Python 把真实设备数据灌进 TDengine。你会发现「查得对」的前提是「写得好」——两条线会在超级表模型上汇合。觉得有用点个关注持续获取优质内容。
返回列表