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开始
?NNNVALUES (?1, ?2, ?3)显式指定位置编号
:AAAVALUES (: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 知识体系总览

下面这张图把本章的核心逻辑串起来了:

数据插入操作知识体系 INSERT INTO 基础 • 显式列名 vs 省略列名 • 单条插入语法 • 避免字符串拼接 参数绑定 • 占位符:? / ?NNN / :AAA • sqlite3_bind_* 系列 • 防注入 + 性能提升 批量插入优化 • 显式事务 BEGIN/COMMIT • PRAGMA synchronous=OFF • 预编译语句复用 核心原则 1. 永远使用参数绑定,不要拼接 SQL 字符串 2. 批量插入务必用显式事务包裹 3. 根据业务场景权衡同步模式与数据安全性

5.5 避坑指南

我曾经踩过的坑:
  • 忘记 reset:循环里只 bind 不 reset,第二次 step 会返回 SQLITE_MISUSE。每次 step 之后必须 reset。
  • bind 索引从 1 开始:不是 0!我刚开始写 C 代码时习惯性从 0 开始,结果绑了半天数据全是 NULL。
  • 事务太大:一个事务插 100 万条,内存占用会飙升。建议每 1 万条提交一次,平衡内存和速度。
我的个人习惯:写批量插入时,我会封装一个辅助函数,内部自动管理事务边界。比如每 5000 条提交一次,既保证速度,又避免事务过大。代码复用率高,也不容易出错。

好了,关于数据插入,核心就是这三板斧:参数绑定保安全、显式事务提速度、预编译复用省资源。你可以在自己的项目里试试,把一条一条插改成批量事务插入,感受一下从「等得想睡觉」到「瞬间完成」的差距。


公众号:蓝海资料掘金营,微信deep3321