
简介面向工业自动化开发者的C#读取WinCC归档数据库示例工程聚焦S7-300过程归档与历史数据查询场景。压缩包内含完整Visual Studio项目共39个文件10个C#源文件、3个可直接运行的exe、2个dll动态库另有配置、资源、解决方案与少量缓存文件整体仅77KB便于快速参考。已有386人学习下载。代码演示了通过System.Data.SqlClient建立WinCC归档库连接、编写SQL检索时间区间数据、封装数据访问对象并涉及异步编程、异常处理与权限安全等关键点清晰的项目结构包含Form1窗体、Program入口与App.config配置适合需要从PLC或WinCC读取归档数据的初学者与集成工程师。通过阅读源程序可理解WinCC历史库表结构与C#调用方式掌握数据库连接串配置与查询封装技巧并复用其核心类缩短自研读取模块开发周期。1. 先搞明白“C#读取WINCC归档数据库”到底是什么需求读归档之前先记住一个关键事实WINCC 的历史数据并不躺在某个私有格式的文件里而是落在它自己内置的 SQL Server 实例中。也就是说用 C# 的 SqlClient 连上那个实例像查普通业务库一样做 SELECT就能把一年甚至几年的归档值捞出来。像“C#读取WINCC归档数据库源程序.rar”这类交付包打包的通常就是一套连接、探查、查询、类型转换和导出逻辑目标很直接让 C# 上位机能拿到历史数据做报表、画曲线、转储给上层系统。适合正在做 C# 上位机、要接西门子 WINCC 历史数据的工程师也适合刚入门 C# 但被现场数据需求砸到的新手。2. 连接串与归档表结构先确认实例名、库名和列名2.1 归档库的本质一个被 WINCC 托管起来的 SQL ServerWINCC 从 6.0 开始不再用私有文件保存历史归档而是统一写入内置的 SQL Server 实例。这个实例的默认名字一般是“机器名\WINCC”归档库名通常带工程名前缀。很多现场工程师第一次拿到这类源程序时最大的心理障碍就在“WINCC”这个名字上以为一定要装西门子专有的数据访问组件。实际上 C# 侧要做的只是把 SqlClient 指向那个实例然后执行 SELECT。常用两种连接串写法// 方式一SqlClient跨 .NET Framework / .NET 6 均可用 string connStr Data Source.\WINCC;Initial CatalogCC_Demo_Archive;User IDsa;Password123456;Connect Timeout30;; // 方式二OLEDB老系统兼容性考虑 string connStr ProviderSQLOLEDB;Data Source.\WINCC;Initial CatalogCC_Demo_Archive;User IDsa;Password123456;;参数说明Data Source.\WINCC.\代表本机远程连接时改成服务器名\WINCC。User IDsa;Password...要求 SQL Server 启用混合验证。如果现场配置的是 Windows 验证直接去掉用户密码改用Integrated SecurityTrue。Connect Timeout30归档实例首次冷启动时要加载大量库页缓存默认 15 秒经常不够我习惯给 30 秒以上。很多现场工程师包括我自己早期都卡在Initial Catalog上拿着示例库名去套现场结果库里根本没有这个名字。归档库的真实库名在项目部署时决定后面可能还跟着日期后缀。所以拿到任何源程序第一件事不是改密码是先在实例上把数据库列表捞出来SELECT name FROM sys.databases WHERE name LIKE CC\_% ESCAPE \ ORDER BY name;这段 SQL 的逻辑LIKE CC\_%匹配以CC_开头的库名ESCAPE \把下划线从通配符转义成普通字符否则_会匹配任意一个字符把不想看的库也带上。列出的结果里挑出当前工程的归档库替换到连接串里再继续。提示如果这里就报“找不到服务器实例”先确认两件事SQL ServerWINCC服务是否在运行远程连接时是否启用了 TCP/IP。很多连不上不是代码问题而是服务或协议没起来。2.2 用 SQL 探查表结构别让列名真面目坑了你如果你拿到的是一份写好的源程序最想删掉的可能就是我接下来这段话先别急着找“读数据的那个方法”花十分钟把归档库的表结构探出来。WINCC 归档库在不同版本、不同项目配置下时间轴表、值表、变量定义表的名字经常变。写死的表名和列名换个现场就翻车。SELECT t.name AS TableName, c.name AS ColumnName, ty.name AS DataType FROM sys.tables t INNER JOIN sys.columns c ON t.object_id c.object_id INNER JOIN sys.types ty ON c.user_type_id ty.user_type_id WHERE t.name LIKE %ARCH% OR t.name LIKE %TIMEBAR% OR t.name LIKE %ALG% OR t.name LIKE %MSG% ORDER BY t.name, c.column_id;这段 SQL 用sys.tables、sys.columns、sys.types三张系统视图拼出“表—列—数据类型”的宽表WHERE 筛掉和归档无关的对象。拿到结果后重点看三件事时间轴表叫什么时间列是不是datetime。值表叫什么值列是float、real还是被包成varbinary。变量定义表和值表之间用哪个 ID 关联。探完表结构我一般会把结果整理成下面这样一张表放在工程注释里方便接手的人快速对照作用常见表名常见列时间轴TIMEBAROPTTIME, TIMEBARID归档值ARCHIVEVALUE, VALUEID, TIMEBARID变量定义PROCESSPARAMSVALUEID, NAME注意以上表名和列名是我在项目里常见的样式不同 WINCC 版本存在差异。现场以刚才的探查结果为准不要拿着这张表硬套。2.3 三条读取路线选型SqlClient、OLEDB 还是 OPC UA“怎么读归档”这个问题的答案不是唯一的。按现场约束不同我通常会从三条路线里面选SqlClient直接连 WINCC 内嵌 SQL Server权限够、表结构清楚时最理想。OLEDB老项目、老驱动环境偶尔会遇到连接串带一个 Provider 前缀但要格外注意驱动位数。OPC UAWINCC 7.x 以后支持配置 OPC UA ServerC# 走 UA 客户端读历史数据不用面对表结构但要在 WINCC 侧做证书和用户配置。选型建议用一张表说清路线连接要点权限要求适用场景SqlClientData Source.\WINCCSQL 账号或 Windows 账号报表、批量导出、离线分析OLEDBProviderSQLOLEDB;Data Source...同上老驱动、老系统兼容OPC UAopc.tcp://IP:端口UA 用户、证书配置在线历史混合读取、不想碰数据库很多 C# 上位机框架里同时封装两条通道在线值走 OPC比如 C# 连接西门子 OPC 服务器拿实时数据历史归档走 SqlClient 直接读数据库。如果你只需要历史报表SqlClient 足够没必要为了读归档强行上 OPC UA如果既读在线又要历史OPC UA 的配置成本就值得付了。3. 用 C# 把归档历史数据读出来三段能直接改的代码3.1 冒烟测试连库、读最近记录、打印列名拿到归档需求我不会一上来就写完整查询而是先跑一个最小的冒烟测试连接库、读最近 100 条、打印出来。代码量控制在 30 行以内跑通了再扩展。using System; using System.Data.SqlClient; class ArchiveSmokeTest { static void Main() { string connStr Data Source.\WINCC;Initial CatalogCC_Demo_Archive;User IDsa;Password123456;Connect Timeout30;; string sql SELECT TOP 100 t.OPTTIME, a.VALUE FROM TIMEBAR t INNER JOIN ARCHIVE a ON t.TIMEBARID a.TIMEBARID ORDER BY t.OPTTIME DESC;; using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlCommand cmd new SqlCommand(sql, conn)) using (SqlDataReader reader cmd.ExecuteReader()) { while (reader.Read()) { DateTime time reader.GetDateTime(0); double value Convert.ToDouble(reader[VALUE]); Console.WriteLine(${time:yyyy-MM-dd HH:mm:ss} {value}); } } } } }逻辑说明using块保证连接随作用域释放TOP 100限流避免把大表全量拖回Convert.ToDouble(reader[VALUE])是对值类型的防御写法。参数上Connect Timeout30应对冷缓存.\\WINCC是本地实例。如果这里就抛登录失败或库名不对先回第 2 章把连接串和表结构确认一遍不要继续往下调。这一段跑通后可以顺手用reader.GetSchemaTable()把列打出来跟 2.2 节探查结果对一遍。很多时候你以为的列名和实际库里的列名就差一两个字母静默出错比报错更难受。3.2 按时间窗过滤参数化查询与分段策略正式做报表时查询条件基本都是“给我某段时间的数据”。这里最不推荐的做法是把时间字符串直接拼进 SQL比如WHERE OPTTIME 2024-01-01 00:00:00字符串格式稍有偏差就查不到或者误报。推荐参数化查询string sql SELECT t.OPTTIME, a.VALUE, a.VALUESTATE FROM TIMEBAR t INNER JOIN ARCHIVE a ON t.TIMEBARID a.TIMEBARID WHERE t.OPTTIME start AND t.OPTTIME end ORDER BY t.OPTTIME ASC;; using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.Add(start, SqlDbType.DateTime).Value startTime; cmd.Parameters.Add(end, SqlDbType.DateTime).Value endTime; using (SqlDataReader reader cmd.ExecuteReader()) { while (reader.Read()) { // 逐行处理不要在这里攒字符串 } } }代码里用 start AND end开区间而不是 BETWEEN原因很简单BETWEEN 是闭区间秒级数据在边界处会重复相邻分段用开区间写法能划清边界避免同一条记录被读两次。参数显式指定SqlDbType.DateTime比AddWithValue更利于 SQL Server 的索引选择大数据量下的差异非常明显。实际项目里查一年数据时我会按天循环DateTime cursor startTime; while (cursor endTime) { DateTime next cursor.AddDays(1); if (next endTime) next endTime; ReadArchiveRange(cursor, next); // 每次只查一天 cursor next; }分段的意义在于归档表数据量随时可能上亿一次性查一年会把 SQL Server 和 C# 两侧内存同时打爆。单段 10 万行以内是经验值具体要看归档变量的数量和采集频率。分段之后每一段独立查询、独立处理即使中途断了也只要从断点继续不用从头再来。3.3 值类型转换与 CSV 导出告别一读就崩另一个高频坑是归档值列未必是 double。不同版本的 WINCC 可能把值存成float、real、decimal甚至byte[]直接reader.GetDouble(列名)在某些列类型下会抛 InvalidCastException。我通常会写一个统一转换函数private static double ToDouble(object value) { switch (value) { case float f: return f; case double d: return d; case int i: return i; case decimal m: return (double)m; case byte[] bytes when bytes.Length 4: return BitConverter.ToDouble(bytes, 0); default: throw new InvalidCastException($不支持的归档值类型: {value?.GetType().Name}); } }case byte[] bytes when bytes.Length 4是 C# 的模式匹配写法专门处理值列被存成二进制的情况。优先按 double 解包解不了再走异常分支至少能给出明确提示而不是模棱两可的类型转换报错。导出 CSV 时也要注意编码和写入方式using (StreamWriter sw new StreamWriter(D:\archive_export.csv, false, Encoding.UTF8)) { sw.WriteLine(Time,Value,State); foreach (DataRow row in dt.Rows) { sw.WriteLine(${row[Time]},{row[Value]},{row[State]}); } }new StreamWriter第二个参数false表示覆盖写Encoding.UTF8显式指定避免中文系统下默认编码导致 Excel 打开乱码。逐行写而不是拼一个大字符串再File.WriteAllText是为了控制内存峰值。如果导出中途失败还能从已写入的行数判断进度不至于全丢。4. 读取 WINCC 归档库的避坑清单最容易翻车的 5 个位置4.1 登录失败实例名、库名和账号三位要对齐现象SqlConnection.Open()抛Login failed for user sa或者Cannot open database CC_xxx requested by the login。原因现场 WINCC 内置 SQL 实例的 sa 密码和你代码里写的不一致或者Initial Catalog根本不存在。这类问题看着像权限问题其实经常是库名没对齐。解决先用 SQL 管理工具以 Windows 身份登录.\WINCC实例列出所有数据库USE [master]; GO SELECT name FROM sys.databases; GO然后用一条测试连接确认账号可用。如果 sa 密码实在不知道优先考虑改成Integrated SecurityTrue因为 WINCC 服务一般用本地 Windows 账号跑这个账号大概率能访问归档库。不到万不得已别去重置 sa 密码否则 WINCC 运行时自己反而可能连不上归档库影响面更大。4.2 平台位数不匹配OLEDB 驱动加载崩溃现象程序一运行到new OleDbConnection()就抛“试图加载格式不正确的程序”或者报缺少 Provider而同一台机器上用管理工具却连得挺好。原因老 OLEDB 驱动是 32 位的C# 进程在 x64 模式下加载失败。最常见就是 Visual Studio 默认的 Any CPU 在 64 位系统上跑成 64 位进程老驱动根本加载不进去。解决能换 SqlClient 就换 SqlClient这是最省事的方案。确需保留 OLEDB 时把工程属性里的“首选 32 位”按驱动实际位数设成一致发布机器也要装对应位数驱动。检查方法很简单用Environment.Is64BitProcess打印一下当前进程位数再对照驱动版本基本一眼能定位。4.3 查询超时与内存暴涨一次把全库拉满了现象查询跑几十秒后抛 CommandTimeout或者 DataTable 占用上 GB 内存程序直接卡死。原因没有加时间过滤或者过滤条件没有落到索引上归档表是高频写入表几年数据全量加载SQL Server 生成结果集和 C# 装载结果集两边同时吃紧。解决先TOP N冒烟再按时间分段每段处理完释放 DataTable。如果查询仍然慢先看执行计划重点确认时间列的过滤有没有走索引有的归档表时间列确实没索引那就要和现场确认能否在停机窗口补一个归档库是 WINCC 在维护加索引前一定要评估。4.4 时间偏差 8 小时UTC 和本地时间混着算现象读出来的OPTTIME和 WINCC 趋势画面对不上普遍差 8 小时或一个固定偏移。原因归档库存的时间不同项目可能存本地时间也可能存 UTCWinCC 在画面层又做了时区转换C# 默认把读到的DateTime当 UnspecifiedToString()时又按本机时区格式化一来一回就偏了。解决先做一次对照实验从库里读一条已知时间的数据和 WINCC 原始趋势画面对比确认库内到底是哪种语义再统一处理DateTime raw reader.GetDateTime(0); DateTime displayTime DateTime.SpecifyKind(raw, DateTimeKind.Unspecified).ToLocalTime();DateTime.SpecifyKind只是设置 Kind 标记不改变数值ToLocalTime()才做真正转换。如果库内已经是本地时间就不要调 ToLocalTime 了。团队里最好固化这一段逻辑别在导出层临时补小时数那样最容易改出隐藏 bug。4.5 运行中的 WINCC 被查询拖住归档写入受影响现象一边用 C# 读历史数据一边 WINCC 画面上的趋势出现空洞或者归档写入明显变慢。原因查询长时间占用资源或者锁竞争影响到了 WINCC 的归档写入进程。归档写入是 WINCC 的核心功能读写冲突处理不好就会丢数据。解决查询都加只读隔离级别避免长时间持有不必要的锁SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;读库尽量挑停机或低峰时段。如果现场必须实时读就在应用层加并发开关控制同时运行的查询数量和单次查询时长避免多个大查询同时打在归档实例上。5. 把读取做成断点续读增量拉取与正确性自查前面的代码能跑通全量接下来就要考虑增量问题如果程序每小时拉一次归档总不能每次把好几年的数据重查一遍。常见做法是记断点用时间字段做增量标记。程序把上次成功读取到的最大OPTTIME存到配置或数据库下次查询从该时间起读再往后压一个边界防止漏掉边界秒string sql SELECT t.OPTTIME, a.VALUE FROM TIMEBAR t INNER JOIN ARCHIVE a ON t.TIMEBARID a.TIMEBARID WHERE t.OPTTIME last AND t.OPTTIME now ORDER BY t.OPTTIME ASC;;last取上次断点now取本次拉取开始时间执行完把最大时间回写断点。断点存哪里简单场景存配置文件频繁读写用 SQLite 或一张业务表都行。关键在于回写断点一定要在数据成功处理完之后不能先更新断点再处理数据否则中途崩了断点已经跳过去数据就漏了。正确性自查是很多人忽略的一步增量拉完后统计本次行数和时间范围和 WINCC 画面里同一时段的数据密度对比。行数能对上说明断点、分段和类型转换都没问题对不上先看是归档本身有缺口还是查询逻辑漏了条件。我自己的习惯是每次拉完在导出 CSV 首尾各留一条时间戳并在日志里记录最小和最大时间回到工位一眼就能判断这次任务是否正常结束。最常犯的错是依赖“加了 WHERE 就一定对”。我写时间窗查询时会先在冒烟模式里跑一小段把结果和 WINCC 趋势截图对比确认列名和值类型没搞错再放全量任务。希望这组方法能帮你少踩几个坑。本文还有配套的精品资源点击获取