简介:面向LabVIEW开发者的一套SQLite数据库操作工具包,适合在测控软件、数据采集与工控项目中实现本地化数据存储的初、中级用户。与常见数据库方案相比,它支持自动生成数据库文件、以表格形式批量插入记录,并能清理数据文件缓存,避免长期运行后文件体积持续膨胀,同时保持了较低的编程门槛。整套工程共148个文件,约2.35MB,以117个vi承担读写、查询与错误处理等基础功能,配合ctl控件、mnu菜单及lvlib、lvproj工程文件组织调用,另附32/64位动态库、sql语句示例与pdf说明文档,解压后即可搭建测试环境。目前已有3160人学习下载。读者可从中获得可直接复用的子vi模块、表结构定义与触发器等控件的封装思路,以及跨平台部署所需的库文件与配置参照,便于快速集成到自有项目中并减少调试成本。
1. LabVIEW 落盘 SQLite:先别急着写 SQL,先把数据流和文件边界画清楚
LabVIEW 采集程序跑起来以后,数据往哪里落,往往比前面板好不好看更影响交付。SQLite 数据库在 LabVIEW 里不是原生控件,但它是单文件、零服务、可嵌入的,特别适合单机采集、离线设备和测试台架。这份资源围绕 LabVIEW 使用 SQLite 数据库的完整链路展开:准备动态库、封装基础函数、建表、参数化写入、读取和排错。适合已经会用 LabVIEW 做采集、但不想为了几千条记录上重型数据库的工程师,也适合接过别人程序、发现数据全塞在 TDMS 或 CSV 里、查询和追溯都费劲的人。先想清楚数据量、写入频率和数据库文件存放位置,再决定表结构和提交策略,比直接抄一段 SQL 更省返工。遇到中文路径、32/64 位 DLL、事务没提交这些坑,后面按现场排查顺序拆开说。
2. 选型与最小闭环:LabVIEW 调 SQLite 的三条路和一张能跑通的表
2.1 三条调用路径:DLL、.NET、中间层,各自适合什么现场
LabVIEW 调 SQLite 常见就三条路,选型错了后面会一直还债。第一条是 Call Library Function Node 直接调 sqlite3.dll 的 C API,依赖最少、部署最轻,目标机只要能加载对应位数的 DLL 就能跑,缺点是句柄、指针、字符串生命周期都要自己管,封装不到位就漏内存。第二条是用 .NET 节点接 System.Data.SQLite,开发速度快,SQL 接口完整,但目标机要带程序集,LabVIEW 位数、.NET 运行时版本、程序集版本三者对不上时,错误信息往往很难看。第三条是中间层,比如用命令行工具或独立进程做导入导出,适合离线归档、跨语言解耦,不适合毫秒级高频写入,因为进程间通信和启动开销会吃掉实时性。
我一般这样选:单机采集、连续运行、只落一个本地库,优先走 DLL 封装;桌面工具、需要快速做查询界面、团队里已有 .NET 资产,走 .NET 节点;数据要给别的系统消费、LabVIEW 只负责生产文件,才考虑中间层。不要因为 .NET 写起来快就把一个长期运行的采集程序压上去,部署时缺一个 DLL 就能让现场停半天。
| 路径 | 部署依赖 | 写入性能 | 调试难度 | 适合场景 |
|---|---|---|---|---|
| sqlite3.dll + CLFN | 一个对应位数的 DLL | 高 | 中高,需要管指针 | 单机采集、长期运行 |
| .NET 节点 | 程序集与运行时 | 中 | 中,异常栈较完整 | 快速原型、桌面查询 |
| 中间层/命令行 | 外部进程 | 低到中 | 低,但链路长 | 离线导入导出、解耦 |
2.2 准备 sqlite3.dll 与 LabVIEW 工程目录
第一步不是写代码,是把位数确认清楚。打开 LabVIEW 的帮助菜单看关于信息,确认当前是 32 位还是 64 位;然后准备同样位数的 sqlite3.dll。位数不匹配时,CLFN 可能直接报加载失败,也可能让 LabVIEW 在调用瞬间崩掉,这类问题不要靠猜。DLL 不要丢进系统目录,也不要依赖目标机某处恰好有一份,我一般把工程目录摆成下面这样,DLL 和数据库文件都跟着工程走,复制到别的机器时整包带走。
LabVIEW_SQLite_Demo/ ├─ bin/ │ └─ sqlite3.dll ├─ data/ │ └─ measure.db ├─ sql/ │ └─ schema.sql └─ vi/ └─ db_open.vi这个结构里,bin 放动态库,data 放数据库文件,sql 放建表和索引脚本,vi 放封装。实际路径用“当前 VI 路径”往上退一级再拼,不要在前面板上让操作员手填绝对路径。数据库文件不要放同步盘、网络盘或只读目录,SQLite 的锁机制在这些位置不可靠,轻则偶发 database is locked,重则文件被同步进程占用后打不开。路径里尽量避开中文和空格,不是 SQLite 不支持,而是中间经过命令行工具、日志、脚本时容易多一层编码问题。
2.3 用 Call Library Function Node 封装 open/exec/close
我一般先封装四个最小函数:open、exec、errmsg、close。open 用 sqlite3_open_v2 比 sqlite3_open 更可控,可以带只读、创建、全互斥锁这些标志。C 原型如下,CLFN 里按参数表配置,不要直接拿字符串常量去试。
int sqlite3_open_v2( const char *filename, sqlite3 **ppDb, int flags, const char *zVfs ); int sqlite3_exec( sqlite3 *db, const char *sql, int (*callback)(void*, int, char**, char**), void *arg, char **errmsg ); const char *sqlite3_errmsg(sqlite3 *db); int sqlite3_close_v2(sqlite3 *db); void sqlite3_free(void *p);| 参数 | C 类型 | LabVIEW 侧配置 | 说明 |
|---|---|---|---|
| filename | const char* | C 字符串指针 | 数据库文件路径,UTF-8 字节 |
| ppDb | sqlite3** | 指向 U64 的指针 | 输出数据库句柄 |
| flags | int | I32 | READWRITE 0x2、CREATE 0x4、FULLMUTEX 0x10000 |
| 返回值 | int | I32 | 0 为 SQLITE_OK |
句柄我一般用 U64 保存,open 的第二个参数按“指向 U64 的指针”配置,调用完从 U64 里读句柄;exec 和 close 的 db 参数按 U64 传入,封装层统一做类型转换。不同 LabVIEW 版本的 CLFN 配置项名称略有差异,以“参数类型/传递方式”为准,不要照抄别人截图里的勾选项。exec 的 errmsg 是 char**,出错后取到字符串,再调用 sqlite3_free 释放,否则每错一次漏一小块内存,跑几天就能看出来。这个细节是血泪经验,现场连续运行的程序尤其要盯住。
2.4 建表并验证写入的最小闭环
最小闭环不要一上来就写批量插入,先确认 open 成功、建表成功、插入一条、close 干净、第三方工具能读回。表结构按采集场景先定一张宽表,时间戳存整数,通道名存文本,值存 REAL,状态码存 INTEGER,后面再按查询条件加索引。
CREATE TABLE IF NOT EXISTS measure ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, channel TEXT NOT NULL, value REAL NOT NULL, status INTEGER DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_measure_ts ON measure(ts);建表后用 exec 插一条,再查最近五条。插入语句里先用固定值,不要接前面板输入,目的是把路径、权限、DLL 三个变量先摘出去。
INSERT INTO measure(ts, channel, value, status) VALUES (strftime('%s','now'), 'AI0', 12.34, 0); SELECT id, ts, channel, value, status FROM measure ORDER BY id DESC LIMIT 5;LabVIEW 侧每一步判断返回值:SQLITE_OK 是 0,SQLITE_ERROR 是 1,SQLITE_BUSY 是 5,SQLITE_LOCKED 是 6,SQLITE_READONLY 是 8,SQLITE_CANTOPEN 是 14。非 0 时把 sqlite3_errmsg 的字符串带到错误簇里,不要只弹一个“数据库打开失败”。如果 LabVIEW 侧读不到数据,先用独立 Python 连接读一次,确认数据库文件里到底有没有表和数据,这一步能把“LabVIEW 封装错”和“SQL 没执行”分开。
import sqlite3 from pathlib import Path db_path = Path("data/measure.db") conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute("SELECT name, sql FROM sqlite_master WHERE type='table' ORDER BY name") for name, sql in cur.fetchall(): print(name) print(sql) cur.execute("SELECT id, ts, channel, value, status FROM measure ORDER BY id DESC LIMIT 5") print(cur.fetchall()) conn.close()这个脚本只做验证,不参与正式采集。它相当于一个黑匣子:LabVIEW 说写成功了但脚本查不到,问题在事务或路径;脚本能查到而 LabVIEW 查不到,问题在读取封装或结果集处理。最小闭环跑通以后,再把固定值换成参数化写入,不要反过来做。
3. 把 SQL 写进 LabVIEW:参数化查询、事务与批量写入
3.1 从 sqlite3_exec 过渡到 prepare/step:为什么拼接字符串迟早翻车
exec 适合建表、改配置、执行 PRAGMA 这类一次性 SQL,不适合把采集值拼进字符串。通道名里带一个单引号、备注里带换行、浮点数用本地化逗号,都会让 SQL 语法直接翻车。下面这种写法在测试数据里没事,到现场就会出问题。
-- 反例:通道名里带单引号,SQL 直接被截断 INSERT INTO measure(ts, channel, value, status) VALUES (strftime('%s','now'), 'AI0'的备用通道', 1.0, 0);正确做法是 prepare + bind + step。SQL 模板里只留问号,值通过绑定接口传进去,SQLite 负责转义和类型处理。核心 C 原型如下,CLFN 封装时重点看 bind 的索引从 1 开始,不是 0。
int sqlite3_prepare_v2( sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail ); int sqlite3_bind_int64(sqlite3_stmt *stmt, int index, sqlite3_int64 value); int sqlite3_bind_double(sqlite3_stmt *stmt, int index, double value); int sqlite3_bind_text( sqlite3_stmt *stmt, int index, const char *value, int nByte, void (*destroy)(void*) ); int sqlite3_step(sqlite3_stmt *stmt); int sqlite3_reset(sqlite3_stmt *stmt); int sqlite3_finalize(sqlite3_stmt *stmt);nByte 传 -1 表示字符串以零结尾;bind_text 的最后一个参数我一般固定传 SQLITE_TRANSIENT,让 SQLite 自己拷贝一份字符串。不要传 SQLITE_STATIC 去省内存,除非你非常确定 LabVIEW 字符串在 step 之前不会被回收,否则就是随机乱码或崩溃。SQL 模板写成下面这样,时间戳、通道、值、状态各占一个问号,顺序不要和 bind 索引错位。
INSERT INTO measure(ts, channel, value, status) VALUES (?, ?, ?, ?);3.2 封装 prepare_bind_step 的 VI 结构
我一般把 prepare、bind、step、finalize 放在同一个 VI 里,输入是数据库句柄、SQL 模板和变体参数数组,输出是错误码、错误信息和 last_insert_rowid。不要把 stmt 句柄跨多层 VI 传出去,除非你有明确的缓存和释放策略;跨 VI 传 stmt 最容易出现“前面板关了,语句还挂着”,最后 close 返回 SQLITE_BUSY。参数数组里每个元素带一个类型标记,I64 走 bind_int64,DBL 走 bind_double,字符串走 bind_text,NULL 走 bind_null。
import sqlite3 conn = sqlite3.connect("data/measure.db") sql = "INSERT INTO measure(ts, channel, value, status) VALUES (?, ?, ?, ?)" params = (1710000000, "AI0", 12.34, 0) conn.execute(sql, params) conn.commit() conn.close()这段 Python 不是让你在 LabVIEW 里调 Python,而是用来验证 SQL 模板和参数顺序。LabVIEW 封装层报错时,先把同样的 SQL 和参数丢给这个脚本跑一遍;脚本能过,问题在 bind 或类型转换;脚本也过不了,问题在表结构或约束。封装 VI 里每次 prepare 成功后,不管 step 返回什么,都要走 finalize,错误分支也不例外。finalize 返回非 0 时不要覆盖原始错误,单独记下来,否则现场只看到“finalize 失败”,前面真正的错因被吞掉。
3.3 事务与批量提交:1000 条记录怎么不把盘写爆
SQLite 默认每条 INSERT 自动提交,采集循环里一条一条写,磁盘同步次数会非常高,表现为程序不报错但写入速度上不去,CPU 占用也不高。批量写入用显式事务,把一批 INSERT 包在 BEGIN 和 COMMIT 之间,提交频率按数据重要性和掉电风险定。我一般 500 到 2000 条提交一次,或者按 1 秒周期提交,不要把一个采集会话全包在一个事务里,否则中途异常会回滚一大段。
BEGIN IMMEDIATE; INSERT INTO measure(ts, channel, value, status) VALUES (?, ?, ?, ?); INSERT INTO measure(ts, channel, value, status) VALUES (?, ?, ?, ?); COMMIT;BEGIN IMMEDIATE 比 BEGIN 更早拿写锁,能减少“读事务升级为写事务”时的 BUSY。错误分支要显式 ROLLBACK,不要只关连接;关连接虽然会回滚未提交事务,但现场日志里看不出到底回滚了哪些批次。批量大小不是越大越好,事务太大时 WAL 文件和内存都会涨,异常恢复时间也变长。关键数据可以把 synchronous 设成 FULL,普通采集设 NORMAL,这个取舍在最后一章展开。
3.4 读取:column 类型与 LabVIEW 变体转换
读取比写入更容易被低估。SQLite 是动态类型,同一列里理论上可以存 INTEGER、REAL、TEXT、BLOB、NULL,LabVIEW 侧如果直接按 DBL 取,遇到 NULL 或文本就会得到奇怪的值。稳妥做法是先取 column_count,再逐列取 column_type,按类型调 column_int64、column_double、column_text,最后组装成变体数组或集群数组。C 原型如下。
int sqlite3_column_count(sqlite3_stmt *stmt); int sqlite3_column_type(sqlite3_stmt *stmt, int col); sqlite3_int64 sqlite3_column_int64(sqlite3_stmt *stmt, int col); double sqlite3_column_double(sqlite3_stmt *stmt, int col); const unsigned char *sqlite3_column_text(sqlite3_stmt *stmt, int col); int sqlite3_column_bytes(sqlite3_stmt *stmt, int col);查询按时间范围走索引,不要把整张表拉到 LabVIEW 里再过滤。下面这条 SQL 的 ts 和 channel 条件要和索引设计对上,否则数据量上来后前面板会卡。
SELECT id, ts, channel, value, status FROM measure WHERE ts BETWEEN ? AND ? ORDER BY ts;column_text 返回的是 SQLite 内部缓冲区,step 下一次或 finalize 后就可能失效,LabVIEW 侧要立刻转成字符串副本,不要只存指针。BLOB 字段同理,先取 column_bytes 再拷贝。读取循环里每一步判断 step 返回值:SQLITE_ROW 表示有一行,SQLITE_DONE 表示结束,其他值走错误处理。不要把 SQLITE_DONE 当错误弹窗,也不要在 SQLITE_ROW 分支里忘记继续 step。
4. 数据读写之外:时间戳、浮点、中文与文件路径的坑
4.1 时间戳统一存整数:LabVIEW 时间转 Unix 秒
LabVIEW 时间戳的纪元是 1904 年,SQLite 的 unixepoch 是 1970 年,两者差 2082844800 秒。直接把 LabVIEW 时间戳写进数据库,查询时用 datetime 函数会得到离谱的年份。我一般统一存 Unix 秒,INTEGER 字段,显示层再转本地时间。转换公式如下,先减纪元差,再取整或保留毫秒。
from datetime import datetime, timezone labview_seconds = 1710000000.0 unix_seconds = labview_seconds - 2082844800 print(int(unix_seconds)) print(datetime.fromtimestamp(unix_seconds, tz=timezone.utc).isoformat())存进去以后,SQL 查询用 datetime(unix_ts, 'unixepoch', 'localtime') 转成本地时间字符串,但不要在 WHERE 条件里对 ts 套函数,否则索引失效。要按天查询,先在外层算好当天的起止 Unix 秒,再用 BETWEEN。毫秒场景就存毫秒整数,字段名写清楚 ts_ms,别让后面接手的人猜。
4.2 浮点精度与 REAL 字段的显示误差
SQLite 的 REAL 是 8 字节 IEEE754,LabVIEW 的 DBL 也是 8 字节,存储本身不会额外丢精度。现场看到的“12.34 变成 12.339999”多半是显示格式或格式化字符串造成的,不是数据库把值改了。判断相等不要用 value = 12.34,用范围查询;如果要做校准,原始码和工程值分两列存,不要在一个 REAL 列里反复换算。
SELECT id, ts, channel, value FROM measure WHERE value BETWEEN 12.3399 AND 12.3401 ORDER BY ts DESC;还有一种情况是 bind 类型用错:把字符串 bind 到 REAL 列,SQLite 会尝试转换,转不了就存成 TEXT,后续按数值排序会乱。封装层按参数类型标记走 bind_double,不要图省事全部 bind_text。表字段加 NOT NULL 和默认值只能挡一部分,类型纪律还是要靠封装层。
4.3 中文与 UTF-8:LabVIEW 字符串编码转换
LabVIEW 字符串本质是字节数组,默认多字节编码跟系统代码页有关;SQLite 默认按 UTF-8 处理文本。中文通道名、工位名、备注直接 bind 进去,可能在数据库里显示为乱码,或者查询条件匹配不上。稳妥做法是写入前把 LabVIEW 字符串转成 UTF-8 字节数组,读取后把 UTF-8 字节转回本地编码;如果团队能接受,通道名统一用英文加编号,显示层再做映射,能省掉大量编码排查。
import sqlite3 conn = sqlite3.connect("data/measure.db") conn.execute("PRAGMA encoding='UTF-8';") conn.execute( "INSERT INTO measure(ts, channel, value, status) VALUES (?, ?, ?, ?)", (1710000000, "工位A-电压", 12.34, 0), ) conn.commit() conn.close()PRAGMA encoding 只在空库第一次写入前生效,已经有表的库再设不会改编码。怀疑乱码时,用 hex(channel) 看实际字节,UTF-8 的中文通常是三个字节一组,GBK 是两个字节一组,对比一下就能定位。不要用前面板显示正常来判断数据库里正常,显示层可能又转了一次。
4.4 数据库文件路径与相对路径
路径问题在实验室里很少暴露,到现场一装就出。数据库文件放在 Program Files 下,普通用户没有写权限,open 返回 CANTOPEN;放在同步盘里,同步进程会锁文件;路径超过系统限制,文件明明存在也打不开。我一般把 data 目录放在工程同级或用户可写目录,启动时检查目录是否存在、是否可写,再拼数据库路径。
mkdir -p data test -w data && echo "data writable" || echo "data not writable"LabVIEW 里用“当前 VI 路径”取到 vi 目录,拆分路径后向上退,再拼 data 和数据库文件名。不要在前面板暴露绝对路径输入框给操作员,误改一次后面所有写入都失败。路径字符串转 C 字符串时确认编码,中文路径尽量避开;如果必须用,先把路径转 UTF-8 再传给 open。数据库文件和 WAL、SHM 文件要在同一个目录,不要只拷贝 .db 文件而漏掉 -wal 文件,否则最近提交的数据可能读不到。
5. 避坑与排查:五个把现场拖到下班的 SQLite 问题
5.1 数据库被锁:database is locked 的排查顺序
现象是采集程序偶发写入失败,错误码 5,重启后又正常。原因通常不是 SQLite 坏了,而是另一个连接持有写事务,或者某个 prepare 过的语句没有 finalize,导致连接一直占着锁。排查时先看有没有第二个 LabVIEW VI、Python 脚本、数据库浏览器同时开着;再看自己的写事务是不是包了太久,前面板循环里千万不要在事件结构里 BEGIN 后等用户操作再 COMMIT。
PRAGMA busy_timeout = 5000;每个连接打开后都设 busy_timeout,让 SQLite 在遇到锁时等一会儿再返回,而不是立刻失败。等待时间按写入周期定,5000 毫秒对大多数采集够用,但不要用无限等待掩盖长事务。解决方向是缩短事务、减少并发写连接、确保语句 finalize;重试只能兜底,不能当主策略。
5.2 内存泄漏:finalize 与 close 的配对
现象是程序连续跑几天内存缓慢上涨,数据库文件删不掉,close 返回 SQLITE_BUSY。原因多半是 prepare 成功后某个错误分支直接返回,没有执行 finalize;或者把 stmt 存进全局变量,后面忘了释放。close 之前可以先遍历未决语句,确认没有挂着的 stmt。
int sqlite3_close_v2(sqlite3 *db); sqlite3_stmt *sqlite3_next_stmt(sqlite3 *db, sqlite3_stmt *stmt);封装层用“最后执行”结构保证 finalize,不管 step 成功还是失败都走同一条释放路径。close 返回非 0 时不要直接退出,先把错误码和未决语句数量记到日志里。数据库文件删不掉时,先关连接,再检查有没有 -wal、-shm 残留,不要直接强删正在使用的文件。
5.3 DLL 位数不匹配:LabVIEW 64 位却加载 32 位 sqlite3.dll
现象是 CLFN 配置完调用就报加载失败,或者 LabVIEW 直接崩溃。原因很简单:LabVIEW 位数和 sqlite3.dll 位数不一致。先看 LabVIEW 关于信息,再用工具看 DLL 的 PE 头,确认 Machine 字段是 x86 还是 x64。命令行里可以用 dumpbin 查,找不到 dumpbin 就用 Python 读 PE 头,方法很多,核心是别靠文件名猜。
dumpbin /headers sqlite3.dll | findstr /i machine解决方式是把对应位数的 DLL 放到工程 bin 目录,CLFN 里用相对路径或工程内绝对路径加载。不要往系统目录塞 DLL 来“碰运气”,目标机可能已经有另一个版本,加载顺序一变就出玄学问题。换 DLL 后把 LabVIEW 重启一次,确保旧模块没有缓存。
5.4 长路径、权限与杀软占用
现象是 open 返回 14,文件明明存在但打不开;或者昨天能写,今天突然只读。原因可能是路径太长、目录没有写权限、文件被同步盘或杀软扫描占用。先把数据库路径缩短,放到用户可写目录,再用系统工具确认当前账户有写权限。不要放在需要管理员权限的目录里长期运行。
ls -l data/measure.db test -w data/measure.db && echo "db writable" || echo "db readonly"如果文件被占用,关掉数据库浏览器、同步客户端和杀软实时扫描再试。WAL 模式下目录里会多出 -wal 和 -shm 文件,权限要一起检查。解决后不要只改当前机器,把数据目录规则写进部署说明,避免下一台机器再踩一遍。
5.5 事务没提交导致“写进去又没了”
现象是 LabVIEW 里查得到数据,程序重启后数据消失。原因是连接上开了事务但没 COMMIT,close 时回滚了。SQLite 的自动提交默认开启,一旦显式 BEGIN,后面必须 COMMIT 或 ROLLBACK。错误分支里只写日志不 ROLLBACK,连接又被复用,下一批数据可能继续挂在这个事务里。
BEGIN IMMEDIATE; INSERT INTO measure(ts, channel, value, status) VALUES (?, ?, ?, ?); -- 忘记 COMMIT,close 时回滚 ROLLBACK;解决方式是把 COMMIT 放在批处理成功的唯一出口,失败出口统一 ROLLBACK,并在日志里记录批次号和条数。验证时用独立连接查 count(*),不要用同一个连接查,因为同一个连接看到的是未提交数据。对关键批次,提交后可以再查一次 last_insert_rowid 和总条数,确认落盘。
6. 进阶:把 SQLite 做成可复用的 LabVIEW 数据层:WAL、busy_timeout 与索引验证
数据层要长期跑,三个 PRAGMA 和一次查询计划验证比多写几个 VI 更值。打开连接后先设 journal_mode=WAL,让读写不互相堵死;再设 busy_timeout,遇到锁先等而不是立刻报错;synchronous 按数据重要性选 NORMAL 或 FULL。WAL 下读连接不会阻塞写连接,但 checkpoint 需要有人做,wal_autocheckpoint 可以设一个合理页数,不要让 WAL 文件无限长。
PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000; PRAGMA wal_autocheckpoint = 1000;| PRAGMA | 建议值 | 影响 |
|---|---|---|
| journal_mode | WAL | 读写并发更好,目录多出 -wal/-shm |
| synchronous | NORMAL/FULL | NORMAL 快,掉电可能丢最近事务;FULL 更稳 |
| busy_timeout | 3000~5000 | 锁等待时间,按写入周期调 |
| wal_autocheckpoint | 1000 页左右 | 控制 WAL 增长,频繁 checkpoint 会影响写性能 |
索引不要凭感觉加。采集表最常用的是按时间范围和通道查询,建联合索引 (ts, channel) 或 (channel, ts),顺序看 WHERE 里哪个条件选择性更高。建完用 EXPLAIN QUERY PLAN 看有没有 SCAN TABLE;出现全表扫描就说明索引没命中,或者条件里对字段套了函数。把查询计划输出贴到调试日志里,比事后猜快得多。
CREATE INDEX IF NOT EXISTS idx_measure_ts_channel ON measure(ts, channel); EXPLAIN QUERY PLAN SELECT id, ts, channel, value FROM measure WHERE ts BETWEEN ? AND ? AND channel = ?;我现在的习惯是:每次交付前用独立连接跑一遍“写入一批、强制关连接、再读回、删测试库”的流程,确认 WAL 文件、事务提交、索引都正常,再让程序连续跑一夜。从那以后我每次改表结构都强制走一遍这个最小闭环,不再相信“前面板能显示就是写进去了”。这份 LabVIEW+SQLite 的封装示例和 SQL 脚本,适合先拿最小闭环跑通,再按你的表结构替换;需要取用的人在项目页按说明拿,先别改封装,把 open、建表、插入、读回四步走通再说。希望帮到你。
本文还有配套的精品资源,点击获取