3、数据存储进阶:SQLite数据库入门、表结构设计、CRUD操作
各位工程师,咱们继续往下聊。
上一章我们讲了怎么把数据存到CSV文件里。说实话,那玩意儿临时用用还行。真到了工业现场,数据量一大,查询一复杂,CSV就有点力不从心了。你想想看,一个班次下来几千条记录,你要按时间查、按设备查、按报警类型查……用CSV得把整个文件读进来,再一行一行遍历,效率太低了。
所以,这一章我们来点硬核的——SQLite数据库。
3.1 为什么是SQLite?
我在HMI项目里选数据库,首选就是SQLite。原因很简单:
- 零配置:不需要安装数据库服务,一个DLL文件搞定。HMI上位机软件里直接引用就行。
- 单文件存储:整个数据库就是一个.db文件,拷贝、备份、迁移都方便。我记得有一次现场需要把历史数据拷回来分析,直接U盘一插,把.db文件拖走完事。
- 轻量级:占用内存极小,适合嵌入式HMI和工控机。
- 标准SQL支持:你会的SQL语句,90%以上都能用。
3.2 表结构设计——别急着写代码
嗯,这里我要多说两句。很多新手上来就写CREATE TABLE,结果用着用着发现字段不够、类型不对、查询巨慢。我建议你先画个表结构草图。
以我们HMI最常用的历史数据记录表为例,我一般这样设计:
| 字段名 | 数据类型 | 说明 |
|---|---|---|
| ID | INTEGER PRIMARY KEY AUTOINCREMENT | 自增主键,每条记录唯一标识 |
| Timestamp | TEXT (ISO8601格式) | 记录时间,如 "2025-03-21 14:30:00" |
| TagName | TEXT | 变量名称,如 "温度_1号炉" |
| Value | REAL | 数值,保留两位小数 |
| Quality | INTEGER | 质量戳,0=正常,1=无效,2=手动输入 |
| AlarmFlag | INTEGER | 报警标志,0=无报警,1=报警 |
为什么时间用TEXT而不是DATETIME?
说白了,SQLite没有真正的日期时间类型。用TEXT存ISO8601格式,排序、比较都方便,而且可读性强。我在项目中吃过亏,一开始用INTEGER存时间戳,调试时看数据还得拿计算器换算,太痛苦了。
3.3 CRUD操作——实战代码
好,表结构定好了,咱们开始写代码。我用C#举例,因为工业HMI开发中C#用得最多。但原理是通用的,你用Python、Java也一样。
3.3.1 创建数据库和表
using System.Data.SQLite;
// 创建或打开数据库
string connStr = "Data Source=C:\\HMI_Data\\History.db;Version=3;";
using (SQLiteConnection conn = new SQLiteConnection(connStr))
{
conn.Open();
// 创建表
string sql = @"CREATE TABLE IF NOT EXISTS HistoryData (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
Timestamp TEXT NOT NULL,
TagName TEXT NOT NULL,
Value REAL,
Quality INTEGER DEFAULT 0,
AlarmFlag INTEGER DEFAULT 0
)";
SQLiteCommand cmd = new SQLiteCommand(sql, conn);
cmd.ExecuteNonQuery();
}
3.3.2 插入数据(Create)
插入数据时,我建议用参数化查询,别拼字符串。为什么?防止SQL注入是一方面,更重要的是——参数化查询效率更高,SQLite会缓存执行计划。
using (SQLiteConnection conn = new SQLiteConnection(connStr))
{
conn.Open();
string sql = @"INSERT INTO HistoryData
(Timestamp, TagName, Value, Quality, AlarmFlag)
VALUES (@ts, @tag, @val, @qual, @alarm)";
SQLiteCommand cmd = new SQLiteCommand(sql, conn);
cmd.Parameters.AddWithValue("@ts", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
cmd.Parameters.AddWithValue("@tag", "温度_1号炉");
cmd.Parameters.AddWithValue("@val", 125.6);
cmd.Parameters.AddWithValue("@qual", 0);
cmd.Parameters.AddWithValue("@alarm", 0);
cmd.ExecuteNonQuery();
}
3.3.3 查询数据(Read)
查询是CRUD里最常用的操作。我一般会按时间范围查,或者按变量名查。
// 查询最近1小时的数据
string sql = @"SELECT * FROM HistoryData
WHERE Timestamp > @startTime
ORDER BY Timestamp DESC";
using (SQLiteCommand cmd = new SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@startTime",
DateTime.Now.AddHours(-1).ToString("yyyy-MM-dd HH:mm:ss"));
using (SQLiteDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
int id = reader.GetInt32(0);
string ts = reader.GetString(1);
string tag = reader.GetString(2);
double val = reader.GetDouble(3);
// 处理每一行数据
Console.WriteLine($"{ts} - {tag}: {val}");
}
}
}
3.3.4 更新数据(Update)
更新操作在HMI里用得不多,但偶尔会用到。比如修正某个错误记录的质量戳。
string sql = "UPDATE HistoryData SET Quality = 1 WHERE ID = @id";
SQLiteCommand cmd = new SQLiteCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", 1024);
cmd.ExecuteNonQuery();
3.3.5 删除数据(Delete)
工业现场的数据,一般不建议物理删除。真要删,通常是清理旧数据。
// 删除30天前的数据
string sql = "DELETE FROM HistoryData WHERE Timestamp < @cutoff";
SQLiteCommand cmd = new SQLiteCommand(sql, conn);
cmd.Parameters.AddWithValue("@cutoff",
DateTime.Now.AddDays(-30).ToString("yyyy-MM-dd HH:mm:ss"));
int deletedRows = cmd.ExecuteNonQuery();
Console.WriteLine($"清理了 {deletedRows} 条旧数据");
3.4 实战中的避坑指南
做HMI数据存储这几年,我总结了几条经验,分享给你:
- 事务批量插入:如果一秒要存几十条数据,别一条一条Insert。用事务包起来,速度能快10倍以上。
- 定期VACUUM:SQLite删除数据后不会立即释放磁盘空间。我习惯每周执行一次VACUUM命令,压缩数据库文件。
- 文件路径别写死:用相对路径或者配置文件。我见过有人把路径写死在代码里,换台电脑就找不到数据库了。
- 并发写入要加锁:SQLite支持多线程读,但写操作是串行的。如果多个HMI页面同时写库,记得用锁机制。
好了,这一章的内容就到这里。SQLite入门其实不难,关键是把表结构设计好,CRUD操作写规范。下一章我们聊聊怎么把这些数据做成报表,生成PDF或者Excel,让老板和客户看得明明白白。
记住:数据存进去只是第一步,能查出来、能用起来,才是真本事。