核心内核:PHP预处理语句(Prepared Statement),是数据库层面把SQL模板和参数数据拆分开的执行方案。承接前面的符号表模型类比:SQL模板是固定的骨架符号,用户输入的变量是动态值;不再直接拼接字符串生成完整SQL,杜绝把用户输入当成SQL指令解析,根治SQL注入。
说明:下文以PHP PDO为主,mysqli也支持预处理,原理一致。
一、本源基础术语
- 预处理语句 Prepared Statement
预先向数据库发送一段不带真实参数的SQL模板,数据库编译、解析、生成执行计划并缓存;之后单独传入参数执行。分成两步:编译 + 执行。 - SQL模板(模板SQL)
用占位符代替动态变量的SQL语句,语句结构固定。例:SELECT * FROM user WHERE id = ?,?就是占位符。模板本身不会被用户输入篡改。 - 占位符 Placeholder
模板中预留位置,用来后续填充数据。两种:问号占位?(位置占位)、命名占位:id(命名占位,PDO推荐)。占位不能用来替换表名、字段名,只能替换值。 - SQL注入 SQL Injection
高危漏洞。直接字符串拼接SQL时,用户输入被数据库当成SQL语法执行。例如输入1 or 1=1,篡改查询逻辑。预处理的核心目标就是隔离「语句结构」和「用户数据」,让用户输入永远只被当成纯数据,不会解析成SQL命令。 - PDO PHP Data Objects
PHP的数据库抽象层,统一不同数据库的操作接口,原生支持预处理语句,是生产环境首选。 - mysqli预处理
mysqli扩展也支持prepare预处理,分面向对象/面向过程写法;但PDO通用性更强。
二、预处理完整生命周期术语
- prepare(准备阶段)
PHP把SQL模板发送给数据库服务端。数据库做语法解析、语义校验、生成查询执行计划,保存这个编译好的语句对象,此时参数还没有传入。
重点:只编译模板,不执行查询。
- bindParam 绑定参数
把PHP变量绑定到占位符。变量是引用绑定,执行时读取变量当前的值。 - bindValue 绑定值
把固定值绑定到占位符,绑定瞬间就取值,不是引用。bindParam绑变量,bindValue绑常量/即时值。 - execute() 执行阶段
向数据库发送真实参数,数据库使用已经编译好的执行计划,代入参数运行SQL,返回结果。可以多次调用execute,复用同一个编译模板。 - 结果集 ResultSet
execute执行成功后,数据库返回的数据集合,通过fetch/fetchAll读取。 - 语句对象 PDOStatement
prepare返回的对象,承载编译后的预处理模板、绑定的参数、执行后的结果。类比:编译后的程序实例。 - 释放游标 closeCursor()
一次预处理执行完成,释放服务端结果游标,方便下一次execute复用。 - 释放语句
语句使用完毕,销毁PDOStatement对象,释放数据库服务端缓存的执行计划。
三、性能相关术语
- 执行计划缓存
数据库收到模板后,生成执行计划并缓存。同一条SQL模板多次执行,只编译一次。循环批量插入、多次查询场景,减少重复编译开销,提升性能。
误区:单次只执行一次SQL,预处理几乎没有性能收益,它最大价值是防注入。
- 网络包拆分
SQL模板、参数分两次网络传输。模板先发,后续每次execute只传输参数数据。 - 批量复用
同一个prepare模板,循环多次execute,传入不同参数。适合批量insert、循环查询。
四、安全相关术语
- 数据与语法分离
预处理最核心底层原理:数据库区分两部分,SQL语法结构(固定模板)、参数数据(外部输入)。参数永远作为字面量,不会解析为SQL关键字、运算符。 - 转义逃逸
普通字符串拼接方式,需要手动对变量做转义,把特殊字符' " \等转义。预处理不需要手动addslashes,数据库底层自动处理。 - 模拟预处理 Emulate Prepare
PDO的一项开关PDO::ATTR_EMULATE_PREPARES。开启时不是真正交给数据库服务端预处理,是PHP驱动在本地模拟参数替换,再拼接SQL发给数据库。
重大关键点:模拟预处理依然存在注入风险,生产环境必须关闭模拟预处理,使用数据库原生预处理。
- 白名单校验
占位符不能替换表名、字段名。如果需要动态选择表/字段,不能用占位符,只能做白名单校验,只允许预设合法名称。
五、易混淆术语
- 字符串拼接SQL
直接"SELECT * FROM user WHERE id = $id",把变量拼进SQL文本。这是高危写法,和预处理完全对立。 - quote 手动转义
$pdo->quote(),手动对变量转义加引号。可以简单防注入,但容易遗漏,不适合复杂场景,优先用预处理。 - 命名占位符 :name
PDO特有,可读性更好,WHERE name=:name AND age=:age。同一个模板,命名占位符不能重复同名。 - 问号占位 ?
位置顺序绑定,参数顺序必须和SQL里面?顺序严格一一对应。
六、典型误区术语
- 预处理=性能一定更快
误区。单次执行SQL,预处理多一次网络往返(prepare + execute两次请求),性能反而略低。优势体现在多次复用模板,以及安全防护。安全才是首要目标。 - 写了预处理就绝对安全
误区。如果开启emulate_prepares模拟预处理,或者用占位符去替换表名、字段名,依旧会产生注入漏洞。 - bindParam和bindValue随便互换
误区。bindParam是引用绑定,execute才取值;bindValue在绑定时直接取值。循环中用bindParam容易踩变量引用的坑。 - 预处理可以防止XSS
误区。SQL预处理只管SQL注入。XSS是前端输出问题,二者无关,需要htmlspecialchars处理。 - mysqli预处理和PDO预处理完全一样
底层原理一致,但API不同,PDO支持命名占位符,mysqli仅支持问号占位。
庖丁解牛一句话总结
PHP预处理语句,核心是模板先行编译、参数后续传入,把SQL语法结构和外部输入数据隔离;数据库不会将参数解析为SQL指令,从根源抵御SQL注入;它有真实服务端预处理和PHP模拟预处理两种模式,模拟预处理不安全;占位符仅能替代字段值,不能替换表名字段;多次复用模板才有性能收益,单次查询收益主要是安全层面。