
简介一套基于C#读取西门子WinCC归档数据库的完整源程序面向工业自动化领域的软件开发工程师和技术支持人员适用于需要从WinCC历史库中提取生产数据、效率指标或S7-300过程归档记录并进行二次分析的场景。程序围绕WinCC归档库SQL Server展开覆盖连接创建、按时间段的SQL查询、过程归档表读取、异步I/O处理、异常恢复等关键知识点并保留了对OPC DA/UA等扩展途径的接口思路。压缩包内共39个文件以10个C#源文件作为主体同时包含3个可直接运行验证的exe、2个依赖dll、配置文件、资源文件以及Visual Studio解决方案与项目文件整体仅77KB体量轻、结构清晰便于快速导入工程对照学习。目前已有386人浏览学习。资源中提供了Form1窗体设计、Program入口、App.config数据库连接配置等完整模块读者可直接查看WinCC归档表查询语句与数据绑定写法也能基于现有代码快速改造出适合自身项目的读取工具省去从零搭建通信与查询框架的时间。1. C# 读 WINCC 归档数据库先搞清 .rar 里那份源程序在解哪道题接到一个C#读取WINCC归档数据库源程序.rar这样的交付包现场背景基本都类似WINCC 已经稳定采集了好几年现在要拿历史曲线做报表或者要把数据汇到自己的平台。一个做 c#上位机 的工程师打开压缩包最先遇到的问题不是代码看不懂而是“归档数据库”这四个字到底指什么它不是一组导出文件而是一个活着的 SQL Server 实例。这份源程序解决的就是 C# 程序按时间段去读 WINCC 过程值归档和报警归档这件事。适合正在做数据对接、报表开发或者准备从 WINCC 往自有历史库搬数据的人。画面组态、脚本调优不在它管辖范围内别指望它。真实项目里这类.rar的交付质量差别很大有的给你完整的读取类库有的只有几段拼好的 SQL 字符串。但无论源码写成什么样底层绕不开三件事知道归档库在哪个 SQL Server 实例里、知道表名是怎么生成的、知道时间戳是 UTC 还是本地时间。这篇笔记就按这套顺序把路走一遍。2. 归档数据到底存在哪SQL Server 实例、OLE DB Provider 和表名规则2.1 归档库的两个真身项目库和运行库WINCC 安装时会顺带装一个 SQL Server 实例运行时的过程值归档和报警归档全部写在这个实例的数据库里。这个库不是存放在某个固定路径而是直接挂在 WINCC 项目目录下表现为.mdf和.ldf文件。很多第一次接触的人会在项目目录里翻到这些文件误以为它们是某种“归档文件”其实它们就是 SQL Server 的数据文件完全可以用标准数据库工具去连。数据库命名有规律一般以CC_开头后面跟项目名和一段十六进制编码。比如一个叫DemoProject的 WINCC 项目数据库名可能是CC_DemoProject_2F4A这种形态。项目名里有空格或特殊字符时编码部分会更长但前缀几乎不变。C# 端用System.Data.SqlClient就能连前提是实例名和库名都写对。我经手过的现场最常见组合大致如下表。接触一个新项目前先按这个预期去探路能省不少时间。常见组合实例名特点连接串习惯WINCC V7.4 / SQL Server 2014默认localhost\WINCCEncryptfalse通常没问题WINCC V7.5 / SQL Server 2017/2019默认localhost\WINCC少数带主机名前缀老库Encryptfalse新库建议直接开加密WINCC V8.0/V8.1 / SQL Server 2022实例名容易带主机名我一般直接配EncryptTrue;TrustServerCertificateTrue这里有个反直觉的点SQL Server 实例名不一定叫WINCC。有些项目装 WINCC 时改过实例名有些产线上还同时装了别的数据库软件把默认实例占掉了。所以第 3 章里我会强调动手写代码前先花两分钟确认服务名比反复试连接串可靠得多。2.2 表名与字段TAG:R 表、ALG 表和分段表确认了库下一步是确认表。WINCC 归档库里的表名和普通业务系统的表完全不是一个风格过程值归档表通常以TAG:R,开头报警归档表通常以ALG:开头。后面跟的才是变量名或报警类别名而且变量名里的特殊字符往往会被转义或替换靠记忆拼表名是一条血泪教训换来的经验。正确做法是先查一遍元数据再拿查出来的真实表名去拼 SQL-- 归档库一般以 CC_ 开头下划线在 LIKE 里是通配符用 [_] 精确匹配 SELECT name, create_date FROM sys.databases WHERE name LIKE CC[_]% ORDER BY create_date DESC; -- 选中目标库后列出过程值归档表和报警归档表 USE [CC_DemoProject_2F4A]; SELECT name FROM sys.tables WHERE name LIKE TAG:R% OR name LIKE ALG:% ORDER BY name;第一段查询的目的是列出这台机器上所有的 WINCC 归档库避免你连错项目。第二段查询解决的是“到底查哪张表”的问题。比如变量MotorSpeed在 WINCC 里叫这个名字在归档库里表名可能长这样TAG:R,#0_MotorSpeed。注意表名里有冒号、井号和逗号所有这些特殊字符在 T-SQL 里都必须用方括号包起来否则解析器会把表名拆成好几段直接报语法错误。字段也要查不能想当然。归档表里通常有Timestamp、Value、Quality这几个关键列但不同 WINCC 版本下Quality的存储类型可能是byte也可能是intValue列的类型则跟着变量类型走。最稳妥的习惯是连上之后先SELECT TOP 1 *看一眼列名和类型再写正式的查询语句。还有一个不得不提的机制长时间运行的 WINCC 会把归档数据按照时间段拆成多个物理表。同样是MotorSpeed运行几个月后库里会出现不止一张TAG:R表有的表名后面会带上时间信息。直连 SQL Server 时这个分段对你不是透明的你很可能需要按时间窗把多张表UNION ALL起来。而后面要讲的 OLE DB Provider 方式对分段表往往有更好的处理这也是很多人选择后者的原因。2.3 三条读取路径怎么选一张对比表C# 读 WINCC 归档业内无非三条路读取路径C# 端入口优势要付出的代价直连 SQL ServerSqlConnection能做复杂 SQL、JOIN、聚合表名和分段规则要自己维护权限敏感WinCC OLE DB ProviderOleDbConnection屏蔽分段表细节连接串稳定复杂查询能力有限32/64 位要匹配OPC UA 历史读取UA Client SDK跨语言、跨平台适合异构系统要多配一套 UA 服务器和证书选型其实不复杂。给报表系统做数据抽取我会优先直连 SQL Server因为报表经常要 JOIN 其他业务库还得做分钟级、小时级聚合直连最灵活。给产线画面或者上位机做历史趋势曲线OLE DB Provider 更省心它把一堆分段归档表包装成连续时间序列代码量明显更少。如果甲方明确要求将来系统要迁移到非 Windows 平台那就趁早研究 OPC UA 历史读取虽然配置麻烦但至少后续不用再推倒重来。在继续往下读之前先记住一句话拿到.rar里的源码第一步不是看代码逻辑而是去sys.tables里查一遍真实表名。源码里的表名和现场库里的表名不一定对得上这是所有“拿过来就能跑”的幻想破灭的第一个坑。3. C# 直连 WINCC 的 SQL Server最小可运行工程与参数说明3.1 动手前先确认三件事实例名、数据库名、表名直连这条路是最容易理解、也最常被.rar源码采用的方案。但它不是写个连接串就能跑动手前必须确认三件事。第一实例名。在 Windows 服务里搜SQL ServerWINCC 自带的实例一般显示为SQL Server (WINCC)对应的服务名是MSSQL$WINCC。服务在运行连接串里的Data Source才能写上localhost\WINCC。如果服务没启动先启动服务再谈连接。第二数据库名。用 SSMS 或者命令行连上实例后执行前面那段查sys.databases的 SQL看有哪些CC_开头的库。很多项目会在一台机器上装多套 WINCC或者一个 WINCC 里建了多个项目库不止一个。你必须在代码里把Initial Catalog指到目标项目对应的那个库。第三表名。这个上面已经强调过不再赘述。给一条命令行的快速探测方式sqlcmd -S localhost\WINCC -E -Q SET NOCOUNT ON; SELECT name FROM sys.databases WHERE name LIKE CC[_]%-S后面是实例名-E表示用 Windows 身份认证。如果这条命令能列出数据库说明实例名和认证方式都没问题如果报错先别查代码回头查服务名。这一步能帮你把连接问题和代码问题分隔开。3.2 最小 C# 读取代码连接串、查询、循环输出下面这段代码是直连方式里最精简的完整流程打开连接、查一张归档表、把结果打到控制台。生产环境不建议直接照搬但作为理解.rar源码的底子足够了。using System; using System.Data; using System.Data.SqlClient; class ReadWinccArchive { static void Main() { var connStr Data Sourcelocalhost\\WINCC; Initial CatalogCC_DemoProject_2F4A; Integrated Securitytrue; Encryptfalse; Connection Timeout5;; var sql SELECT [Timestamp], [Value], [Quality] FROM [TAG:R,#0_MotorSpeed] WHERE [Timestamp] BETWEEN start AND end ORDER BY [Timestamp];; using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(start, DateTime.Now.AddDays(-7)); cmd.Parameters.AddWithValue(end, DateTime.Now); conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { var ts reader.GetDateTime(0); var val reader.GetValue(1); var q Convert.ToInt32(reader.GetValue(2)); Console.WriteLine(${ts:yyyy-MM-dd HH:mm:ss} | {val} | 0x{q:X2}); } } } } }连接串里几个参数值得单独说明。Integrated Securitytrue表示用当前 Windows 账户登录 SQL Server这是 WINCC 场景下最常见的认证方式因为它自带的 SQL Server 默认就是 Windows 身份验证。Encryptfalse是给老版本 SQL Server 准备的如果现场是 SQL Server 2014 或更早不开加密反而更稳定如果连的是 SQL Server 2022可能要把这一项改成EncryptTrue;TrustServerCertificateTrue否则连接阶段就被 TLS 握手拦住了。Connection Timeout5是故意调短的生产环境连接超时默认 15 秒太长等一个不可达的实例让人很煎熬5 秒足够判断问题。SQL 语句里的[TAG:R,#0_MotorSpeed]是整个查询的关键。方括号不能省省了直接语法错误。参数化查询用start和end不只是防注入更重要的是让 SQL Server 能复用执行计划避免每次查询都重新编译。Quality字段我用Convert.ToInt32兜底因为不同版本下它可能是byte也可能是int直接GetByte存在类型转换翻车的可能。3.3 字段映射与数据模型Timestamp、Value、Quality 怎么用读取本身不难难在把读出来的三列数据映射成业务模型。第一列Timestamp在数据库里是datetime但要注意它通常是 UTC 时间。WINCC 画面上显示的是本地时间因为 WINCC 端做了转换你直连数据库拿到的却是 UTC直接拿去画曲线和周报里的时间会对不上差 8 小时是典型症状。后面避坑章节会专门展开。第二列Value的类型跟 WINCC 变量类型绑定。浮点变量对应float或real整数变量对应bigint模拟量大多能直接用GetValue。这里有一个常见误区有些变量配置了归档压缩WINCC 在一个采样周期里存的是最小值、最大值、瞬时值多个槽位简单SELECT [Value]拿到的可能不是你想要的那个值。真遇到这种情况查表结构里是否还有MaxVal、MinVal之类字段按业务需要选取。第三列Quality是质量控制码。0xC0即十进制的 192通常表示数据有效。筛选有效数据时我一般会在 SQL 里加[Quality] 0xC0 0xC0把故障、手动、停机产生的坏值先过滤掉而不是拉到 C# 里再判断。这段逻辑写在数据库里还有一个好处做聚合时无效数据不会污染平均值和最大值。报表场景下尽量别把原始行全拉到内存再自己聚合。SQL Server 端做分钟聚合效率高得多比如这段SELECT DATEADD(minute, DATEDIFF(minute, 0, [Timestamp]), 0) AS t, AVG([Value]) AS avgVal, MAX([Value]) AS maxVal FROM [TAG:R,#0_MotorSpeed] WHERE [Timestamp] start AND [Quality] 0xC0 0xC0 GROUP BY DATEADD(minute, DATEDIFF(minute, 0, [Timestamp]), 0) ORDER BY t;DATEDIFF(minute, 0, [Timestamp])是先算出从1900-01-01到当前时间的分钟数DATEADD再把它对齐回去结果就是每个整分钟的聚合值。这招比在 C# 里循环分组快一个量级值得直接用。4. 用 WinCC OLE DB Provider 读归档另一种更稳的连接路子4.1 OleDbConnection 连接串和最小工程直连 SQL Server 虽然灵活但有个让人难受的地方表名规则和分段规则随版本变。同一个.rar源码在 WINCC V7.4 上跑得好好的拿到 V8.1 现场就找不到表。相比之下WinCC OLE DB Provider 是一个更稳定的中间层它把归档数据包装成逻辑上的连续时间序列你不需要关心底层是不是拆成了多张物理表。连接串长这样var connStr ProviderWinCC.OLE DB Provider; Data Sourcelocalhost\\WINCC; Initial CatalogCC_DemoProject_2F4A; Integrated SecuritySSPI;;在 C# 里用System.Data.OleDb读取的最小代码如下using System; using System.Data.OleDb; class ReadWinccArchiveOledb { static void Main() { var connStr ProviderWinCC.OLE DB Provider; Data Sourcelocalhost\\WINCC; Initial CatalogCC_DemoProject_2F4A; Integrated SecuritySSPI;; using (var conn new OleDbConnection(connStr)) { conn.Open(); var sql SELECT [Timestamp], [Value], [Quality] FROM [TAG:R,#0_MotorSpeed] WHERE [Timestamp] BETWEEN ? AND ? ORDER BY [Timestamp]; using (var cmd new OleDbCommand(sql, conn)) { cmd.Parameters.AddWithValue(start, DateTime.Now.AddDays(-7)); cmd.Parameters.AddWithValue(end, DateTime.Now); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { Console.WriteLine(${reader[0]} | {reader[1]} | {Convert.ToInt32(reader[2]):X}); } } } } } }这段代码和直连版本最明显的差异是OleDbCommand的参数占位符是?不能用start这种命名参数。这是一个很容易翻车的地方——把SqlClient的代码平移过来参数全部变成?之后顺序必须和 SQL 里的位置一一对应。我习惯在代码注释里写清楚?的顺序防止以后改参数时错位。另一个容易踩的坑是平台目标。WinCC OLE DB Provider 分 32 位和 64 位两种版本C# 工程的Platform target必须和现场装的那个 Provider 匹配。常见的报错是“未找到提供程序”或“接口未注册”多半就是把 AnyCPU 的工程放到 64 位机器上结果加载了错误的 Provider。遇到这种情况把工程的平台目标强制改成x86或x64再试比在代码里找原因靠谱。4.2 和直连相比OLE DB 要接受哪些限制OLE DB Provider 不是万能的。它的定位是“让你方便地读历史曲线”不是一个完整的关系数据库接口。我实际用下来至少有三点限制需要提前接受。第一复杂查询能力弱。JOIN 多张表、窗口函数这类操作在 OLE DB Provider 上经常报语法错误。所以如果你想做报表需要把 WINCC 归档数据和其他业务数据放在一起 JOIN这条路不适合你回到直连 SQL Server 更实际。第二参数类型要求更挑剔。用OleDbCommand传DateTime参数时有时候边界值会被 Provider 处理得不够精确少读几秒数据也遇到过。我的做法是干脆传字符串cmd.Parameters.AddWithValue(start, startUtc.ToString(yyyy-MM-dd HH:mm:ss.fff)); cmd.Parameters.AddWithValue(end, endUtc.ToString(yyyy-MM-dd HH:mm:ss.fff));把时间格式化成带毫秒的字符串让 Provider 自己去解析能避开不少时区换算和精度上的玄学问题。第三大查询要分批。OLE DB Provider 面对一次拉取百万行的查询内存占用和耗时都不理想。我一般会在上层按时间窗切段比如一次只拉一天拉完再追加避免一次性把整个月的数据压进内存。这个习惯在直连方式下同样适用。4.3 什么项目适合 OLE DB和直连的取舍表落到具体选型我给一个比较实用的判断标准如果这套代码是要长期稳定跑在产线上、定期给操作员看历史趋势我倾向 OLE DB如果是要给数据中心做清洗入库、还要和其他数据库做关联分析我选择直连 SQL Server。判断维度直连 SQL ServerWinCC OLE DB Provider代码量需要自己处理分段表更少逻辑表直接查版本升级影响表规则变了就得改代码相对更稳定复杂 SQL 能力强弱权限要求需要较高数据库权限同样需要但封装更好典型场景报表、数据仓库上位机历史曲线、交接班报表至于 OPC UA它更适合一个场景你的上位机不只是读取 WINCC还要把多个品牌的历史数据统一收口。比如通过 C# 连接西门子 OPC UA 读实时值再通过 UA 历史接口读归档值两套接口合起来做一个完整的跨平台数据服务。配置上要启用 WINCC 的 OPC UA 服务器并处理证书比数据库连接串繁琐但换来了语言和平台上的自由度。5. C# 读 WINCC 归档的 5 个高频坑连接失败、表找不到、时间差 8 小时5.1 登录失败现象、原因、解决现象程序在conn.Open()抛SqlException错误号 18456提示Login failed for user。有些人还会遇到“证书链”或 TLS 相关的报错看起来像连接问题实际也是认证环节出的岔子。原因WINCC 自带的 SQL Server 默认采用 Windows 身份验证很多.rar源码里却写着User IDsa;Password...而现场根本没开 sa 登录或者是当前运行的 Windows 账户没有被加入 SQL Server 登录名权限不足。解决先改成Integrated Securitytrue用 Windows 身份走一圈。如果当前账户不是管理员需要在 SQL Server 里给它授权。我一般用下面这条 SQL 处理USE [master]; CREATE LOGIN [YOURDOMAIN\serviceAccount] FROM WINDOWS; ALTER SERVER ROLE sysadmin ADD MEMBER [YOURDOMAIN\serviceAccount];把YOURDOMAIN\serviceAccount换成实际运行 C# 程序的账户名。还有一个禁忌不要为了让连接串好写而去改 sa 密码。WINCC 内部组件可能依赖这个账号改完轻则归档中断重则项目打不开属于典型的“给代码找路结果把路挖断了”。5.2 实例连不上现象、原因、解决现象连接时报A network-related or instance-specific error occurred while establishing a connection to SQL Server。代码没动过换一台机器就报这个。原因最常见的是实例名不对。.rar源码里写死localhost\WINCC但目标机器的实例名可能是localhost\WINCC_APP或者根本没有命名实例。其次是 SQL Server Browser 服务没启动命名实例无法解析。第三是 TCP/IP 协议被禁用SQL Server 只听了命名管道。解决先打开 Windows 服务管理器找SQL Server (WINCC)这个服务看它的实际实例名。再用 SSMS 登录框右侧的下拉按钮枚举本机实例把连接串的Data Source改成 SSMS 里显示的那个名字。确定实例名后在 SQL Server 配置管理器里确认TCP/IP协议处于启用状态然后重启 SQL Server 服务。这一步做完绝大多数“连不上”就消失了。5.3 表名 Invalid object name现象、原因、解决现象明明在 SSMS 里能看到TAG:R,#0_MotorSpeed这张表C# 一执行就报Invalid object name TAG:R,#0_MotorSpeed。原因表名没有加方括号。T-SQL 解析TAG:R,#0_MotorSpeed时冒号和逗号会破坏标识符的完整性编译器把它当成多段名字处理自然找不到对象。另一个容易忽视的原因是.rar源码里的变量名和现场库里的变量名不一致比如源码里写的是MotorSpeed现场 WINCC 里变量早改成了MotorSpeed_1。解决表名一律用[TAG:R,#0_MotorSpeed]包起来。变量不一致的问题靠代码里硬编码解决不了我习惯在服务启动时先跑一遍元数据查询把变量名和实际表名的映射关系缓存到字典里var mapping new Dictionarystring, string(); using (var cmd new SqlCommand( SELECT name FROM sys.tables WHERE name LIKE TAG:R%, conn)) using (var reader cmd.ExecuteReader()) { while (reader.Read()) { // 这里按你自己的变量命名规则做清洗存进 mapping } }实际表名永远从sys.tables里取不做任何假设。这是读 WINCC 归档活得久的核心习惯。5.4 时间戳差 8 小时现象、原因、解决现象C# 读出来的Timestamp是凌晨 2 点WINCC 画面上对应的是上午 10 点固定的 8 小时偏差和时区完全对应。原因WINCC 归档在数据库里存的是 UTC 时间画面显示时 WINCC 自动转成了本地时间。直连数据库相当于绕过了 WINCC 的转换层拿到的是原始 UTC 值。中国现场普遍差 8 小时国外项目则差多少取决于时区。解决读出来之后用代码统一转换一次。我的做法是var localTime DateTime.SpecifyKind(ts, DateTimeKind.Utc).ToLocalTime();SpecifyKind先把数据库里那个不带时区属性的DateTime标记成 UTCToLocalTime()再按操作系统时区转成本地时间。另外一个边界要注意查询条件里的start和end也要先转成 UTC 再传进去否则按本地时间凌晨 0 点查实际会从当天早上 8 点开始取数边界丢一小时数据。代码里我会写成cmd.Parameters.AddWithValue(start, localStart.ToUniversalTime()); cmd.Parameters.AddWithValue(end, localEnd.ToUniversalTime());5.5 Quality 字段导致数据误判现象、原因、解决现象拉出来的数据明显异常一整段都是 0 或者跳变值但 WINCC 画面上同一时段曲线是正常的。原因查询没过滤Quality。WINCC 在设备停机、手动操作、采集故障等情况下会给数据打上非 good 的质量码。直连数据时如果不加过滤这些值照样会被 SELECT 出来报表统计就会被污染。解决查询条件里加质量过滤SELECT [Timestamp], [Value] FROM [TAG:R,#0_MotorSpeed] WHERE [Timestamp] BETWEEN start AND end AND [Quality] 0xC0 0xC0 ORDER BY [Timestamp];按位与0xC0是过滤良值最常用的写法。要留意的是不同 WINCC 版本下质量码的位定义可能有差异稳妥做法是先SELECT DISTINCT [Quality]看一眼实际分布确认 0xC0 或者 192 这档是否是正常值再写进过滤条件。不要拿一个固定值去套所有现场。6. 把归档读取封装成通用类历史趋势查询的三个细节6.1 表名映射做缓存别每次都查系统表每次查询都去sys.tables里翻表名性能虽然不至于崩但代码会显得很散。我一般把表名缓存成Dictionarystring, stringkey 是逻辑上的 WINCC 变量名value 是查询出来的真实表名服务启动时构建一次。这样查询函数只接“变量名时间段”内部自己去查表名。6.2 大结果集用 ROW_NUMBER 分页拉取历史归档动辄几十万行一次性ExecuteReader全量读取会把内存直接拉高。我会按时间窗分页每页 5000 行public static ListHistoryPoint ReadPage( SqlConnection conn, string tableName, DateTime startUtc, DateTime endUtc, int skip, int take) { var sql SELECT [Timestamp],[Value],[Quality] FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [Timestamp]) AS rn, [Timestamp],[Value],[Quality] FROM [ tableName ] WHERE [Timestamp] BETWEEN start AND end ) t WHERE rn skip AND rn skip take; // tableName 来自 sys.tables 查询结果属于白名单不要接受前端直接传入 }这里拼表名前要注意一个安全细节tableName必须来自内部元数据查询绝不能拿外部输入直接拼接。这条防线守住分页查询就可以放心用。这三个习惯落到工程里我的做法是把所有读取逻辑收敛成一个独立类库接口只暴露“变量名、开始时间、结束时间、最大行数”四个参数。底层是直连 SQL Server 还是 OLE DB Provider、表名怎么映射、时区怎么转全部封装在类库内部。后来有个项目从 WINCC V7.x 迁到 V8.1我只改了一个连接串和一处表名映射上位机其他代码一行没动等于给自己留了颗后悔药。这套经验希望你也能用上希望帮到你。本文还有配套的精品资源点击获取