多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

WinCC累计值差值日报表:SQL实现与归档配置全攻略

WinCC累计值差值日报表:SQL实现与归档配置全攻略 做WinCC项目这些年被业主塞过来最多的一句话就是“给我做张报表每天24小时的数据注意我这个值是累计值你帮我算成每小时的差值。”这句话听着不难但真落地的时候里面全是细节累计值怎么取、差值在哪个环节算、归档配置不对数据为什么对不上、跨天清零又怎么处理。这篇文章就把我多次做wincc报表的完整思路和踩坑过程捋一遍尤其是“累计值差值日报表”这个需求从需求拆解讲到SQL实现再到归档配置和排错希望能帮大家少走点弯路。适合正在用WinCC做生产报表、能耗报表、产量统计的朋友参考也适合刚接手工控项目报表开发的新手。1. 先读懂需求日报表不是“抄24个数”那么简单1.1 区分累计值和瞬时值报表逻辑完全不同做报表之前先把变量分类理清楚。瞬时值看的是当前状态比如压力、温度、流量速度日报表显示整点快照或者小时平均值就可以累计值看的是从某个起点累积起来的总量比如电度、水流量积算、产量计数它只增不减除非复位。你想想家里水表月末看表底数减去月初表底数才知道这个月用了多少吨水把表底数直接报成“这个月用水量”是没有意义的。工业现场的道理完全一样。所以凡是从电表、流量积算仪、计数器进WinCC的变量做日报表时必须先算差值再谈显示。很多项目里甲方自己都没说清楚只丢一句“给我出日报表”。这时你要主动问三个问题这个变量是累计值还是瞬时值报表是按每小时一行出24行还是按整点快照出24个点累计值如果清零了怎么处理这三个问题不搞清楚后面返工概率极高。1.2 “每日24点数据”有两种理解别一开始就做反标题里“每日24点数据”这句话我见过至少两种理解。第一种是抄24个整点瞬时值0点、1点……23点各一行第二种是把一天切成24个小时段每个小时段显示该时段内的用量。结合后面那句“如果设置的是累计值计算每小时的差值”基本可以断定需求是第二种每天出24行每行是某个小时内的累计值增量。如果你只按第一种做了甲方会追着你改如果你两种都做了反而显得专业。实际项目里累计值日报表最常见的是下面这种布局时间区间电度差值(kWh)用水差值(m³)班次备注00:00-01:0012.53.6夜班正常01:00-02:0011.83.4夜班正常02:00-03:0013.23.8夜班正常...............23:00-24:0010.23.1中班正常日合计287.489.2--注意这里每一行的“差值”指的是该小时段内的增量而不是“这个小时结束时表上显示的数字”。这是整套报表的核心语义一定先跟甲方对齐。1.3 被截断的“和最”到底还需要哪些统计项标题最后“和最”两个字被截断了但根据经验后面大概率是“和最大值、最小值、平均值”或者“和日合计”。做报表需求调研时要把甲方这句话问全否则做出去不是缺列就是多列。一份完整的累计值日报表除了每小时差值通常还包括日合计、当天0点/24点的累计值底数。如果工厂是两班制或三班制还要有班次小计。瞬时量变量则经常需要小时平均、小时最大、小时最小。我给一个典型列结构做参考列类型说明时间区间每小时的起止时间跨班次时标注班次差值本小时末的累计值减去上一小时末的累计值累计值底数本小时末表上显示的实际累计值供核对日合计当天所有小时差值的总和班次小计按班次分段汇总的小计数数据状态标记该小时数据是否稀疏、是否发生复位把这些带齐了再写代码报表一版过的概率高很多。2. 为什么不能直接读变量累计值差值离不开归档机制2.1 WinCC里没有“累计值类型”有的是累计型过程量很多刚入门的人会找WinCC里有没有“累计值”这个属性结论是WinCC变量类型只有二进制、8/16/32位整数、浮点、文本这些并没有一个“累计值类型”的勾选项。我们常说的累计值本质是外部设备电表、流量计、PLC累加器算好后送进来的过程量。因此差值计算必须自己做可以在三个位置做PLC里算、WinCC脚本里算、报表查询里算。我的习惯是放到报表查询里算因为PLC里算要改下位机程序脚本里算会长期占用WinCC运行资源而SQL一条语句就能解决报表生成时现算现出平时不消耗任何性能。2.2 归档方式决定你能拿到多细的数据WinCC的变量归档有三种常见方式对报表取数的影响差别很大归档方式记录条件对差值报表的影响周期归档每固定时间记一条数据最完整适合差值计算变化归档数值变化才记录累计量长时间不变时点数太少小时末值可能缺失压缩归档在周期归档基础上按时间段压缩存AVG/MIN/MAX/LAST直接算差值容易失真对累计值做差值最理想的数据源是周期归档的原始数据。每个小时段内至少要能取到最后一条有效记录如果归档周期太长或者点数太少差值计算会缺脚。这里有个容易忽略的点归档周期和画面采集周期是两回事。你在画面上看到的数值刷新频率由画面采集周期决定而数据库里写不写历史记录由归档周期决定。有些工程师只改了画面采集周期忘了改归档周期结果报表里拉出来的数据颗粒度完全不对。2.3 差值计算的标准数学定义差值计算的本质很简单设T0时刻读取累计值C0T1时刻读取累计值C1那么T0到T1时段内的用量 C1 - C0。日报表每小时差值严格来说是“该小时最后一条累计值”减去“上一小时最后一条累计值”。这里用“最后一条”而不是“平均值”更不是“整点值”原因有两个一是采样时刻不一定恰好落在整点整点那条记录可能不存在二是累计值在小时段内可能一直在涨取该小时段最后一条最接近真实小时末状态。如果用SQL实现就是先按小时分组再在每个小时内按时间倒序取第一条之后用LAG窗口函数和上一小时的末值做差。具体的SQL写法在第4节展开。3. 报表实现路线对比在线表格控件、SQL直查、脚本导出我各试过一遍3.1 在线表格控件适合“看”历史不适合“算”差值WinCC自带的在线表格控件Online Table Control可以在画面里拖一个控件配置归档变量和时间区间自动生成历史数据表。优点是零开发配置一下就能看历史值。缺点是它查出来的是原始采样序列不能自动按小时分组不能自动算差值行和列的格式也不能完全自定义。如果硬要用得先在脚本里把差值算好再塞进表格那不如直接写SQL来得痛快。所以我的结论是这个控件适合操作员在线查历史值比如“昨天下午三点那台设备压力是多少”但不适合给生产部出日报表日报表要的是格式化、可打印、带统计的产出物。3.2 直接查归档库 VBS脚本导出是单项目交付最通用的组合WinCC所有归档数据都存在自带的后台数据库里具体来说是同机安装的SQL Server实例。用VBS脚本创建ADO连接写SQL按小时分组算出差值最后用Excel.Application导出文件这套流程是工控行业最通用的做法。这个组合的好处是不需要额外购买报表授权凡是装了WinCC的机器基本都能跑代码量适中客户要调整格式直接改脚本交付时把脚本挂到按钮上操作员点一下自动生成Excel或者用定时器每天凌晨自动跑。我曾经在一个水厂项目里用这套方案做了12张报表从电耗到流量到药剂用量全部是VBSSQLExcel。项目运行三年报表功能基本没动过。3.3 SSRS和C#小工具适合沉淀成标准品如果公司不是做一个项目而是要做一套“WinCC报表包”重复卖给甲方那建议用SQL Server Reporting ServicesSSRS或者C#写定时服务周期抓取归档库数据出PDF/Excel。SQL Server Reporting Services的好处是格式规范、权限管理成熟、支持定时邮件推送集团型项目很喜欢。C#写报表服务的灵活性更高可以嵌入你们自己的业务逻辑比如自动计算班次达成率、自动发企业消息。坏处也很明显前期开发成本高而且WinCC版本升级后归档库表结构可能有变化要跟着改。所以这条路线只适合产品化不适合单项目快速交付。3.4 我的选型建议和适用场景项目情况推荐路线理由单项目、快速交付全局脚本VBSSQL导出Excel零授权成本改格式方便客户会自己维护报表脚本界面按钮附使用说明操作员一键导出减少售后多项目、产品化SSRS或C#报表服务格式统一支持批量部署集团数采平台数据库视图标准报表平台与上层平台对接方便需要说明的是WinCC版本从v7.x到v8.x上面这些路线都成立。版本变化主要影响归档库内部结构不影响“查归档库算差值”这个思路本身。4. 核心实现SQL窗口函数把“每小时末累计值”取出来再用LAG算差值4.1 归档数据怎么取连接字符串和视图WinCC归档数据查询一般通过ADO/ODBC走后台数据库。常用的连接字符串长这样ProviderSQLOLEDB;Data Source.\WinCC;Initial CatalogWINCC;Integrated SecuritySSPI注意第四部分是重点Data Source里的实例名要按实际安装情况修改默认是.\WinCC但有的项目装的是命名实例。Initial Catalog是数据库名WinCC默认是WINCC。如果连接失败先在数据库管理工具里确认实例名和库名。WinCC归档数据在后台库里不是一张简单的平表而是按时间段切分的归档段。查询时一般会接触到类似TAG:R,_xxx的视图或表。不同版本的表名有差异我不建议读者把某个具体表名死记硬背正确做法是在数据库客户端里找到你们项目对应的归档视图确认时间字段和值字段的名称再往下写SQL。下面所有SQL里的ArchiveView都是示意表名落地时换成你们项目实际的归档视图名。4.2 把时间戳切成小时桶取每小时最后一条累计值先写最核心的一步把原始时间序列按小时分组然后在每个小时内取最后一条累计值。WITH raw_data AS ( SELECT SampleTime, Value FROM ArchiveView WHERE TagName 电度 AND SampleTime 2024-12-01 00:00:00 AND SampleTime 2024-12-02 00:00:00 ), hourly_last AS ( SELECT DATEADD(hour, DATEDIFF(hour, 0, SampleTime), 0) AS hour_start, SampleTime, Value, ROW_NUMBER() OVER ( PARTITION BY DATEADD(hour, DATEDIFF(hour, 0, SampleTime), 0) ORDER BY SampleTime DESC ) AS rn FROM raw_data ) SELECT hour_start, Value AS hour_end_value FROM hourly_last WHERE rn 1 ORDER BY hour_start这段SQL做了三件事第一用DATEADD和DATEDIFF把任意时间戳归到它所在小时的起点相当于把数据“切”成小时桶第二用ROW_NUMBER()在每个小时桶内按时间倒序编号最新一条标号为1第三取rn1的记录就是每个小时最后一条累计值。为什么用ROW_NUMBER()而不是直接MAX(Value)因为累计值在小时段内可能发生过清零再上涨MAX(Value)取到的可能不是时间上最后一条记录而是清零前的高值这样差值就错了。按时间倒序取最后一条才是严格正确的做法。4.3 用LAG窗口函数做相邻小时差值拿到每小时末值之后下一步就是和上一小时末值做差。SQL里用LAG窗口函数最方便它可以直接取前面一行的值WITH raw_data AS ( SELECT SampleTime, Value FROM ArchiveView WHERE TagName 电度 AND SampleTime 2024-12-01 00:00:00 AND SampleTime 2024-12-02 00:00:00 ), hourly_last AS ( SELECT DATEADD(hour, DATEDIFF(hour, 0, SampleTime), 0) AS hour_start, SampleTime, Value, ROW_NUMBER() OVER ( PARTITION BY DATEADD(hour, DATEDIFF(hour, 0, SampleTime), 0) ORDER BY SampleTime DESC ) AS rn FROM raw_data ), hourly_end AS ( SELECT hour_start, Value AS hour_end_value FROM hourly_last WHERE rn 1 ), hourly_diff AS ( SELECT hour_start, hour_end_value, LAG(hour_end_value) OVER (ORDER BY hour_start) AS prev_hour_value FROM hourly_end ) SELECT hour_start, hour_end_value, ISNULL(hour_end_value - prev_hour_value, 0) AS hour_diff FROM hourly_diff ORDER BY hour_start这里的关键点是LAG(hour_end_value) OVER (ORDER BY hour_start)会按小时顺序取出上一行的累计末值然后当前小时末值减去上一小时末值就是该小时的用量。第一小时因为没有“上一小时末值”LAG返回NULL所以用ISNULL(..., 0)兜底或者根据业务填成空值。如果第一小时从0点开始时累计量不是从0起那第一小时的真实用量本来就是“1点末值减去0点前最后一个值”需要额外补一个0点之前的历史点这个后面排错部分专门讲。4.4 日合计和扩展统计一天的总用量最简单的是把上面结果里的hour_diff加起来SELECT SUM(hour_diff) AS day_total FROM ( -- 上面那段 hour_diff 的完整子查询 ) AS t也可以直接用当天最后一个累计值减昨天最后一个累计值。两种算法理论上结果一致实际以哪个稳定用哪个。我的经验是如果归档数据完整小时差值累加的结果更可靠因为出现小时级别的复位时能被发现。如果你还需要瞬时量的小时平均、最大、最小可以在hourly桶上继续加聚合SELECT hour_start, AVG(Value) AS hour_avg, MAX(Value) AS hour_max, MIN(Value) AS hour_min, COUNT(Value) AS sample_count FROM raw_data GROUP BY DATEADD(hour, DATEDIFF(hour, 0, SampleTime), 0)这里的sample_count就是后面要说的“数据完整性”标记的数据来源——如果某个小时只有一条记录那这个小时的差值可信度就要打问号。4.5 VBS脚本落地连库、取数、写ExcelSQL想好了接下来就是把查询嵌进VBS脚本跑出结果写到Excel。核心骨架如下Dim conn, rs, sql Set conn CreateObject(ADODB.Connection) conn.ConnectionString ProviderSQLOLEDB;Data Source.\WinCC;Initial CatalogWINCC;Integrated SecuritySSPI conn.Open sql WITH raw_data AS (...) 这里放前面完整SQL Set rs conn.Execute(sql) Dim xlApp, xlBook, xlSheet Set xlApp CreateObject(Excel.Application) xlApp.Visible False Set xlBook xlApp.Workbooks.Add() Set xlSheet xlBook.Worksheets(1) 表头 xlSheet.Cells(1, 1).Value 时间区间 xlSheet.Cells(1, 2).Value 累计值差值 xlSheet.Cells(1, 3).Value 小时末累计值 Dim rowIdx rowIdx 2 Do While Not rs.EOF xlSheet.Cells(rowIdx, 1).Value rs(hour_start) xlSheet.Cells(rowIdx, 2).Value rs(hour_diff) xlSheet.Cells(rowIdx, 3).Value rs(hour_end_value) rowIdx rowIdx 1 rs.MoveNext Loop rs.Close conn.Close xlBook.SaveAs D:\Report\daily_report.xlsx xlBook.Close xlApp.Quit Set xlSheet Nothing Set xlBook Nothing Set xlApp Nothing这里提醒一句Excel对象用完一定要释放否则脚本跑几次之后服务器上会残留一堆EXCEL.EXE进程把内存吃光。我习惯在脚本里加一个错误处理On Error Resume Next配合Err判断在异常时先杀掉残留进程再退出。如果项目对稳定性要求高建议用定时触发器在凌晨执行生成前一天报表。5. 归档配置的坑归档周期、压缩归档和变量类型是数据准不准的前提5.1 归档周期决定差值可取的最小颗粒归档周期是差值报表的第一前提。假设累计值从仪表过来WinCC变量设成1小时归档一次你再怎么写SQL也拿不到小时内的“最后一条”因为整小时内可能只有一条记录前一小时的最后一条和当前小时的唯一一条之间隔了整整一小时差值算出来就是错的。我建议累计量归档周期设5到20秒瞬时量可以更短。每小时记录数可以用3600除以周期估算比如10秒周期每小时360条做报表绰绰有余。如果再短到1秒数据库膨胀速度会很快对差值报表的精度提升却几乎没有没必要。归档周期和采集周期是两个独立设置都在变量属性里。改完归档周期后要重新激活一次项目让归档配置生效。有些工程师改了参数发现没变化就是忘了重新激活。5.2 压缩归档会“吃掉”累计值的真实增量这里重点说压缩归档。WinCC的压缩归档会把一段时间比如1小时内的原始数据合成一条记录字段有平均值、最小值、最大值、最后值等。很多人图省事直接读压缩归档做报表结果累计值差值完全对不上。原因很简单累计值是个积分量它的“平均值”没有物理意义。假设这个小时前半段从1000涨到1100后半段从1100涨到1200平均值算出来可能是某个不存在的中间值拿平均值相减当然不对。如果实在要用压缩归档一定要用“最后值”字段做差值不要用平均值。但我的建议是把原始归档保留至少30天报表全走原始数据压缩归档只给WinCC趋势显示用。归档压缩的配置位置在WinCC归档管理里压缩周期可以设成1小时压缩字段选“最后值”给趋势用平均值给瞬时量报表备用。5.3 变量类型、溢出和数据复位外部仪表的累计值类型要特别小心。很多电表走Modbus RTU寄存器是32位DWORD4个字节的顺序还有ABCD、CDAB之分。WinCC变量如果按有符号16位接收数值超过32767就变成负数报表里会出现莫名其妙的跳变。正确做法是在通讯层把寄存器拼成32位无符号或浮点在WinCC侧统一用浮点变量接收。这样还能避免累计值超过32位上限后溢出。溢出或清零都会导致差值变成负值这个在第6节展开怎么判断。另外如果累计值来自PLC内部累加器要注意PLC程序是32位还是64位累加是否有自动清零逻辑。一些老项目的PLC累加器会在数值到达上限后自动滚动归零如果没有提前沟通报表里就会出现规律性的负差值。5.4 跨天、班次和服务器时区日报表按自然日切分是最常见需求但工厂往往有班次概念比如0到8点、8到16点、16到24点。SQL里小时分组默认按服务器本地时间如果WinCC服务器时区设置不对报表的起始小时会整体错位跨天那行对不上交接班。部署时把服务器时区固定为项目所在地时间并且让报表时间范围和交接班表对齐。如果项目里有多个子公司跨时区建议在报表脚本里把“报表本地时间”作为参数传入SQL而不是直接依赖服务器默认时间。有夏令时的地区一年里有两天小时数会多一个或少一个报表逻辑要提前考虑否则会出现25点或23点这种行。国内项目基本没有这个问题但如果做海外项目这块一定要提前问清楚。6. 排错实录报表空白、负差值、查询卡顿的完整排查链路6.1 报表全是空白先分清“归档问题”还是“查询问题”遇到报表空白不要一头扎进SQL。我一般按这套顺序排查第一步先用WinCC的历史趋势曲线控件拉同一个变量、同一个时间段的曲线。如果曲线有数说明归档有数据问题出在查询层如果曲线也是空的直接去变量归档属性里看是否勾选了“归档”再去检查WinCC归档进程是否启动、磁盘空间是否足够。第二步如果曲线有数而报表空检查SQL时间参数。很常见的坑是SQL查询的时间范围用字符串拼接中文系统日期格式是yyyy/m/d而SQL Server往往按yyyy-mm-dd或yyyymmdd解析一旦格式不匹配就查不到数据。建议在脚本里统一用FormatDateTime或直接拼成ISO格式。第三步检查TagName匹配。WinCC归档的变量名大小写、前后缀可能和画面变量不完全一致有的是内部名称、有的是外部名称查询语句里写错一个字符就查不到。在数据库客户端里先SELECT DISTINCT TagName看看实际存的名称。6.2 负差值和跳变识别“清零复位”而不是“数据异常”我碰到过一个真实案例某车间电度表每天凌晨会通讯闪断恢复后仪表清零重启结果那个小时的差值出现负几百紧接着下一个小时又突然正几百日合计凭空少了一段。处理思路是当相邻差值小于0时不要简单把这个负数写进报表先判断是不是一次复位。如果复位点对应仪表重启那么复位后那一小时的用量应该是“本小时末值减去0”也就是说该小时用量直接等于本小时末值因为仪表是从0重新开始累计的。如果负值很小可能是计量单位切换、乘法因子变了或者现场仪表重新拨码。这些情况要拿到仪表说明书和通讯配置确认不要自己在报表里瞎改。SQL里可以加一列标记CASE WHEN hour_diff 0 THEN 复位/异常 ELSE 正常 END AS data_status然后把“复位/异常”的标记行保留在报表里让甲方去现场确认。这个标记在项目验收时可以省掉大量扯皮。6.3 查询越来越慢跨归档段和索引失效WinCC把归档数据按时间切段存放一个查询跨多个归档段时SQL如果写得不好就会特别慢。常见错误是在WHERE条件里对时间字段做函数运算比如WHERE DATEPART(hour, SampleTime) 8这会让索引直接失效。正确做法是把时间范围写成区间比较WHERE SampleTime 2024-12-01 00:00:00 AND SampleTime 2024-12-02 00:00:00并且单次查询跨度控制在31天以内更长的数据按月拆分。如果VBS执行SQL时超时可以在连接字符串里加Connect Timeout30如果还是很慢优先检查是不是把归档视图全表扫了。另外报表并发也会拖慢查询。多台操作站同时点导出按钮时数据库压力会很大。我的做法是在脚本入口加一个互斥标记用一个网络文件夹或数据库表记录“正在生成中”第二个请求直接弹窗提示稍后再试。6.4 KepServerEX转发数据的质量戳和数据类型很多累计值不是PLC直接给WinCC而是通过KepServerEX从电表、流量计转发过来。OPC通讯里有个质量戳Bad质量时KepServerEX通常会把上一次的好值保持不变继续往WinCC推报表里就出现一段平线差值变成0通讯恢复后差值突然变大。所以做报表前先确认OPC链路的质量处理策略。最好在转发侧把Bad质量置为无效或者让WinCC侧对连续多次不变的时间戳做标记。SQL里可以查每个小时段的最后一条和第一条是否完全相等如果相等且持续整个小时就加一个“疑似通讯保持值”的标记提醒人工复核。另一个点Modbus仪表里的DWORD如果按INT解析会出负值。比如一块电表累计到30000 kWh时就超过16位有符号整数上限如果通讯层配置错误WinCC收到的值会突然变成负数。转发配置里一定要把数据类型配成Long/ULongWinCC侧用浮点接收中间再用脚本做一次范围检查。7. 把整套东西落地后我有几点实际操作心得7.1 差值运算尽量下沉到SQL别在VBS里逐行循环算我最开始做差值计算时是在VBS里逐行循环先取所有原始点再按小时遍历、相减。变量少还行几十个变量查一个月数据就卡爆了报表生成一次要半小时。后来把逻辑全部下沉到SQL窗口函数一条SQL完成分组、取末值、差值、合计报表从半小时变成几秒。这个经验直接决定了后面所有报表的写法凡是能在SQL里算的绝不在脚本里循环算。VBS只负责连数据库、执行SQL、填Excel越“笨”越好。7.2 报表交付前用历史趋势曲线抽查三天数据报表做出来先别急着交付拿历史趋势曲线抽查前三天数据。我一般会挑几个典型时段用电尖峰、交接班附近、跨零点那一小时。把趋势控件的曲线和日报表并排放比较对应时段差值是否一致。这个动作能发现很多“逻辑对但数据不对”的隐藏问题比如某个变量根本没有归档、仪表通讯一直断、时间分组边界错位等。等甲方自己在报表里发现数据不对再被叫去现场排查成本完全不一样。7.3 在报表里加“数据完整性”标记能帮你省掉大量答疑每张报表我都建议加一列“数据完整性”。SQL查询每个小时段的记录数如果记录数明显少于预期比如每小时少于60个采样点就在报表里标注“数据稀疏”。 这个标记的好处是甲方看到异常数据时你可以快速判断是生产原因还是采集链路问题不用每次都被拉去现场。很多情况下“那小时产量对不上”其实是当小时通讯中断导致的采集缺失加一个标记就解释清楚了。7.4 WinCC版本迁移时报表脚本怎么减少改动WinCC从v7.x到v8.x归档库结构有变化。我迁过一个v7.3SE项目到v8.1报表脚本里的SQL表名、视图名基本都要重配但窗口函数、Excel导出框架、定时触发逻辑全部可以复用。所以新项目如果预见到以后要升级尽量把“归档查询”封装成一个独立的函数或存储过程表结构变化时只改这一层报表界面和导出逻辑不用动。我自己在v7.x项目里就是先把所有查询语句集中到一个VBS函数库文件里后来升级时只改了一个文件画面和导出逻辑几乎没碰。这个习惯在项目维护阶段省下的时间远比当初多写几行代码多。
返回列表