news 2026/9/30 15:02:50

SQLite 基本命令与 C/C++ 接口实战:从嵌入式场景到代码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQLite 基本命令与 C/C++ 接口实战:从嵌入式场景到代码实现

SQLite 这个数据库,在嵌入式圈子里基本就是"标配"般的存在。我最早接触它是在做一个车载数据记录仪的项目,内存只有几十兆,却要实时存储 GPS 轨迹、传感器日志、设备状态,还要支持事后按时间范围查询。当时团队里有人提议上 MySQL,直接被功耗和存储空间劝退。后来换了 SQLite,一行建表语句、几个 API 调用,问题就解决了。这也是我为什么一直建议刚入门嵌入式或者 C/C++ 开发的朋友,先别急着啃那些重型数据库,把 SQLite 玩明白,很多场景真的够用。

这篇文章就围绕 SQLite 的"基本命令操作"展开,从命令行到 C/C++ 接口,把我实际用过、踩过坑的地方都捋一遍。内容面向小白,但不会只停留在"敲命令"层面,我会把背后的设计逻辑和坑点也讲清楚,保证你读完不光能上手,还能理解为什么这样做。

1. 为什么嵌入式项目里 SQLite 出现频率这么高

很多人第一次听到"嵌入式数据库",下意识会觉得是个很复杂的东西。实际上 SQLite 的设计哲学恰恰相反——它不复杂,甚至可以说是"简单到极致"。

1.1 零配置和单文件存储

SQLite 不需要独立的服务器进程,不需要配置文件,不需要用户密码体系。你的整个数据库就是一个普通文件,后缀通常是.db,但这个文件内部是结构化存储的。程序要使用它,只需要通过 SQLite 提供的 API 打开这个文件即可。

这种设计带来的直接好处是部署成本极低。你在 Windows 上创建了一个数据库文件,拷到 Linux 开发板上,只要架构一致,直接就能打开使用。我在实际项目里用 U 盘在开发板和 PC 之间拷数据库文件是常事,完全不用关心"导出导入"这类繁琐操作。

1.2 为什么 C/C++ 场景选它最顺手

SQLite 本身就是用 C 语言写的,所以对 C/C++ 的亲和力天然就好。它提供了一个 C 语言 API 库,你只需要在源码中包含sqlite3.h,链接时加上sqlite3库,就能直接调用。没有任何中间层,也没有 JVM、解释器之类的运行时依赖。

对于嵌入式设备,这太重要了。因为嵌入式系统资源受限,CPU 主频低、内存小、Flash 空间有限。SQLite 编译后的库文件大概只有几百 KB 到 1MB 左右(取决于编译选项),运行时内存占用也可以控制在较低水平。相比之下,如果引入 MySQL 或者 PostgreSQL 的客户端库,体积和依赖会复杂得多。

还有一点,SQLite 对断电恢复有专门的日志机制。虽然不能像企业级数据库那样做到极端数据安全,但在嵌入式常见的异常断电场景下,配合WAL模式(后面会讲到),可以把数据损坏的概率降到很低。

1.3 小白最容易误解的一点

可能有人会说:"SQLite 能处理并发吗?性能会不会很差?"这个问题要分场景看。

SQLite 支持多线程访问,但写入操作是串行的——同一时刻只允许一个写事务。这跟 MySQL 的 MVCC 并发控制完全不同。但这恰恰是嵌入式场景需要的特性:数据量小、并发写少、读写快速。如果你硬要用 SQLite 去扛每秒几千次并发写入,那是用错了地方;但如果你的设备每秒只记录几条数据,SQLite 的性能绰绰有余。

所以选型的时候要先认清需求:你的数据量是多少?写入频率多高?需不需要多进程同时写?想清楚这些,就不会纠结 SQLite 够不够用了。

2. 环境准备:命令行工具和 C/C++ 开发库的两种获取方式

在开始敲命令之前,先把环境搞定。这里分两条线:一条是 SQLite 命令行工具(用于手动操作数据库),另一条是 C/C++ 开发库(用于写代码时调用)。

2.1 Linux 环境下的安装

