绑定是把值放进预处理语句的方式
预处理语句是带有空位的 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 占位符的语句很难阅读,也很容易绑错。每条语句只选一种写法,值超过两三个时通常用命名参数。