单机版工业上位机,数据存哪里?不用上MySQL,SQLite一个文件就够——免安装、免服务、拷贝即走。但"够用"不等于"随便用":连接模型错了报database is locked,一条条insert能把采集线程拖死。这篇把Qt+SQLite本地存储的几个关键坑一次讲透。
单机版上位机,为什么选SQLite而不是MySQL?
很多人一听"数据库"就想到装服务、配账号。单机版场景完全没必要——一台工控机跑一个软件,数据量也就每天几万到几十万条,SQLite嵌入式引擎随程序走,db文件放本地磁盘,双击即用,客户现场部署零依赖。MySQL那种客户端/服务架构在单机场景纯属给自己找事:服务没起来软件就罢工,备份还得单独配。
当然也有边界:多台设备共享一份数据、客户端并发访问十几个,那确实该上网络数据库。单机闭环,SQLite是正解。
多线程访问,为什么总报 database is locked?
Qt里QSqlDatabase有个铁规矩:连接不能跨线程使用。一个QSqlDatabase连接在哪个线程addDatabase,就只能在哪个线程用。采集线程、写库线程、UI查询线程各开各的连接,用不同的连接名(addDatabase第二个参数)区分。图省事把主线程创建的连接传给子线程用,偶发崩溃都是轻的,锁库是常态。
第二件事是打开WAL模式,执行一句PRAGMA journal_mode=WAL就行。默认的DELETE日志模式下写操作直接锁全库,界面查个历史曲线都能卡住;WAL模式读写不互斥,采集写入和界面查询各走各的,这一条就能解决九成的锁库抱怨。顺手再把synchronous设为NORMAL,掉电安全性够用,写入速度还能再提一截。
一条条insert太慢,怎么把写入速度提上来?
这是最立竿见影的一条:批量写入必须包事务。SQLite每条独立insert都隐含一次事务,要刷一次磁盘。采集线程攒一批数据(比如500条或1秒一批),begin transaction—批量insert—commit,一次提交。实测同样的表结构,单条插入每秒几百条,包事务后轻松上万条,差距百倍。
还有个容易忽略的点:QSqlQuery对象别复用过头,也别每条记录都new一个。prepare一次,bindValue循环用,finish后及时清理。写库线程用队列接收采集线程的数据,和UI彻底解耦——界面卡不卡,跟数据库写得快慢就没关系了。
历史数据越攒越多,查询怎么保证不拖垮界面?
两张牌:分表和索引。按月分表,data_202609、data_202610这样建,查历史曲线先定位月份表,单表数据量锁死在百万级以内,查一年前的数据也不用全表扫。查询永远带时间范围条件,时间戳字段建索引。别在UI线程直接exec查询,Qt里moveToThread或者用QtConcurrent跑,结果通过信号回主线程刷新。
数据生命周期也提前想好:保留12个月或24个月,超期的月份表直接drop,比delete快得多,db文件体积也不会无限膨胀。定期VACUUM或者用VACUUM INTO做归档备份——备份其实更简单,软件退出时把db文件拷一份到备份目录,SQLite单文件的优势就在这。
版本升级要加字段,老客户的库怎么办?
这个坑我见得太多:新版本表结构加了列,软件一打开老库直接报no such column。注意SQLite的ALTER TABLE不支持ADD COLUMN IF NOT EXISTS这种写法,别想当然。稳的做法是启动时先PRAGMA table_info读出现有列清单,缺哪个列再执行ALTER TABLE ADD COLUMN,一步步迁移;改动大的版本就走"建新表—拷数据—删旧表—改名"的路子。迁移逻辑包在事务里,失败回滚,别把客户的历史数据改半截。
我之前做过一个环境试验箱的单机上位机,客户现场db文件跑了三年多,升级新功能时就是靠这套迁移脚本平滑过来的,一条历史数据没丢。说实话,单机软件卖出去之后能不能省心,一半功夫在这种看不见的地方。
第一步怎么动手?
行动建议:新项目第一天就把这四件事配好——每个线程独立命名连接、开启WAL、写入走批量事务、历史表按月分。四行配置级别的工作量,能省掉后期无数个加班的夜晚。等数据攒到几百万条再想分表,那才是真麻烦。