如果你用的是 Ubuntu 或者 Debian 系系统,安装非常简单:

sudo apt update sudo apt install sqlite3 libsqlite3-dev

这里要特别注意:sqlite3这个包是命令行工具,libsqlite3-dev是开发库(包含头文件和链接库)。很多小白只装了前者,结果在 C/C++ 里#include <sqlite3.h>的时候报找不到头文件,就是这个原因。

装完之后验证一下:

sqlite3 --version

能看到版本号就说明命令行工具正常了。

2.2 Windows 环境下的下载配置

Windows 下稍微麻烦一点,但也就是下载几个文件的事。到 SQLite 官网的下载页面,找到 Windows 区,通常需要这两个文件:

  • sqlite-tools-win32-x86-*.zip:命令行 shell 工具
  • sqlite-dll-win32-x86-*.zip或者sqlite-dll-win64-x64-*.zip:动态链接库
  • 如果要做开发,还需要sqlite-amalgamation-*.zip:源码合并包,里面包含sqlite3.h、sqlite3.c、sqlite3ext.h

下载后解压到一个目录,比如C:\sqlite,然后把C:\sqlite加到系统环境变量PATH里。这样在 CMD 里就能直接输入sqlite3命令了。

2.3 VSCode 配置 C/C++ 环境时容易踩的坑

如果你是在 VSCode 里写 C/C++ 代码调用 SQLite,有两条路可以走。

一条路是用系统库:Linux 下装了libsqlite3-dev,在tasks.json的编译命令里加-lsqlite3,在c_cpp_properties.json里配置头文件路径。Windows 下如果用的是 MinGW,可以在链接参数里加-lsqlite3,前提是你已经把库文件放到了 MinGW 的 lib 目录下。

另一条路是直接编译源码:把sqlite3.c和sqlite3.h直接放进你的项目源码目录,编译时把sqlite3.c一起编译。这种方式我建议新手优先考虑,因为它绕开了所有链接路径配置的麻烦,而且 SQLite 是公有领域代码,你可以放心地把它编进自己的项目里。

VSCode 里配置的头文件路径在.vscode/c_cpp_properties.json里配置的includePath优先级高于 IntelliSense 自动搜索。如果你发现代码编译能通过,但编辑器里全是红色波浪线,大概率就是这个配置有问题。

3. 从零开始:命令行下的库、表、增删改查实操

环境准备好后,打开终端输入sqlite3,你会进入一个sqlite>提示符。如果不带参数直接启动,它操作的是内存数据库,退出后数据就没了。我们需要指定数据库文件。

3.1 创建数据库文件:一条命令搞定

sqlite3 mydevice.db

如果mydevice.db不存在,SQLite 会自动创建这个文件并打开。如果存在,就直接打开。这一点非常符合嵌入式场景——设备第一次上电,程序启动后sqlite3_open一个文件路径,文件不存在就自动建,省去了繁琐的初始化流程。

进入sqlite>提示符后,你可以先看看当前数据库信息:

.databases

这个命令会列出当前连接的数据库文件路径。

3.2 建表:理解数据类型和约束

建表是数据库操作的第一步。我先给一个实际的例子,然后逐段解释。

CREATE TABLE sensor_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, temperature REAL NOT NULL, humidity REAL, sample_time TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), remark TEXT );

这张表模拟的是温湿度传感器数据的存储。下面拆解关键点:

  • id INTEGER PRIMARY KEY AUTOINCREMENT:自增主键。每次插入新记录时自动加 1,无需手动指定。AUTOINCREMENT关键字保证了 id 不会重复利用(即使用 DELETE 删掉最大 id 的记录,也不会把 id 回退)。但要注意,它会额外维护一个sqlite_sequence内部表,略微增加写入开销。如果对主键连续性要求不高,也可以只用INTEGER PRIMARY KEY,SQLite 同样会自增,只是在某些情况下会复用已删除的最大 id。
  • TEXT NOT NULL:非空约束。如果插入时不给这个字段赋值,SQLite 会直接报错。这对设备日志类场景很重要,避免写入半截数据。
  • REAL:浮点类型。SQLite 的类型系统比较灵活,这里先不展开,后面我会专门说。
  • DEFAULT (datetime('now', 'localtime')):默认值为当前本地时间。这样插入数据时不用手动填写时间戳,非常方便。

