5、数据插入操作:INSERT INTO语句、参数绑定、批量插入优化
插入数据,说白了就是把新记录塞进数据库里。这事儿看起来简单,但真要在项目里用好,里面的门道可不少。我刚开始用 SQLite 的时候,觉得 INSERT 不就是拼个字符串嘛,后来被坑了几次才明白——嗯,这里面的学问,值得好好聊聊。
5.1 最基本的 INSERT INTO
先看最直白的写法。假设我们有个用户表:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER,
email TEXT
);
插入一条记录,SQL 语句长这样:
INSERT INTO users (name, age, email) VALUES ('张三', 28, 'zhangsan@example.com');
如果你要插入所有字段,可以省略列名:
INSERT INTO users VALUES (NULL, '李四', 32, 'lisi@example.com');
但我不建议这么写。为什么?因为表结构一旦变了,你的 SQL 就废了。我在项目中吃过这个亏——加了一个字段,结果所有省略列名的 INSERT 全部报错。所以,永远显式写出列名,这是好习惯。
5.2 参数绑定:别再用字符串拼接了
很多新手喜欢这样写:
char sql[256];
sprintf(sql, "INSERT INTO users VALUES(NULL, '%s', %d, '%s')", name, age, email);
看着挺灵活对吧?但这里藏着两个大坑:
- SQL 注入风险:如果 name 里包含单引号,比如 "O'Brien",你的 SQL 就炸了。更坏的情况是有人故意传恶意数据。
- 性能差:每次拼接字符串,SQLite 都要重新解析 SQL,重复劳动。
正确的做法是用参数绑定。SQLite 提供了 sqlite3_bind_* 系列函数,说白了就是先把 SQL 准备好,留出占位符,再把数据填进去。
5.2.1 占位符的写法
SQLite 支持三种占位符:
| 占位符 | 示例 | 说明 |
|---|---|---|
? | VALUES (?, ?, ?) | 按位置绑定,从1开始 |
?NNN | VALUES (?1, ?2, ?3) | 显式指定位置编号 |
:AAA | VALUES (:name, :age, :email) | 命名参数,可读性好 |
我个人习惯用命名参数,代码可读性高,维护起来也省心。
5.2.2 完整示例
sqlite3 *db;
sqlite3_stmt *stmt;
// 1. 准备 SQL,用命名参数
const char *sql = "INSERT INTO users (name, age, email) VALUES (:name, :age, :email);";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
// 2. 绑定参数
sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":name"), "张三", -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, sqlite3_bind_parameter_index(stmt, ":age"), 28);
sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":email"), "zhangsan@example.com", -1, SQLITE_TRANSIENT);
// 3. 执行
int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
printf("插入成功!\n");
}
// 4. 重置,准备下一次使用
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
SQLITE_TRANSIENT 告诉 SQLite 在内部拷贝字符串。如果你的字符串在绑定后不会被释放,也可以用 SQLITE_STATIC 避免拷贝,但一定要确保指针有效。
5.3 批量插入优化:从秒级到毫秒级
假设你要插入 10 万条记录。一条一条插,你会等得怀疑人生。我最早做数据迁移时就这么干过,插了 5 万条花了快两分钟——太慢了。
为什么会这么慢?因为每执行一次 INSERT,SQLite 默认会:
- 开启一个事务(隐式的)
- 写入数据
- 提交事务
- 写一次日志
10 万次 INSERT,就是 10 万次事务提交、10 万次磁盘写入。磁盘 I/O 是最大的瓶颈。
5.3.1 显式事务:最有效的优化
把 10 万次 INSERT 包在一个事务里:
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
for (int i = 0; i < 100000; i++) {
// 绑定参数并执行
sqlite3_step(stmt);
sqlite3_reset(stmt);
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
就这么简单的一改,10 万条插入从 90 秒降到了 0.8 秒。你想想看,差了 100 多倍。
5.3.2 关闭同步模式
如果你能接受极端情况下丢失少量数据(比如日志、缓存场景),可以关闭同步写入:
sqlite3_exec(db, "PRAGMA synchronous = OFF;", NULL, NULL, NULL);
默认是 FULL,每次写入都等磁盘刷完。改成 OFF 后,写入速度还能再翻倍。但注意——断电或崩溃可能丢数据。我曾经在一个日志采集模块里用过这个技巧,写入速度从每秒 2 万条飙到了 8 万条,但后来产品经理说日志不能丢,我又改回去了。
5.3.3 使用预编译语句复用
上面示例里,sqlite3_prepare_v2 只调用一次,然后循环里只做 bind → step → reset。这比每次重新 prepare 快得多。prepare 本身有开销,能省就省。
5.4 知识体系总览
下面这张图把本章的核心逻辑串起来了:
5.5 避坑指南
- 忘记 reset:循环里只 bind 不 reset,第二次 step 会返回 SQLITE_MISUSE。每次 step 之后必须 reset。
- bind 索引从 1 开始:不是 0!我刚开始写 C 代码时习惯性从 0 开始,结果绑了半天数据全是 NULL。
- 事务太大:一个事务插 100 万条,内存占用会飙升。建议每 1 万条提交一次,平衡内存和速度。
好了,关于数据插入,核心就是这三板斧:参数绑定保安全、显式事务提速度、预编译复用省资源。你可以在自己的项目里试试,把一条一条插改成批量事务插入,感受一下从「等得想睡觉」到「瞬间完成」的差距。