3、数据存储进阶:SQLite数据库入门、表结构设计、CRUD操作

各位工程师,咱们继续往下聊。

上一章我们讲了怎么把数据存到CSV文件里。说实话,那玩意儿临时用用还行。真到了工业现场,数据量一大,查询一复杂,CSV就有点力不从心了。你想想看,一个班次下来几千条记录,你要按时间查、按设备查、按报警类型查……用CSV得把整个文件读进来,再一行一行遍历,效率太低了。

所以,这一章我们来点硬核的——SQLite数据库

3.1 为什么是SQLite?

我在HMI项目里选数据库,首选就是SQLite。原因很简单:

  • 零配置:不需要安装数据库服务,一个DLL文件搞定。HMI上位机软件里直接引用就行。
  • 单文件存储:整个数据库就是一个.db文件,拷贝、备份、迁移都方便。我记得有一次现场需要把历史数据拷回来分析,直接U盘一插,把.db文件拖走完事。
  • 轻量级:占用内存极小,适合嵌入式HMI和工控机。
  • 标准SQL支持:你会的SQL语句,90%以上都能用。
我的个人习惯:只要HMI项目需要存储超过1000条历史记录,我就直接用SQLite。CSV只用来做临时导出或者调试用。

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();
}
我曾经踩过的坑:一开始没加"IF NOT EXISTS",每次程序启动都执行CREATE TABLE,结果第二次启动就报错说表已存在。后来养成了习惯,所有建表语句都带上这个子句。

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}");
        }
    }
}
性能小技巧:如果数据量超过10万条,记得给Timestamp字段加索引。CREATE INDEX idx_time ON HistoryData(Timestamp); 加了索引后,查询速度能提升几十倍。

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,让老板和客户看得明明白白。

记住:数据存进去只是第一步,能查出来、能用起来,才是真本事。