建表的命令是:

CREATE TABLE sensor_data (...);

建完表后可以用.schema sensor_data查看表结构确认一下。

3.3 插入数据:基本写操作

最基础的插入语句:

INSERT INTO sensor_data (device_id, temperature, humidity, remark) VALUES ('DEV-001', 25.6, 60.2, '客厅传感器');

注意几点:

  • 因为id是自增主键,我们没有给它赋值。
  • sample_time有默认值,不写会自动填充当前时间。
  • 如果humidity允许为空,也可以不写这个字段,比如:
INSERT INTO sensor_data (device_id, temperature, remark) VALUES ('DEV-002', 26.1, '设备未上报湿度');

这样humidity字段的值就是 NULL。在 SQLite 里,NULL 和空字符串完全是两回事,判断时要特别注意。

3.4 查询数据:最常用的 SELECT

查询是日常操作中最频繁的动作。几个高频写法:

-- 查询全部 SELECT * FROM sensor_data; -- 按条件过滤 SELECT * FROM sensor_data WHERE device_id = 'DEV-001'; -- 只查指定字段,按时间倒序 SELECT id, temperature, sample_time FROM sensor_data ORDER BY sample_time DESC; -- 限制返回条数 SELECT * FROM sensor_data LIMIT 10; -- 聚合统计 SELECT device_id, COUNT(*) AS record_count, AVG(temperature) AS avg_temp FROM sensor_data GROUP BY device_id;

这里要提醒一个坑:在 SQLite 命令行里,SQL 语句必须以分号;结尾才会执行。如果你敲完语句没加分号,SQLite 会认为你还要继续输入下一行,提示符会变成...>,这是很多新手卡住的地方。如果发现自己处于...>状态,直接输入;再回车就行。

3.5 更新和删除数据

-- 更新指定记录 UPDATE sensor_data SET temperature = 27.3, remark = '数据修正' WHERE id = 5; -- 删除指定记录 DELETE FROM sensor_data WHERE id = 5;

这里最重要的提醒是:UPDATE 和 DELETE 一定要带上 WHERE 条件,否则会作用到整张表的所有记录上。我刚学 SQLite 时,就因为在测试环境执行了没有 WHERE 的 DELETE,一张测试表的数据瞬间清空。生产环境遇到这种情况,那真是欲哭无泪。

如果想删除表中所有记录,推荐用:

DELETE FROM sensor_data;

或者更快的方式:

DELETE FROM sensor_data; -- 逐行删除 DELETE FROM sqlite_sequence WHERE name = 'sensor_data'; -- 重置自增计数

如果确实要清空表并保留表结构,也可以用:

DELETE FROM sensor_data;

但要注意,DELETE不会重置AUTOINCREMENT的计数。如果你希望 id 从 1 重新开始,需要手动更新sqlite_sequence表,或者干脆用DROP TABLE后重建。

3.6 删除表

DROP TABLE sensor_data;

这个命令会把整张表连同结构、数据、索引全部删除,不可恢复。执行前一定要确认三遍。

3.7 其他常用点命令

SQLite 命令行里,以点.开头的命令叫点命令,它们不是 SQL,比如:

.tables -- 查看当前库中所有的表 .schema 表名 -- 查看建表语句 .indexes 表名 -- 查看索引 .quit -- 退出命令行 .output 文件名 -- 将查询结果输出到文件 .headers on -- 显示列名 .mode column -- 以列模式显示,好看一些

我平时调试时最喜欢的是.headers on和.mode column组合,查询结果一目了然。

4. SQLite 的几个核心机制,理解了才不会踩坑

这一节的内容是很多教程忽略但实际开发中非常重要的部分。如果只看官方文档,容易忽略背后的设计意图。

4.1 动态类型:SQLite 和 MySQL 最大的区别

传统关系型数据库(比如 MySQL)的字段类型是严格的:你定义了一个 INT 字段,往里面插入字符串就会报错或者隐式转换。SQLite 不同,它采用动态类型系统。

