Menu

SQLite 参数绑定:?、:name 与安全传值

SQLite 中的参数绑定如何工作:位置占位符、命名参数,以及从应用中安全传值的规则。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

绑定是把值放进预处理语句的方式

预处理语句是带有空位的 SQL。绑定就是用值填充这些空位:通过驱动的 API,安全地一个一个填进去,而不是把字符串拼在一起。

形式总是一样的:先写带占位符的 SQL,再单独把值交过去。

在命令行工具中其实没法演示绑定(shell 背后没有应用代码),但上面的 SQL 正是你的应用发送的内容。? 就是占位符。你的驱动(Python 中的 sqlite3、Node 中的 better-sqlite3、Rust 中的 rusqlite)会通过单独的 bind 调用把它们填上。

可以这样理解:SQL 是菜谱,绑定的值是食材。两者永远不会混在一起。

位置占位符:?

最简单的占位符是 ?。每一个 ? 按顺序对应你绑定的下一个值。

INSERT INTO users (name, email) VALUES (?, ?);

在 Python 中是这样的:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    ("Rosa", "rosa@example.com"),
)

第一个 ? 得到 "Rosa",第二个得到 "rosa@example.com"。传入的值太少或太多,驱动都会在语句运行之前报错。

你也可以用 ?1、?2、?3 给它们明确编号,当同一个值出现不止一次时,这很有用:

SELECT ?1 AS greeting, ?1 AS still_the_same;

?1 复用第一个绑定的值。如果不编号,你就得把同一个值绑定两次。

命名占位符::name

一条语句的空位一旦超过两三个,按位置绑定就变成了猜谜游戏。命名参数解决了这个问题:

INSERT INTO users (name, email)
VALUES (:name, :email);

在 Python 中:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (:name, :email)",
    {"name": "Boris", "email": "boris@example.com"},
)

字典中键的顺序无关紧要,只有名称才重要。SQLite 也接受 @name 和 $name 作为替代前缀,它们的行为完全一样。:name 是目前最常用的写法。

当你有一条涉及五列的 UPDATE,或者一条在 WHERE 和 RETURNING 中使用同一个值的查询时,命名参数的好处立刻就显现出来了。

绑定 NULL

插入 NULL 的正确方式,是通过绑定 API 传入你所用语言的空值。驱动会负责转换:

INSERT INTO users (name, email) VALUES (?, ?);
-- Bind: ("Cyrus", None)   in Python
-- Bind: ["Cyrus", null]   in Node

SELECT id, name, email FROM users;

None、null、nil,不管你的语言叫它什么,驱动都会把它变成真正的 SQL NULL。不要绑定字符串 "NULL",那样存进去的是四个字符的文本“NULL”。也不要把 NULL 这个词拼接进 SQL 文本,那样就完全违背了绑定的初衷。

数字、blob、日期也遵循同样的规则:传入原生的值,让驱动去绑定。

用不同的值复用同一条语句

绑定和预处理语句天然搭配。准备一次,然后多次绑定并执行。解析器只工作一次,数据库对每一组绑定的值都复用编译好的执行计划。

INSERT INTO users (name, email) VALUES (?, ?);
-- Bind ("Ada",   "ada@example.com")    -> execute
-- Bind ("Boris", "boris@example.com")  -> execute
-- Bind ("Cyrus", NULL)                 -> execute

SELECT id, name, email FROM users ORDER BY id;

大多数驱动把这个过程封装成 executemany(Python)或一个 .run() 循环(Node)。无论哪种方式,你省下的都是解析的开销:单条语句的开销很小,但当你插入成千上万行时,就很可观了。

不要在一条语句中混用写法

从技术上讲,SQLite 允许在同一条语句中同时使用位置占位符和命名占位符。请克制住这种冲动。

-- Legal but a footgun:
INSERT INTO users (name, email) VALUES (?, :email);

读代码的人得在脑子里同时追踪两套绑定方式,而且大多数驱动并不能很好地支持这种混合写法。每条语句只选一种写法:一两个值用 ?,其他情况都用 :name。

常见陷阱:绑定不是字符串格式化

绑定的全部意义在于,值不会经过 SQL 解析。比较下面这两行 Python 代码:

# Wrong - string formatting:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# Right - parameter binding:
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

第一行通过拼接构造 SQL。如果 name 是 "'; DROP TABLE users; --",数据库就会照单全收,解析并运行被注入的语句。第二行通过不同的通道发送 SQL 和值:不管值里包含什么字符,它都只会被当作字符串绑定,仅此而已。这就是为什么每份指南都让你使用绑定:这跟代码风格无关,关乎的是解析器看到了什么。

下一页我们会深入讲解注入这一面。

另一个陷阱:标识符不能绑定

占位符只适用于值:字符串、数字、blob、NULL。它们不能用于表名、列名或 SQL 关键字:

-- This does NOT do what you want:
SELECT * FROM ? WHERE id = ?;
-- The first ? binds as a string literal, not a table name.

如果你确实需要动态的表名或列名(这在应用代码中很少见),请用白名单验证它,再自己把它拼接进 SQL,并且永远不要直接使用用户输入。其他所有情况,都用绑定。

完整示例

把这些内容组合起来:一张小小的 users 表,完全通过绑定来写入和读取:

在真实的代码中,INSERT 和 SELECT 都会使用占位符。命令行工具只是没有应用来执行绑定,所以这里用字面量代替绑定产生的结果。

下一步:防止 SQL 注入

参数绑定是一种机制。它为什么能阻止 SQL 注入,以及在少数几种情况下为什么光靠绑定还不够,就是下一页要讲的内容。

常见问题

SQLite 中的参数绑定是什么?

参数绑定是一种把值和 SQL 文本分开、单独传给预处理语句的方式。你在 SQL 中写一个占位符,比如 ? 或 :name,然后通过驱动的绑定 API 传入实际的值。SQLite 把绑定的值纯粹当作数据处理,它们永远不会被当作 SQL 解析。

SQLite 中 ? 和 :name 有什么区别?

? 是位置占位符,值按它们出现的顺序绑定。:name(以及 @name、$name)是命名占位符,按名称而不是位置绑定。当值超过两三个时,命名参数更容易阅读,也更方便调整顺序。

如何在 SQLite 中绑定 NULL 值?

通过绑定 API 传入你所用语言的空值(null/None/nil),驱动会自动把它转换成 SQL 的 NULL。永远不要写字符串 'NULL',也不要把 NULL 这个词拼接进 SQL 文本。绑定的全部意义就在于让值远离 SQL 解析器。

可以在一条语句中混用位置参数和命名参数吗?

SQLite 允许,但不要这样做。同时包含 ? 和 :name 占位符的语句很难阅读,也很容易绑错。每条语句只选一种写法,值超过两三个时通常用命名参数。

Coddy 编程语言插图

和 Coddy 一起学编程

开始