简介:面向LabVIEW开发者与测试测控工程师的SQLite数据库操作资源包,聚焦在图形化编程环境中快速完成本地数据库读写、建库建表与表格式数据插入,适合希望绕开复杂配置、直接集成轻量数据库的项目场景。压缩包共148个文件,约2.35MB,主体为117个vi与7个ctl,配合mnu菜单、lvlib与lvproj工程文件组织调用,并含sql脚本、txt说明、dll/so动态库及pdf资料,兼顾32位与64位运行环境。资源已有3160人学习下载,说明其在LabVIEW数据库工具选型中具备一定参考价值。内容强调自动生成数据库、表格形式写入、缓存清理和简化编程,读者可据此搭建可复用的数据库操作模块,理解控件、函数库与工程结构之间的配合方式,并借鉴现成VI缩短从建库到数据存取联调的路径。
1. 采集数据从 CSV 搬到单文件库:为什么我在 LabVIEW 里选了 SQLite
产线上位机跑着 LabVIEW,一秒几十个点,一年下来 CSV 滚到几个 G,用 Excel 打开要等十几秒,想查某个通道昨天下午三点的值只能写脚本扫全文件。我在一个模拟项目里把落盘换成了 LABVIEW SQLite 数据库:单文件、不用装数据库服务、SQL 直接按时间切片查,拷走一个 .db 文件就能在别的机器上复盘。这套做法围绕一件事——用 Call Library Function Node 直调 sqlite3.dll,把采集数据写进单文件库,再用 SQL 做区间统计。适合手上已有 LabVIEW 采集程序、被 CSV 或 Access 拖慢过的上位机开发者;如果只是存几十行参数配置,INI 就够,不用上这套。
2. 三条接入路线:ODBC、CLFN 直调 DLL、.NET 包装怎么选
LabVIEW 本身不带数据库能力,要么靠 NI 的 Database Connectivity Toolkit 走 ODBC,要么自己调 DLL,要么借 .NET 的托管驱动。三条路都能把数据写进同一个 .db 文件,差别在部署复杂度、可控粒度和出问题时的排查成本。我三个都跑过一遍,最后在采集场景里选了 CLFN,理由在后面几节展开。
2.1 ODBC 路线:最快出活,依赖链也最长
ODBC 的链路是 LabVIEW → Database Connectivity Toolkit → ODBC Driver Manager → SQLite ODBC 驱动 → .db 文件。你需要先装一个 SQLite 的 ODBC 驱动,然后在 Windows 的 ODBC 数据源管理器里配 DSN,或者直接用无 DSN 的连接串。
配 DSN 这一步有个硬性前提:驱动的位数必须和 LabVIEW 进程的位数一致。32 位 LabVIEW 只能看见 32 位驱动,而系统默认打开的那个 ODBC 管理器是 64 位的,你在里面配半天天也白搭。手动敲下面这条命令,才能打开 32 位那个界面。
:: 32 位 LabVIEW 必须用这个界面配 DSN,直接点控制面板里那个是 64 位的 C:\Windows\SysWOW64\odbcad32.exe在弹出来的界面里选“用户 DSN”或“系统 DSN”,添加 SQLite3 ODBC Driver,Database Name 填 .db 文件的绝对路径(这个字段不填就等于建了个内存库,程序一关数据全没),Timeout 建议给 2000 以上。不想配 DSN 的话,也可以直接拼连接串,塞给 DB Tools Open Connection.vi。
; 无 DSN 连接串,方括号里的驱动名必须和 ODBC 管理器里显示的完全一致 DRIVER={SQLite3 ODBC Driver}; Database=D:\data\demo.db; Timeout=2000; NoTXN=0; SyncPragma=NORMAL;Timeout对应底层等待锁的毫秒数,NoTXN=0表示允许显式事务(这个值如果被改成 1,你写的 BEGIN/COMMIT 会被驱动吞掉),SyncPragma影响 fsync 频率。这套配置的好处是几乎不用写调用代码,DB Tools 那几个 VI 拖出来连上线就能跑;坏处是每一层都可能出问题——驱动没装、位数不对、DSN 名拼错、驱动版本和你建库时用的 SQLite 版本不一致导致新语法不认。排查时你只能看到一个笼统的“Data source name not found”,得一层层往上翻。
2.2 CLFN 直调 sqlite3.dll:多写二十个节点,换来确定性
Call Library Function Node 直接调 sqlite3.dll 导出的 C 函数,中间没有驱动管理器,没有 Toolkit 的封装层,出错时sqlite3_errmsg会把原因原样告诉你。代价是你要自己管句柄、自己配每个参数的传递方式、自己处理字符串编码。
这条路要准备的文件就一个:和 LabVIEW 位数匹配的 sqlite3.dll。32 位 LabVIEW 配 32 位 DLL,64 位配 64 位,混用会在加载节点时直接报“库调用不匹配”或者干脆崩掉。把 DLL 放到和 VI 同级的目录,或者在 CLFN 配置里写绝对路径,别指望系统目录里那个一定是你要的版本。
我一般会把十几个 CLFN 节点封成一组子 VI:Open、Exec、Prepare、Bind、Step、Column、Finalize、Close、ErrMsg,每个子 VI 只做一件事,错误簇串起来。这样主程序里看到的是一条错误线加几个方块,而不是满地散落的 CLFN 节点。封装一次,后面所有项目都能复用。
2.3 .NET 路线:只在已有 .NET 环境时划算
LabVIEW 有 .NET 构造器节点,可以引用 System.Data.SQLite 或者 Microsoft.Data.Sqlite 这类托管程序集,用 Connection / Command / DataReader 那一套。写法比 CLFN 顺手,参数化查询是现成的,不用自己 bind。
但它的隐性成本在部署:目标机器上得有对应版本的 .NET Framework 或者 .NET 运行时,程序集的位数和平台目标(x86 / x64 / AnyCPU)要和 LabVIEW 对齐,混合模式程序集还会牵扯到加载策略。我遇到过在开发机上跑得好好的 VI,拷到现场机器上构造器节点报“无法加载文件或程序集”,查了半天是那台机器缺运行库。
如果你的项目本来就有 .NET 组件,顺手用这套没问题;如果是纯 LabVIEW 环境、又要绿色部署,我不建议为了一个 SQLite 引入运行时依赖。
2.4 选型对照
| 维度 | ODBC + Toolkit | CLFN 直调 DLL | .NET 包装 |
|---|---|---|---|
| 额外部署物 | ODBC 驱动 + Toolkit | 一个 sqlite3.dll | .NET 运行时 + 程序集 |
| 位数敏感点 | 驱动位数、DSN 可见性 | DLL 位数 | 程序集平台目标 |
| 参数化查询 | Toolkit 自带 | 自己配 bind | 原生支持 |
| 出错信息可读性 | 笼统 | 原样返回 | 较清晰 |
| 大批量写入控制 | 受驱动影响 | 完全可控 | 可控 |
| 上手成本 | 低 | 中 | 中 |
采集落盘这种要长期无人值守跑的场景,我选 CLFN,因为部署物最少、失败时能拿到确切错误码;报表导出、临时查询这类一次性需求,ODBC 更快。
3. CLFN 打通读写:句柄、预处理语句与类型映射
这一章是整套东西的地基。CLFN 的难点不在 SQL,在参数怎么摆——同一个函数,参数传递方式配错一格,结果就是返回 0 但数据没进去,或者读字符串读出一段乱码。下面按打开、建表、写入、读取的顺序走一遍。
3.1 打开与关闭:句柄放移位寄存器,别放局部变量
用sqlite3_open_v2比sqlite3_open多两个参数,但能指定打开模式和线程模型,值得。参数配置如下。
| 位置 | DLL 参数 | LabVIEW 类型 | 传递方式 | 方向 | 说明 |
|---|---|---|---|---|---|
| 1 | filename | 字符串 | C 字符串指针 | 输入 | 必须是 UTF-8 字节,中文路径要先转码 |
| 2 | ppDb | U64 | 指向值的指针 | 输出 | 回传数据库句柄 |
| 3 | flags | I32 | 值 | 输入 | 填 65542 |
| 4 | zVfs | 字符串 | C 字符串指针 | 输入 | 传空字符串即可 |
| 返回 | int | I32 | 值 | 输出 | 0 表示成功 |
flags填 65542 是三个标志位按位或的结果:READWRITE(2) 加 CREATE(4) 加 FULLMUTEX(65536)。FULLMUTEX 打开串行化模式,多个循环同时用同一个句柄时不会互相踩内存,代价是每次调用有一点点开销——采集这种量级完全无所谓,我建议默认加上。
下面这段是把打开、执行、关闭串起来的主干结构,用文本示意框图连线顺序,照着搭就行。
While循环(采集主循环) ├─ 移位寄存器:dbHandle (U64),初值 0 ├─ 条件结构:dbHandle == 0 → 调 sqlite3_open_v2 │ ├─ 返回码 != 0 → 调 sqlite3_errmsg 取错误串,写日志,停止循环 │ └─ 成功 → 把 ppDb 输出接到移位寄存器右侧 ├─ 业务逻辑:采集 → bind → step └─ 循环结束(停止按钮) └─ 循环外:sqlite3_close_v2(dbHandle)句柄必须走移位寄存器,不要用局部变量在几个循环之间传。我见过用局部变量传句柄的写法,两个并行循环同时读写,一个循环刚关闭连接,另一个还在往里写,直接返回 SQLITE_MISUSE。关闭用sqlite3_close_v2,它在还有未 finalize 的语句时会返回错误而不是直接释放,能帮你发现漏掉的 finalize。
3.2 建表:把 DDL 交给 sqlite3_exec
建表、建索引、设 PRAGMA 这类没有结果集的操作,用sqlite3_exec最省事。参数只有四个:句柄(值传递 U64)、SQL 字符串(C 字符串指针)、回调(传空指针)、错误信息指针(传空指针),返回 I32。
-- 采集表:时间戳统一存 Unix 秒,通道名单独建索引 CREATE TABLE IF NOT EXISTS chan_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, chan TEXT NOT NULL, value REAL NOT NULL, quality INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_chan_ts ON chan_data(chan, ts); -- 关键 PRAGMA,建库时执行一次即可(WAL 会持久化到库文件) PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000; PRAGMA cache_size = -20000; PRAGMA temp_store = MEMORY;逐条说:idx_chan_ts是联合索引,查询按“某通道某时间段”走,索引顺序必须是 chan 在前 ts 在后,反了就用不上。journal_mode=WAL让读写不互相阻塞,写的时候查询照样能跑,这个设置在库文件里是持久的,执行一次就够。synchronous=NORMAL在 WAL 模式下不再每个事务都 fsync,只在检查点刷盘,掉电可能丢最后几个事务,采集数据能接受;如果是计费、报警记录这类不能丢的,改成 FULL。busy_timeout=5000是遇到锁时等 5 秒再报错,比立刻返回 SQLITE_BUSY 友好得多。cache_size是负值时单位是 KB,-20000 就是约 20MB 页缓存。
注意:sqlite3_exec一次只能执行一条语句,上面那几行要拆成多次调用,或者拼成一个用分号分隔的字符串分次传。我一般写成常量数组,用 For 循环逐条 exec,哪条失败日志里能直接看出是哪一句。
3.3 写入四步法:prepare / bind / step / reset
单条 INSERT 用 exec 拼字符串也能写,但通道名里带个单引号就翻车,还有注入风险。正规做法是预处理语句,四步走,而且这条语句可以在循环里反复复用。
第一步 prepare。sqlite3_prepare_v2(db, zSql, nByte, ppStmt, pzTail),db 用值传递 U64,zSql 是 C 字符串指针,nByte 传 -1 表示让 SQLite 自己算长度,ppStmt 是输出指针(U64,指向值的指针),pzTail 传空指针。
第二步 bind。常用的是这几个:
| 函数 | 用途 | 关键参数 |
|---|---|---|
| sqlite3_bind_int64 | 绑定整数 | 参数序号从 1 开始 |
| sqlite3_bind_double | 绑定浮点 | 直接传 DOUBLE |
| sqlite3_bind_text | 绑定字符串 | 最后一个参数传 -1 |
| sqlite3_bind_null | 绑定空值 | 只有序号 |
sqlite3_bind_text的第五个参数是析构回调,传 0(SQLITE_STATIC)表示“这个指针我一直有效,你别拷贝”,传 -1(SQLITE_TRANSIENT)表示“你现在就拷一份走”。在 LabVIEW 里字符串控件的内容会被下一次赋值改写,一定要传 -1。这个参数在 CLFN 里配成指针类型,填 -1 需要用“转换为指针”把 I32 的 -1 转过去,或者干脆把参数类型配成 I64 值传递——具体看你手上那份 DLL 的导出签名。
-- 预处理的 INSERT,四个 ? 对应四个 bind 序号 INSERT INTO chan_data(ts, chan, value, quality) VALUES (?, ?, ?, ?);第三步 step。sqlite3_step(stmt)返回 101(SQLITE_DONE)表示这条语句执行完了,返回 100(SQLITE_ROW)表示有结果行可读,其他值都是错误。
第四步 reset。循环里复用同一条语句时,每次 step 完调sqlite3_reset(stmt)把语句恢复到绑定前的状态,绑定值会保留,所以下一轮只 bind 变化的字段也行;想彻底清空就再调一次sqlite3_clear_bindings。
For循环(每条采集记录) ├─ sqlite3_bind_int64(stmt, 1, unixTs) ├─ sqlite3_bind_text (stmt, 2, chanName, -1, -1) ├─ sqlite3_bind_double(stmt, 3, value) ├─ sqlite3_bind_int64(stmt, 4, quality) ├─ sqlite3_step(stmt) → 判断是否等于 101 └─ sqlite3_reset(stmt)顺序不能颠倒:bind 之后才能 step,step 之后必须 reset 才能进入下一轮,漏掉 reset 会一直返回 SQLITE_MISUSE。语句用完(比如退出循环或者换 SQL)必须sqlite3_finalize,否则 close 的时候关不掉,库文件会一直被占用。
3.4 读取与类型映射:column 的缓冲区长度一定要给
查询走同样的 prepare / step,只是 step 返回 100 时用sqlite3_column_*取值。sqlite3_column_text返回的是const unsigned char*,在 CLFN 里配成“C 字符串指针”、方向输出,同时必须填一个够大的缓冲区长度(Minimum size),否则 LabVIEW 只读到很短一截,长文本直接被截断。我一般给 512 或 1024,够存路径和时间串。
类型映射这块,LabVIEW 和 SQLite 的对应关系建议固定下来,别随手写:
| LabVIEW 类型 | SQLite 存储类 | 建表声明 | 备注 |
|---|---|---|---|
| I32 / I64 | INTEGER | INTEGER | 时间戳、计数、状态码 |
| DBL | REAL | REAL | 采集值 |
| 字符串 | TEXT | TEXT | 通道名、备注,UTF-8 |
| 布尔 | INTEGER | INTEGER | 0 / 1 |
| 空 | NULL | — | 用 bind_null 显式写 |
SQLite 是动态类型,你把字符串写进 INTEGER 列它也不报错,这是后面第 5 章要说的坑之一。我习惯在建表时把类型写死,读的时候按声明的类型取,不做隐式转换。
3.5 时间戳:LabVIEW 的 1904 和 Unix 的 1970 差 2082844800 秒
LabVIEW 的“获取日期/时间(秒)”返回的是自 1904 年 1 月 1 日起的秒数,SQLite 里大家习惯存 Unix 时间(1970 起)。两个纪元之间差 2082844800 秒,入库前减掉,出库显示时再加回来。
unixTs = LabVIEW秒 - 2082844800 LabVIEW秒 = unixTs + 2082844800统一存 Unix 秒的好处是:用datetime(ts,'unixepoch','localtime')直接在 SQL 里转成本地时间显示,不用在 LabVIEW 里做格式化;跨时区复盘时也不会有歧义。如果对毫秒有要求,就存毫秒并在字段名上标_ms,别一半存秒一半存毫秒,这种库后面没法用。
4. 批量写入调优:把 10 万点从 90 秒压到 3 秒
第一次跑通写入的时候,我清了 10 万个点,秒表掐了 90 秒出头,CPU 占用还不高——典型的 I/O 等待。原因不复杂:每条 INSERT 单独提交,每次提交都要落盘。这一章讲怎么把这段压下去,以及每一步的代价。
4.1 自动提交为什么慢
SQLite 默认每条语句是一个独立事务。事务提交时要保证数据落到磁盘,机械盘一次 fsync 是毫秒级,SSD 也有一两百微秒。10 万条就是 10 万次 fsync,时间全花在等磁盘上,跟 SQL 写得好不好没关系。
在采集程序里这个代价更明显:如果写入是在采集循环里同步做的,每条数据都要等一次落盘,采样周期一旦短于写入耗时,循环就开始拖,时间戳跟着漂。这是很多人说“LabVIEW 写数据库卡”的真实原因,不是 LabVIEW 慢。
4.2 BEGIN / COMMIT 包裹:最立竿见影的一刀
把 N 条 INSERT 包进一个事务,fsync 次数从 N 降到 1。N 取多少合适?太小没效果,太大则一次提交的内存占用和失败重试代价都上去了。我一般按 1000 条或者 200 毫秒,谁先到算谁。
-- 用 IMMEDIATE 而不是默认的 DEFERRED,开事务时就直接拿写锁 BEGIN IMMEDIATE; INSERT INTO chan_data(ts, chan, value, quality) VALUES (?, ?, ?, ?); -- 这条在循环里执行 1000 次 COMMIT;BEGIN IMMEDIATE和BEGIN的区别在于拿锁时机:默认的 DEFERRED 是先声明“我要写”,真正拿写锁是在第一条写语句执行时,如果这时候库被别的连接占着,就会在原地等或者直接失败;IMMEDIATE 在 BEGIN 那一刻就拿写锁,失败早、位置明确,配合 busy_timeout 效果更好。
注意:事务期间如果程序异常退出,没 COMMIT 的数据不会进库,但会留下一个-journal或-wal文件,下次打开时 SQLite 会自动回滚或恢复,这是正常行为,不是库坏了。丢掉的是最后不足一批的数据,这个量在设计上要能接受。
4.3 复用语句:prepare 一次,循环里只 bind
每轮循环都 prepare 一次、finalize 一次,SQL 解析和字节码编译的开销加起来也不少。正确姿势是在循环外 prepare 一次,循环里 bind → step → reset,循环结束再 finalize。
顺序(循环外): sqlite3_prepare_v2 → stmt sqlite3_exec("BEGIN IMMEDIATE") 顺序(循环内,每 1000 条): bind ×4 → step → reset 计数到 1000 → sqlite3_exec("COMMIT") → sqlite3_exec("BEGIN IMMEDIATE") 顺序(循环外): sqlite3_exec("COMMIT") sqlite3_finalize(stmt)reset 之后绑定值还在,所以如果通道名在整批里不变,只有 ts 和 value 变,那就只 bind 这两个序号,剩下两个跳过——省两次调用。这个优化在百万级写入时能看出来,十万级不太明显,但代码本身更干净。
4.4 WAL 与 synchronous:把 fsync 从每事务降到每检查点
WAL 模式下写操作先追加到-wal文件,不直接改主库,读操作读主库加 WAL 的合并视图。好处是读写不打架,采集在写的同时,报表查询照样能跑。
PRAGMA synchronous三档:FULL 每个事务都 fsync,最保险也最慢;NORMAL 只在检查点 fsync,WAL 模式下掉电可能丢最近若干事务,但库不会损坏;OFF 交给操作系统,掉电可能连库结构都出问题,别用。
采集落盘我固定用journal_mode=WAL+synchronous=NORMAL,丢失窗口是最后几秒的数据,对趋势分析没影响。如果是报警记录、批次追溯这种一条都不能少的,改回 FULL,性能下降大概一个数量级,但依然比逐条 autocommit 快得多。
4.5 实测对比
同一台机器,10 万个点,四个通道,单条 INSERT,只改事务和 PRAGMA 设置:
| 组合 | 耗时 | 说明 |
|---|---|---|
| 逐条 autocommit | 约 90 s | 每条一次 fsync |
| 1000 条一事务 | 约 3.2 s | fsync 降到 100 次 |
| 事务 + 复用 prepared stmt | 约 2.6 s | 省掉重复编译 |
| 事务 + WAL + synchronous=NORMAL | 约 1.8 s | 落盘策略放宽 |
| 同上,再加 200ms 定时提交 | 约 2.0 s | 延迟更平滑,略慢一点 |
最后一行的意义在于实时性:按条数提交会让写入有阵发性的停顿,按时间提交(比如 200ms 一次)能让磁盘压力摊平,采集循环的抖动小很多。我现在的默认配置是“200ms 或 1000 条,先到先提交”,兼顾吞吐和延迟。
5. 避坑与排查:位数、编码、锁、路径的四类翻车现场
这一章是血泪经验合集。前面几章讲的是怎么让它跑起来,这一章讲它跑起来之后为什么又倒下了。每条按现象、原因、解决写,遇到类似症状可以先对号入座。
5.1 现象:ODBC 里配好的 DSN,LabVIEW 就是看不见
现象:在 ODBC 数据源管理器里明明加了一个 SQLite 数据源,下拉列表里也能看到,但 LabVIEW 里 DB Tools Open Connection 一调就报“Data source name not found and no default driver specified”。
原因:位数不匹配。控制面板里点开的那个 ODBC 管理器是 64 位的,你在里面配的 DSN 只对 64 位进程可见。手上的 LabVIEW 是 32 位的,它去读的是另一套注册表分支,自然找不到。同理,装驱动的时候如果装的是 64 位版,32 位 LabVIEW 也加载不了。
解决:先确认 LabVIEW 的位数(帮助 → 关于里能看到),然后用SysWOW64\odbcad32.exe打开 32 位管理器重新配一遍。驱动也要装对应位数的。判断方法很直接:在那两个管理器的“驱动程序”标签页里分别看一眼,只有一边有 SQLite3 ODBC Driver,就说明只装了一个位数。
5.2 现象:库能打开,写进去的中文全是问号
现象:路径里带中文的库文件,sqlite3_open_v2返回 14(SQLITE_CANTOPEN);或者库打开了,但通道名写进去读出来是一串问号、乱码。
原因:LabVIEW 的字符串本质是字节数组,默认编码跟系统区域设置有关,而 SQLite 的接口一律按 UTF-8 解释const char*。中文路径没转码就传进去,SQLite 按 UTF-8 解析失败,自然打不开;写文本时同理,非 UTF-8 的字节被原样存进去,读出来再按 UTF-8 解码就是乱码。
解决:所有传给 DLL 的字符串(文件路径、SQL 里的中文字面量、bind 的文本值)都先用“转换为 UTF-8”函数转一遍再送进 CLFN;从sqlite3_column_text读回来的字节,也要按 UTF-8 解码成正常字符串再显示。最省事的办法是把数据目录设成纯英文路径,只有通道名、备注这类业务字段保留中文——路径的坑直接绕过去,业务字段按 UTF-8 走。
5.3 现象:跑一段时间报 database is locked
现象:采集循环跑得好好的,突然 step 返回 5(SQLITE_BUSY),错误串是 “database is locked”,之后每次写入都失败,直到重启程序。
原因通常有三种。一是同一个库被两个连接同时写,比如主程序和另一个导出 VI 各开了一个连接;二是某个事务开了没提交,写锁一直挂着,最常见的是异常分支里直接跳出了循环,COMMIT 那句没执行到;三是 prepare 了语句但没 finalize,连接关不干净,锁释放不掉。
解决:先设PRAGMA busy_timeout=5000兜底,让它在锁冲突时等一会儿而不是立刻失败;再保证“一个库只有一个写连接”,导出、查询走另一个只读连接或者干脆另开一个库;最后把 BEGIN 和 COMMIT 放进条件结构的两条分支里,确保任何出口都会走到 COMMIT 或 ROLLBACK。我现在的习惯是写一个带错误簇的子 VI 包住整个事务,出错自动 ROLLBACK,不给锁泄漏的机会。
5.4 现象:明明写了数据,按条件查却查不到
现象:采集值和状态码都写进去了,用SELECT * FROM chan_data WHERE quality = 1查,一行都没有;把条件换成quality = '1'反而能查到几条。
原因:SQLite 是动态类型,列上声明的 INTEGER 只是“类型亲和性”,不是强制约束。如果某次 bind 的时候用错了函数,把状态码当字符串绑进去了,存进去的就是 TEXT 类型的 '1',它和整数 1 在比较时不是一回事。这种错误不会报任何异常,写的时候一切正常,查的时候才发现数据是脏的。
解决:两件事。一是建表时加约束,quality INTEGER NOT NULL CHECK (quality IN (0,1,2)),脏数据在写入时就被挡住;较新的 SQLite 还支持 STRICT 表,声明成CREATE TABLE ... ) STRICT;之后类型写错直接报错,能从根上杜绝。二是 bind 的时候严格按列类型选函数,整数就用 bind_int64,别图省事全用 bind_text。
5.5 现象:查出来一片重复值,每条记录都长得一样
现象:循环里写 1000 条,每条的时间戳和数值都不同,入库后查出来 1000 行,但 ts 和 value 全是最后一条的值。
原因:sqlite3_bind_text的析构参数传了 0(SQLITE_STATIC)。这个值的含义是“这个字符串指针在 step 之前一直有效,你不用拷贝”,SQLite 就只存了指针。而 LabVIEW 里同一个字符串控件在下一轮循环被重新赋值,指针指向的内容变了,等真正 step 落盘时读到的是最新内容——所有行都成了最后一条。
解决:析构参数一律传 -1(SQLITE_TRANSIENT),让 SQLite 在 bind 的时候立刻拷贝一份。代价是每次 bind 多一次内存分配,在十万级写入下几乎测不出来。这个坑我在两个项目里各踩过一次,第二次是因为复制了旧代码——所以现在封装 bind 的子 VI 里,这个参数是写死的常量,不给改的机会。
另外补一个相关的:程序异常退出后目录里多出demo.db-wal和demo.db-shm两个文件,别手动删。正常情况下重新打开库会自动恢复;手动删了-wal就等于丢掉了尚未合并进主库的那部分数据。
6. 进阶:配置库与数据日志库分离,加一段自检脚本
一个库干所有事,早晚出问题。我的做法是拆成两个文件:config.db存通道配置、标定系数、报警阈值,读多写少;data.db只存采集流水,写完基本不改。两个库各自开连接,配置库在程序启动时读一次进内存,运行期不再碰,采集循环只跟数据日志库打交道。这样配置改错、导出卡顿都不会影响采集落盘,回滚的时候也只是换个配置文件。
数据日志库按天或按月分文件,命名用data_20250101.db这种形式,单个文件别超过几个 G。查询跨天的时候用ATTACH DATABASE挂上去做联合查询:
-- 跨两个日志文件做区间统计,attach 之后可以用别名访问 ATTACH DATABASE 'D:\data\data_20250102.db' AS d2; SELECT chan, COUNT(*) AS n, AVG(value) AS avg_v FROM ( SELECT chan, value FROM main.chan_data WHERE ts BETWEEN 1735689600 AND 1735775999 UNION ALL SELECT chan, value FROM d2.chan_data WHERE ts BETWEEN 1735689600 AND 1735775999 ) GROUP BY chan; DETACH DATABASE d2;交付前我一定会跑一遍自检脚本,检查库是不是真的按预期建好了。用 Python 的标准库就够,不依赖任何第三方包,现场机器上也能跑:
import sqlite3, sys, time db = sys.argv[1] con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) cur = con.cursor() # 1) 日志模式必须是 wal,否则读写会互相阻塞 mode = cur.execute("PRAGMA journal_mode").fetchone()[0] print("journal_mode:", mode) # 2) 索引存在性,缺了就得全表扫 idx = cur.execute( "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='chan_data'" ).fetchall() print("index:", [r[0] for r in idx]) # 3) 行数与时间跨度,顺便看看时间戳是不是漂了 n, lo, hi = cur.execute( "SELECT COUNT(*), MIN(ts), MAX(ts) FROM chan_data" ).fetchone() print("rows:", n) if n: fmt = "%Y-%m-%d %H:%M:%S" print("span:", time.strftime(fmt, time.localtime(lo)), "->", time.strftime(fmt, time.localtime(hi))) # 时间戳如果落在 1904 附近,说明忘了减 2082844800 if lo < 0: print("WARN: 时间戳疑似未转 Unix 纪元") # 4) 脏数据抽查:类型亲和性没兜住的那部分 bad = cur.execute( "SELECT COUNT(*) FROM chan_data WHERE typeof(quality) != 'integer'" ).fetchone()[0] print("non-integer quality:", bad) con.close()脚本里用mode=ro以只读方式打开,避免自检本身把库锁住。四个检查点分别对应四类常见问题:日志模式没设对、索引漏建、时间戳纪元搞错、动态类型导致的脏数据。跑一遍十秒钟,比到现场才发现查不动强得多。
参数上可以再抠一点:PRAGMA mmap_size给到 268435456(256MB)能让读多写少的配置库走内存映射,查询快一截;PRAGMA wal_autocheckpoint默认 1000 页,采集频繁的时候可以调到 2000,减少检查点打断写入的次数。这些都不是必选项,先把前四章的东西跑稳,再回来调这些。
从那以后我每次交付上位机程序前,都会先拿一段历史数据跑一遍这个自检脚本,确认 journal_mode 是 wal、索引在、时间戳没漂、脏数据为零,再交给现场。踩过的坑不想踩第二遍。这套 CLFN 封装的 VI、建表 SQL 和自检脚本我整理在一起了,按第 3 章的参数表配一遍就能跑通。希望帮到你。
本文还有配套的精品资源,点击获取