在 SQLite 中,一个字段的"类型"更像是一种建议(或者说存储类别倾向),而不是强约束。你可以往INTEGER字段里存字符串,SQLite 不会报错;你也可以往REAL字段里存整数。它有五种存储类别:

存储类别说明
NULL空值
INTEGER有符号整数,根据值的大小用 1/2/3/4/6/8 字节存储
REAL浮点数,8 字节
TEXT字符串,使用数据库编码(UTF-8/UTF-16)
BLOB二进制数据,原样存储

这个特性在嵌入式场景里是一个双刃剑。好处是灵活,比如设备上报的数据有时候是字符串、有时候是数字,你可以统一存成 TEXT,处理起来方便;坏处是容易在不知不觉中存入了脏数据,查询统计时出现诡异结果。

我的建议是:在表设计时仍然要明确规定字段类型,让 SQLite 的类型建议发挥作用。你可以把它当成一个"弱约束",但不要主动去破坏它。程序代码里按约定写入正确类型,这样既能享受灵活性,又不会因为脏数据掉进坑里。

4.2 PRIMARY KEY 和 AUTOINCREMENT 的底层逻辑

SQLite 的这张表如果没有显式指定INTEGER PRIMARY KEY,它内部会有一个隐藏的rowid,作为每条记录的唯一标识。当你定义INTEGER PRIMARY KEY时,这个字段就成了rowid的别名。

这意味着:

  • INTEGER PRIMARY KEY字段的值必须是整数(按 SQLite 的规则,也可能是 NULL,此时自动生成)。
  • 如果插入时把这个字段设为 NULL,SQLite 会自动分配一个比当前最大 rowid 大 1 的整数。
  • 如果插入时显式指定了一个很大的值,那么后续自增会从这个值继续。

AUTOINCREMENT关键字则改变了分配规则:它记录了一个sqlite_sequence表,确保新分配的 id 永远比历史最大值大,即使最大值已经被删除。这在某些需要 id 严格递增不可复用的业务逻辑中有用,但会额外增加一次内部表写入操作。

所以,如果你的应用场景只是需要一个唯一标识,并不关心 id 是否复用,建议直接用INTEGER PRIMARY KEY,不要加AUTOINCREMENT,性能和存储都更优。

4.3 事务:批量写入的正确姿势

SQLite 对磁盘的写入操作是有开销的。如果你要插入 1 万条数据,逐条执行INSERT,每条都自动提交(autocommit),那磁盘 I/O 压力会非常大,速度可能慢到无法接受。

正确做法是用事务包裹批量操作:

BEGIN TRANSACTION; INSERT INTO sensor_data (device_id, temperature) VALUES ('DEV-001', 25.1); INSERT INTO sensor_data (device_id, temperature) VALUES ('DEV-001', 25.3); -- ... 更多插入 ... COMMIT;

这样做会把所有 INSERT 放在一个事务里,SQLite 先将数据写入页缓存,最后统一刷盘,性能提升非常明显。我在嵌入式设备上实测过,逐条插入 1 万条记录可能需要几十秒,而使用事务包裹后,基本是毫秒级完成。

事务还有一个重要特性:原子性。如果事务中途失败,执行ROLLBACK,之前的所有操作都会回滚,不会留下半截数据。这在实际项目中非常重要,比如设备在断网、断电的边缘时刻正在写一批数据,事务能保证这批数据要么全部写入,要么全部不写入。

有一点要记住:BEGIN TRANSACTION之后,一定要有对应的COMMIT或ROLLBACK。如果在事务开启状态下直接退出程序,SQLite 会自动回滚未提交的事务。

4.4 WAL 模式:嵌入式场景下的可靠性和并发平衡

SQLite 默认的日志模式是 DELETE 模式(也叫 rollback journal),每次写操作都创建一个回滚日志文件,写完再删掉,这个过程会频繁操作文件系统的元数据,在嵌入式平台(尤其是 SD 卡)上的表现不是很好。

更推荐的模式是 WAL(Write-Ahead Logging,预写日志)。开启方式:

