简介:这是一份关于CITECT内嵌历史数据库的专业技术说明文档,专为工业自动化工程师、SCADA系统维护人员以及工厂数据集成开发人员编写。整个压缩包仅包含一个文档文件,体积为一点四二兆字节,便于离线查阅。目前已有三百一十一人学习使用。文档从数据管理需求切入,详尽介绍了历史数据库软件平台的架构与运作机制,包括冗余连接器的数据回补策略、基于微软数据库服务的内嵌存储方案,以及逢变则存和死区设置等高效率的数据压缩优化技术。同时,细致说明了数据记录的时间标记与质量标识、主动数据交换功能、磁盘空间计算和性能计数器,并阐述了数据库安全机制、用户权限管理以及多种外部访问接口,适合想要系统掌握工业历史数据库存储原理、性能调优方法或数据接口设计思路的读者。
1. CITECT里的“数据库”到底指什么:先把三类存储分清楚
CITECT是一套SCADA组态软件,虽然叫“数据库说明”,但实际生产里被叫做“CITECT数据库”的东西至少有三类:第一类是实时数据库,CITECT进程在内存里维护的变量快照和短时趋势;第二类是历史数据库,CITECT自带的归档文件,按标签(Tag)周期性落盘,默认是私有格式,不是关系型表;第三类是外部数据库,比如SQL Server、MySQL、达梦、Oracle,需要通过ODBC或SQL函数桥接。很多人以为CITECT自带了一个“关系型数据库管理界面”,其实没有。真正长期存生产记录、做报表和二次分析的,几乎都是第三方数据库。这篇博文把这三类边界讲清楚,然后给出从ODBC配置、数据写入、查询到防死锁防连接池耗尽和验收的完整闭环,适合做过DCS/SCADA集成、或者被CITECT数据上传任务缠住的工程师。
2. 用ODBC打通CITECT与外部数据库:从驱动选型到最小闭环
2.1 为什么CITECT必须走ODBC而不是直连数据库
CITECT对关系型数据库的访问,不依赖某一种数据库的私有客户端,而是走ODBC标准接口。CITECT自身维护一个ODBC连接池,通过SQLConnect、SQLExec等函数完成查询和写入。这带来的直接好处是:更换数据库品牌时,CITECT侧的程序代码几乎不用动,只要换ODBC驱动和连接串;坏处是,ODBC驱动的位数、版本、默认参数会变成整套链路里最不稳定的环节。
常见做法是,目标数据库厂商会提供对应操作系统的ODBC驱动。以Windows工控机为例,32位CITECT对应32位ODBC驱动,64位CITECT对应64位ODBC驱动。很多人把64位驱动装好,CITECT却报“找不到数据源”,大概率就是位数不匹配。混用32位与64位驱动,是这类集成项目里出现频率最高的翻车点。
2.2 创建系统DSN:不要让CITECT读到用户DSN
ODBC数据源分用户DSN和系统DSN。CITECT的IOServer或后台任务在Windows服务环境下运行时,用户上下文经常不是当前登录账户,用户DSN会读不到。所以统一用系统DSN。
具体操作路径是:控制面板→管理工具→ODBC数据源(64位或32位按CITECT位数选)→系统DSN→添加。选择对应驱动后,填入服务器地址、数据库名、账号密码。这里有一个容易被忽略的选项:连接超时和查询超时,建议分别设为5秒和30秒,不要默认的无限值,否则数据库宕机时CITECT的SQL操作会长时间挂起,直接拖住扫描周期。
2.3 用odbc.ini文件核对数据源名称
DSN配置完成后,可到注册表或odbc.ini确认名称拼写,因为CITECT项目里的连接串通常是字符串拼接,名称错一个字母,运行时才报错:
cat /etc/odbc.ini 2>/dev/null || true以Linux网关跑CITECT IO采集、用unixODBC的场景,odbc.ini里通常写成:
[CITECT_Historian] Driver=MySQL ODBC 8.0 Unicode Driver Server=192.168.10.20 Database=historian Port=3306 User=citect_user Password=****** Option=3提示:CITECT侧项目里填的DSN名称必须与odbc.ini中的节名完全一致,不能带路径。
2.4 在CITECT里用SQL函数建立最小连接
CITECT Cicode里访问数据库的标准套路是“连接→执行→取结果→关闭”。下面这段代码可以放在画面按钮或启动任务里:
-- 这段是CITECT Cicode,不是纯SQL STRING sConn = "DSN=CITECT_Historian;UID=citect_user;PWD=******"; INT hDBC; INT nResult; hDBC = SQLConnect(sConn); IF hDBC = 0 THEN Message("数据库连接失败", "请检查ODBC DSN或网络", 1); ELSE nResult = SQLExec(hDBC, "SELECT COUNT(*) FROM tag_log WHERE day = '2025-06-01'"); IF nResult = 0 THEN Message("查询失败", SQLGetError(hDBC), 1); END END SQLDisconnect(hDBC);这段代码的逻辑是:先拼一个连接串,调SQLConnect建立连接,连接句柄为0说明失败,非0则执行一条SQL语句,最后用SQLDisconnect释放。参数上要注意,SQLConnect的DSN里不要写账号密码时,就需要在UID/PWD里显式给出;如果数据库启用了Windows身份验证,则不能填UID/PWD,否则连接串会覆盖本机账户。CITECT的SQLExec执行完成后会自动释放结果集,不要在循环里反复SQLConnect又SQLDisconnect,连接池的复用功能会被冲掉。
3. 把生产数据写进历史库:归档配置、查询语句与报表取数的落地写法
3.1 CITECT的历史归档:私有文件与关系库是两套并行体系
CITECT的标签归档默认写入私有历史文件,外部报表无法直接读取。所以工程上常见做法是“双通道”:CITECT自己保留历史文件供画面趋势回放,同时通过计划任务把变化数据插入外部数据库,供报表系统查询。
在CITECT项目管理器的“标签”里,可以设置Tag的趋势使能(History Trending)和采集周期。周期越短,单日数据量越大。现场常用1秒或5秒,但1秒归档在1000个点位下,每天会产生约8000万条记录,这对数据库的写入压力和存储空间都是直接考验。所以归档周期并不是越短越好,而要根据工艺的波动时限来定。
3.2 周期性批量插入:避免逐条INSERT拖垮ODBC
数据写入外部库最忌讳的做法是:每条数据变化都执行一次INSERT。SCADA的点位波动频率高,逐条插入会把ODBC连接池占满,还会造成日志表碎片膨胀。
我一般会写一段Cicode,放在后台循环任务里,每隔30秒把缓冲区的变化数据拼接成一条SQL,一次性插入:
INSERT INTO tag_log (tag_name, tag_value, ts, quality) VALUES ('TEMP_101', 23.5, '2025-06-01 00:00:00', 0), ('PRESS_205', 1.02, '2025-06-01 00:00:00', 0), ('FLOW_301', 88.3, '2025-06-01 00:00:00', 0);批量插入比单条插入要快一到两个数量级。但要注意SQL语句长度限制,不同数据库对单包大小有上限,MySQL默认max_allowed_packet通常是64MB,但ODBC驱动和网络中间层的限制往往更早到来。稳妥值是每次不超过500行,分割成多批执行。如果数据量特别大,还可以改用SQL参数化数组,Citect 7.20以上版本的SQLExec支持绑定参数数组,这比拼字符串更安全,也免去转义单引号和特殊字符的麻烦。
3.3 报表查询:按时间段聚合并处理缺失时间戳
现场报表最常见的要求是查询某条产线在某个班次的平均值、最大值和最小值。这里有一点要注意:CITECT写入外部库的时间戳是采集时间,不是数据库时间。所以查询条件里必须用ts字段,而不是用数据库服务器的当前时间:
SELECT tag_name, AVG(tag_value) AS avg_val, MAX(tag_value) AS max_val, MIN(tag_value) AS min_val FROM tag_log WHERE ts BETWEEN '2025-06-01 08:00:00' AND '2025-06-01 20:00:00' GROUP BY tag_name ORDER BY tag_name;SELECT ts, tag_value FROM tag_log WHERE tag_name IN ('TEMP_101', 'PRESS_205') AND ts >= NOW() - INTERVAL 1 HOUR;3.4 用视图把原始表包装成“无空洞序列”
原始日志表里,停机或通讯中断的时间段是没有数据的。报表系统直接查原始表,画出来的曲线会缺块。我常用的办法是在数据库里建一个视图,把缺失时间段用前值填充,或标成NULL,看报表口径:
CREATE VIEW v_tag_filled AS SELECT t1.ts, COALESCE(t1.tag_value, (SELECT tb.tag_value FROM tag_log tb WHERE tb.tag_name = t1.tag_name AND tb.ts < t1.ts ORDER BY tb.ts DESC LIMIT 1)) AS tag_value FROM tag_log t1;| 场景 | 查询写法 | 注意事项 |
|---|---|---|
| 按班次取均值 | GROUP BY tag_name + AVG | 过滤掉quality非0的记录 |
| 取某点启动时刻 | WHERE ts <= 启动时间 ORDER BY ts DESC LIMIT 1 | 需要联合索引(tag_name, ts) |
| 多变量同时刻对齐 | 用等值连接+前后时间窗 | 避免笛卡尔积,时间窗取1秒 |
CITECT侧做报表,不建议直接在画面上拉大表。正确做法是让CITECT把查询结果写到临时表或导出为CSV,再由报表工具去读。这样CITECT的画面任务不会被大结果集阻塞。
4. 数据库死锁、连接池耗尽与数据落地偏差:四个真正的坑
4.1 连接池参数:为什么CITECT连接数要收敛
CITECT的SQL函数不是长连接。每一次SQLConnect都走一次ODBC分配,用完SQLDisconnect归还。在点位多、周期短的画面里,如果每条记录都现开现关,连接建立的开销会直接压垮工控机。CITECT的数据库连接池受“最大连接数”参数限制,这个参数在Citect INI文件的[ODBC]段中,默认值和现场实际能承受的并发量并不恒定。调大连接池不一定能提升写入性能,反而可能打满数据库端的最大连接许可数。
经验值:操作员画面用只读查询,控制在3到5个连接;后台归档写入,控制在2到3个连接。总数不要超过数据库端max_connections的十分之一。如果打开CITECT的项目诊断工具,看到大量“SQLConnect Timeout”或“Connection not available”,优先检查的便是连接池是否被慢查询占满。
4.2 数据库死锁:SCADA批量写入与报表长事务的对冲
CITECT侧批量INSERT和报表侧的长时间SELECT之间容易产生锁冲突。MySQL的InnoDB默认行锁,但如果SCADA写入时条件没带索引,比如只按tag_name查,而联合索引是(tag_name, ts),优化器选错索引就会升级为表锁。这也是数据库死锁报告里最常见的直接原因。
常规解法是规范索引:tag_log表上必须创建联合索引(tag_name, ts),查询里条件必须带tag_name且用不上cast函数。写操作则尽量把批量INSERT的事务控制在1秒内。报表查询如果必须长时间运行,把它放到只读副本或备库里,不要和CITECT写库抢同一实例。跨天归档切换表时,建议用定时任务在凌晨低峰期做,而不是在采集高峰期DROP旧表。
4.3 科学计数法与字符串字段:CITECT数值写入外部库的类型映射
数据库里身份证号、设备编号这类长数字文本,在CITECT里如果被定义成REAL类型,导出或写入数据库数值字段后,再被Excel读取就会变成科学计数法。本质上是因为CITECT的REAL精度不够,并不是数据库的问题。CITECT的REAL实际是浮点数,精度约7位有效数字,超过15位的编号必须当作STRING类型处理。
在表结构里,这些字段要用VARCHAR(32)而不是BIGINT,否则从CITECT取数时数值仍然会失真。CITECT组态里标签的数据类型要和数据库字段类型保持对应,实际排查时先确认是CITECT标签把编号截断了,还是Excel显示问题,方法是用Notepad打开导出的CSV,看原始文本是否完整,文本正常则只改Excel列格式。
4.4 归档文件打不开与历史数据迁移:用转发工具落库
CITECT自带的Historian文件经常因为非法关机和磁盘已满出现索引损坏。此时不要试图用数据库工具直接打开归档文件,CITECT安装目录下有归档修复和转储工具,转储的目标格式可以选择CSV或ODBC目标库。常见做法是把损坏归档转储为CSV中间文件,再由数据库同步工具把CSV灌入历史表,这样既保留原始数据,又完成了一次格式迁移。数据跨度大的迁移任务,要用断点续传的同步策略,并在目标库建好唯一索引防重。
5. 用一个状态脚本验证明CITECT数据库链路是否健康
5.1 用标签状态表确认链路是否可用
项目验收或者日常巡检时,不要只看CITECT的画面有没有显示数据,因为画面读的是实时库。我一般会在外部数据库里建一张“链路心跳表”,让CITECT每隔1分钟更新一次,并盖上时间戳,从而确认识别链路是完好可用的,而不是仅靠画面数据判断。
mysql -h 192.168.10.20 -u citect_user -p****** historian \ -e "INSERT INTO link_heartbeat(client_name, beat_time) VALUES ('SCADA_01', NOW()) ON DUPLICATE KEY UPDATE beat_time=NOW();"5.2 用SQL跟踪器验证写入是否落库
启动CITECT的SQL跟踪日志,日志里会记录每条SQLConnect、SQLExec的开始时间、结束状态以及失败的错误码。筛选其中返回“SQL_ERROR”的行,就能定位是连接失败、语法错误还是超时问题。另一个验证手段是直接在数据库端执行查询,查看近期是否有数据进来:
SELECT COUNT(*) AS cnt, MAX(ts) AS latest_ts FROM tag_log WHERE ts > NOW() - INTERVAL 5 MINUTE;如果latest_ts没有更新,而链路心跳表正常,说明问题出在CITECT侧的归档任务或SQL插入任务上;如果心跳表也没更新,则问题出在ODBC连接层。
5.3 验收清单
| 检查对象 | 通过标准 |
|---|---|
| ODBC连接测试 | 系统DSN测试无报错,位数与CITECT一致 |
| 写入时延 | 从CITECT发起INSERT到DB落库,平均小于2秒 |
| 数据连续性 | 历史表按1分钟粒度抽查,连续无空洞 |
| 死锁频率 | 数据库错误日志中每周死锁次数为0至1次 |
| 连接池状态 | 峰值连接数不超过上限,无SQLConnect超时 |
| 长数字字段 | CSV导出文本与源值一致,无科学计数法现象 |
这一套清单适合在每次版本变更后快速跑一遍,也可以做成一个批处理脚本,按小时检查结果,让连接池死锁和链路中断等故障在业务受影响之前暴露出来。
本文还有配套的精品资源,点击获取