PRAGMA journal_mode=WAL;

开启后,写操作会先追加到-wal文件中,而不是直接修改主数据库文件。带来的好处有两个:

  1. 读写并发更好:读操作不会阻塞写操作(虽然写操作之间仍然互斥),适合嵌入式设备中一边采集数据、一边后期查询的场景。
  2. 写入性能更稳定:顺序追加写入比随机修改主文件更高效,对 Flash 存储更友好。

开启 WAL 后,你会在数据库文件旁边看到两个额外的文件:mydb.db-wal和mydb.db-shm。这是正常现象,不要删除它们,否则可能造成数据不一致。

不过要注意:WAL 模式在跨平台共享数据库文件时有些注意事项,比如拷贝数据库文件时,要确保-wal文件也一起拷贝,或者先执行PRAGMA wal_checkpoint;把 WAL 内容合并回主库文件再拷贝。

5. C/C++ 调用 SQLite:最小可运行示例到常见坑

命令行的本事再大,最终还是要落到代码里。这一节是我的实际经验最多的部分。

5.1 编译环境的准备

在写代码之前,先确认开发环境里有sqlite3.h头文件和可链接的库。

Linux 下如果装了libsqlite3-dev,头文件一般在/usr/include/sqlite3.h,库文件在/usr/lib/x86_64-linux-gnu/libsqlite3.so。

Windows 下如果用 MinGW,下载 DLL 对应的导入库(.a文件)放到 MinGW 的 lib 目录;如果是从源码编译,把sqlite3.c和sqlite3.h放进项目即可。

5.2 最简单的 open 和 close

一个最基本的程序框架长这样:

#include <stdio.h> #include <sqlite3.h> int main() { sqlite3 *db = NULL; int rc = sqlite3_open("test.db", &db); if (rc != SQLITE_OK) { fprintf(stderr, "打开数据库失败: %s\n", sqlite3_errmsg(db)); return 1; } printf("数据库打开成功\n"); sqlite3_close(db); return 0; }

编译命令(Linux 下):

gcc main.c -lsqlite3 -o test

如果你用的是更完整的系统库路径,也可以显式指定:

gcc main.c /usr/lib/x86_64-linux-gnu/libsqlite3.so -o test

如果你是把sqlite3.c直接编进项目,那就是:

gcc main.c sqlite3.c -o test -lpthread -ldl

注意这里要加-lpthread -ldl,因为 SQLite 在某些配置下依赖 POSIX 线程和动态链接库接口。

5.3 sqlite3_exec 和 prepared statement 的区别

调用 SQLite 有两种主流方式:sqlite3_exec和预编译语句(prepared statement)。

方式一:sqlite3_exec

适合执行简单的、不带参数或参数固定的 SQL:

const char *sql = "CREATE TABLE IF NOT EXISTS test(id INTEGER PRIMARY KEY, name TEXT);"; char *errMsg = NULL; int rc = sqlite3_exec(db, sql, NULL, NULL, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "SQL 错误: %s\n", errMsg); sqlite3_free(errMsg); }

sqlite3_exec的回调参数sqlite3_callback一般用于 SELECT 查询时的结果处理:

int callback(void *data, int argc, char **argv, char **colName) { for (int i = 0; i < argc; i++) { printf("%s = %s\n", colName[i], argv[i] ? argv[i] : "NULL"); } return 0; } sqlite3_exec(db, "SELECT * FROM test", callback, NULL, &errMsg);

方式二:prepared statement

我更推荐这种方式,尤其是涉及用户输入或者循环插入的时候。原因有两个:一是安全(防止 SQL 注入),二是性能(可以复用编译好的语句):

sqlite3_stmt *stmt; const char *sql = "INSERT INTO test(name) VALUES (?1);"; sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); sqlite3_bind_text(stmt, 1, "张三", -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { fprintf(stderr, "执行失败: %s\n", sqlite3_errmsg(db)); } sqlite3_finalize(stmt);

这里有几个关键点:

  • ?1是占位符,sqlite3_bind_text的第二个参数1对应第一个占位符。
  • SQLITE_TRANSIENT告诉 SQLite,绑定的是临时内存,它需要自己复制一份数据。如果你明确知道字符串生命周期足够长,可以用SQLITE_STATIC,性能更好,但如果字符串在sqlite3_step之前就被释放了,就会出问题。
  • sqlite3_step执行完 INSERT 之后返回SQLITE_DONE;如果是 SELECT,第一次调用返回SQLITE_ROW,之后每次调用继续取下一行,直到返回SQLITE_DONE。

查询操作的标准写法:

const char *sql = "SELECT id, name FROM test WHERE id = ?1;"; sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); sqlite3_bind_int(stmt, 1, 1); int rc; while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { int id = sqlite3_column_int(stmt, 0); const unsigned char *name = sqlite3_column_text(stmt, 1); printf("id=%d, name=%s\n", id, name); } sqlite3_finalize(stmt);

注意索引从 0 开始取列,和绑定时从 1 开始是不同的约定。

5.4 常见坑:no column named unnamed

热词里出现的no column named unnamed这个错误,我推测是 SELECT 查询里用了SELECT *但是表结构有问题,或者执行了类似SELECT 1+1;这种没有列名的查询,SQLite 无法返回列名时的表现。

解决方案有两个:

  1. 在 SQL 中给表达式起别名:SELECT 1+1 AS result;
  2. 代码中不要依赖列名,直接用下标sqlite3_column_int(stmt, 0)获取值。

另外,如果你的查询语句写错,sqlite3_prepare_v2会返回SQLITE_ERROR,此时应该用sqlite3_errmsg打印具体错误。我建议在封装数据库操作时,一定要写一个统一的错误检查宏,比如:

#define CHECK_SQLITE_OK(rc, db, msg) \ if ((rc) != SQLITE_OK) { \ fprintf(stderr, "%s: %s\n", (msg), sqlite3_errmsg(db)); \ return (rc); \ }

这样每次调用后检查错误会非常省事。

5.5 多线程访问的注意事项

如果你的程序是多线程的,有几个规则要记住:

  • 同一个sqlite3*连接可以被多个线程使用,但 SQLite 默认编译模式下需要自己保证同一时刻只有一个线程在调用(串行化由你控制)。
  • 更好的方式是开启SQLITE_OPEN_FULLMUTEX或SQLITE_OPEN_NOMUTEX标志,前者让 SQLite 内部加锁保证线程安全,后者则不做任何锁保护。
  • 我自己在嵌入式项目中,比较推荐的方法是:每个线程使用独立的数据库连接,或者用一个全局的数据库操作锁。前者性能更好,后者实现简单。

6. 表设计、索引、备份与误操作恢复的实战心得

这一节不是纯理论,都是我在实际项目里遇到过的真实情况。

6.1 表设计:给嵌入式设备的建议

在嵌入式环境中,存储资源有限,表设计要比后端系统更克制。

一个常见错误是给每个字段都建立索引,这会增大数据库文件体积,同时拖慢写入速度。索引的本质是额外的 B 树结构,每建一个索引,写入时就要同步维护。

我的设计原则是:

  • 只为高频查询字段建索引。比如传感器表经常按device_id查,那就给device_id建索引;如果经常按sample_time排序,给sample_time建索引也有帮助。
  • 不要用 SELECT *。在 C/C++ 代码里,尽量只查你需要的字段,减少内存拷贝和数据量。
  • 合理设置 TEXT 字段长度。SQLite 没有 VARCHAR(n) 的强制长度限制,但你不能因此随意存储无限制的大文本。嵌入式环境下,一个字段塞几 KB 的数据,很容易让数据库文件膨胀。
  • 考虑数据归档。设备长期运行,数据量会持续增加。定期把旧数据导出到另一个文件,或者按时间分表(比如sensor_data_202501),是一个常用的手段。

6.2 关于索引的一个实测案例

我之前在做一个环境监测记录仪,表里有几十万条sample_time记录。最开始没建索引,按时间范围查询一次要一两秒,对于用户来说体验很差。后来执行:

CREATE INDEX idx_sample_time ON sensor_data(sample_time);

再查同一范围的记录,耗时降到了几十毫秒,提升了两个数量级。

这就是索引的价值,但要记住它是一把双刃剑——写入变慢、文件变大,但查询飞快。

6.3 备份:VACUUM INTO 和文件拷贝

嵌入式设备很少有机会停机专门做备份,但数据安全不能马虎。SQLite 从 3.27 版本开始支持在线备份命令:

VACUUM INTO 'backup.db';

这个命令会生成一个当前数据库的一致性快照到指定文件,过程中不需要停掉写入。我觉得这在嵌入式设备上非常实用,比如每天凌晨定时执行一次,把当天的数据备份到 SD 卡。

另外还有一种更原始的备份方式:直接拷贝.db文件。但要注意,如果开启了 WAL 模式,直接拷贝主文件可能会漏掉-wal文件里的最新数据。所以在拷贝前,先执行:

PRAGMA wal_checkpoint(FULL);

确保 WAL 日志中的内容全部合并到主数据库文件里,然后再拷贝。

6.4 误操作恢复:一个个人经验

前面提到UPDATE和DELETE不带WHERE会全表更新/删除,这是新手最容易犯的致命错误。如果真发生了,有几个补救措施:

第一,马上关闭所有连接,用只读方式打开数据库文件。

如果数据库还没有被其他写入操作覆盖,也许还有机会恢复。SQLite 删除数据并不会立即擦除文件内容,而是标记为空闲页。但问题在于,后续任何写入都可能覆盖这些空闲页。

第二,如果是刚执行完误操作,立刻用文件恢复工具处理。

其实最可靠的方式还是从备份恢复。这也是我为什么在关键项目中强制要求开启自动备份的原因。没有备份,任何"数据恢复技巧"都只是死马当活马医。

我在实际项目中的做法是:每天都定时用VACUUM INTO备份一份到 SD 卡,同时保留最近 7 天的备份文件。这样无论设备出什么问题,最高损失不过是一天的数据。

6.5 一个小技巧:用 sqlite3 命令行做数据导出和导入

有时候需要在 PC 上分析设备采集的数据,可以这样导出 CSV:

.headers on .mode csv .output sensor_data.csv SELECT * FROM sensor_data; .output stdout

然后在 Python 或者 Excel 里分析,非常方便。反过来,也可以把 PC 上的 CSV 导入到嵌入式数据库:

.mode csv .import input.csv sensor_data

这在测试阶段灌入模拟数据时特别高效。但是要注意csv文件的格式,如果字段里有逗号或换行,需要正确引用,否则导入会错位。

7. 一个完整的最小 C/C++ 示例:串起来跑一遍

说了这么多,最后给一个完整的示例程序,把前面的知识点串起来。这个程序做三件事:创建表、插入两条记录、查询并打印。

#include <stdio.h> #include <sqlite3.h> static int callback(void *data, int argc, char **argv, char **colName) { for (int i = 0; i < argc; i++) { printf("%s = %s\n", colName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0; } int main(void) { sqlite3 *db = NULL; char *errMsg = NULL; int rc = sqlite3_open("demo.db", &db); if (rc != SQLITE_OK) { fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db)); return 1; } const char *createSql = "CREATE TABLE IF NOT EXISTS user(" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL," "age INTEGER" ");"; rc = sqlite3_exec(db, createSql, NULL, NULL, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "建表失败: %s\n", errMsg); sqlite3_free(errMsg); sqlite3_close(db); return 1; } sqlite3_stmt *stmt; const char *insertSql = "INSERT INTO user(name, age) VALUES (?1, ?2);"; sqlite3_prepare_v2(db, insertSql, -1, &stmt, NULL); sqlite3_bind_text(stmt, 1, "Alice", -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, 25); sqlite3_step(stmt); sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, "Bob", -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, 30); sqlite3_step(stmt); sqlite3_finalize(stmt); const char *querySql = "SELECT * FROM user;"; rc = sqlite3_exec(db, querySql, callback, NULL, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "查询失败: %s\n", errMsg); sqlite3_free(errMsg); } sqlite3_close(db); return 0; }

编译运行(Linux):

gcc demo.c -lsqlite3 -o demo ./demo

输出应该是:

id = 1 name = Alice age = 25 id = 2 name = Bob age = 30

这个示例验证了所有基础流程。你会发现,SQLite 的 C API 其实不多,核心就是open、exec/prepare、step、close这几组函数,记住它们的返回值和错误检查方式,基本就入门了。

再分享一个实际开发中的细节:sqlite3_exec中的回调函数是在 SQLite 内部执行的,如果在回调里处理复杂逻辑,可能会阻塞数据库操作。所以我在做批量查询时,通常会先把结果收集到一个结构体数组里,等sqlite3_exec返回后再统一处理。这样逻辑清晰,也不容易犯错。

另外,如果你发现调试时所有 SQL 都正确,但数据就是不对,可以打开 SQLite 的调试日志。在命令行模式执行.log stderr,或者在 C 代码里设置sqlite3_config(SQLITE_CONFIG_LOG, ...),把内部日志输出到终端,很多隐藏问题会直接暴露出来。我自己有一次在嵌入式设备上发现数据库文件莫名增大,就是靠这个方式定位到某条 SQL 在循环里重复创建了临时表。

SQLite 看起来简单,但用好了能解决嵌入式场景 90% 的数据存储需求。从命令行到 C/C++ 接口,核心的 API 也就那么十几个,真正难的是理解它的存储模型和线程模型。这篇先讲到基础篇,后面如果有机会,我再写一篇进阶,专门说说sqlite3_stmt的底层执行流程、SQLite 的 Pager 层工作原理,以及如何在资源受限的 MCU 上裁剪 SQLite 编译选项。先把基础打牢,命令行命令多敲几遍,C 接口的示例程序自己编译运行一遍,比看十篇教程都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 15:01:50

Smartbits600 测试实战:从开箱到 RFC 2544 吞吐量测试全流程

简介&#xff1a;Smartbits600测试使用指导书是一份面向网络测试初学者与运维人员的实操型文档&#xff0c;围绕NetCom System出品的便携式网络性能测试仪展开&#xff0c;帮助读者从零掌握设备操作与常见测试流程。资源包内共1个doc文件&#xff0c;约977KB&#xff0c;内容按…

作者头像 李华
网站建设 2026/9/30 14:45:18

AST 安全求值

AST 安全求值指的是&#xff1a;把表达式/代码先解析成抽象语法树&#xff08;AST&#xff09;&#xff0c;然后不直接 eval / compile 执行&#xff0c;而是自己遍历 AST&#xff0c;只允许白名单内的节点&#xff0c;并按预定语义解释执行。核心目标是避免任意代码执行、沙箱…

作者头像 李华
网站建设 2026/9/30 14:39:36

【数据集】分省及地级市城投债信用利差数据集(2011-2026年)

数据简介&#xff1a;城投债分省份、地级市信用利差跟踪包括公募债、私募债数据库据库&#xff0c;信用利差个券估值-同期限国开债收益率。剔除剩余期限半年以内或五年以上的个券&#xff0c;估值采用不行权估值&#xff0c;匹配同期限国开债采用插值法。在债券市场中&#xff…

作者头像 李华
网站建设 2026/9/30 14:39:08

提示微调(Prompt Tuning/Prefix Tuning/P-Tuning)技术总结

提示微调属于参数高效微调 PEFT&#xff0c;核心思路&#xff1a;冻结大模型全部主干权重&#xff0c;只训练少量可学习的软提示向量&#xff0c;相比全参数微调显存开销、训练成本大幅下降&#xff0c;是现在大模型落地最常用的方案之一。 1、 核心技术原理提示微调的关键是将…

作者头像 李华
网站建设 2026/9/30 14:35:00

零基础嵌入式学习第三周

嵌入式基础作业三&#xff1a;STM32F103C8T6 三种方式实现流水灯 课程作业范围&#xff1a;实验1&#xff08;寄存器方式&#xff09;、实验2&#xff08;标准外设库方式及 Keil 逻辑分析仪&#xff09;、实验3&#xff08;HAL 库 按键外部中断暂停/恢复&#xff09;。 目标器…

作者头像 李华