从MySQL 4.1版本开始,就支持服务器端的绑定变量(prepared statement),这大大提高了客户端和服务器端数据传输的效率。你若使用一个支持新协议的客户端,如MySQL C API,就可以使用绑定变量功能了。另外,Java和.NET的也都可以使用各自的客户端Connector/J和Connector/NET来使用绑定变量。最后,还有一个SQL接口用于支持绑定变量,后面我们将讨论这个(这里容易引起困扰)。
当创建一个绑定变量SQL时,客户端向服务器发送了一个SQL语句的原型。服务器端收到这个SQL语句框架后,解析并存储这个SQL语句的部分执行计划,返回给客户端一个SQL语句处理句柄。以后每次执行这类查询,客户端都指定使用这个句柄。
绑定变量的SQL,使用问号标记可以接收参数的位置,当真正需要执行具体査询的时候,则使用具体值代替这些问号。例如,下面是一个绑定变量的SQL语句:
INSERT INTO tbl(col1, col2, col3) VALUES (?, ?, ?);
可以通过向服务器端发送各个问号的取值和这个SQL的句柄来执行一个具体的査询。反复使用这样的方式执行具体的査询,这正是绑定变量的优势所在。具体如何发送取值参数和SQL句柄,则和各个客户端的编程语言有关。使用Java和.NET的MySQL连接器就是一种办法。很多使用MySQL C语言链接库的客户端可以提供类似的接口,需要根据使用的编程语言的文档来了解如何使用绑定变量。
因为如下的原因,MySQL在使用绑定变量的时候可以更高效地执行大量的重复语句:
绑定变量相对也更安全。无须在应用程序中处理转义,一则更简单了,二则也大大减少 了SQL注入和攻击的风险。(任何时候都不要信任用户输入,即使是使用绑定变量的时候。)
可以只在使用绑定变量的时候才使用二进制传输协议。如果使用普通的mysql_query()接口则不会使用二进制传输协议。还有一些客户端让你使用绑定变量,先发送带参数的绑定SQL,然后发送变量值,但是实际上,这些客户端只是模拟了绑定变量的接口,最后还是会直接用具体值代替参数后,再使用mysql_query()发送整个査询语句。
对使用绑定变量的SQL, MySQL能够缓存其部分执行计划,如果某些执行计划需要根据传入的参数来计算时,MySQL就无法缓存这部分的执行计划。根据优化器什么时候工作,可以将优化分为三类。Mysql5.6版本之前,下面的三点是适用的。
在准备阶段
服务器解析SQL语句,移除不可能的条件,并且重写子査询。
在第一次执行的时候
如果可能的话,服务器先简化嵌套循环的关联,并将外关联转化成内关联。
在每次SQL语句执行时
服务器做如下事情:
理论上,有些优化只需要做一次,但实际上,上面的操作还是都会被执行。
在4.1和更新的版本中,MySQL支持了SQL接口的绑定变量。不使用二进制传输协议也可以直接以SQL的方式使用绑定变量。下面案例展示了如何使用SQL接口的绑定变量:
mysql> SET @sql := 'SELECT actor_id, first_name, last_name -> FROM sakila.actor WHERE first_name = ?'; mysql> PREPARE stmt_fetch_actor FROM @sql; mysql> SET @actor_name := 'Penelope'; mysql> EXECUTE stmt_fetch_actor USING @actor_name; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 1 | PENELOPE | GUINESS | | 54 | PENELOPE | PINKETT | | 104 | PENELOPE | CRONYN | | 120 | PENELOPE | MONROE | +----------+------------+-----------+ mysql> DEALLOCATE PREPARE stmt_fetch_actor;
当服务器收到这些SQL语句后,先会像一般客户端的链接库一样将其翻译成对应的操作。这意味着你无须使用二进制协议也可以使用绑定变量。
正如你看到的,比起直接编写的SQL语句,这里的语法看起来有一些怪怪的。那么,这种写法实现的绑定变量到底有什么优势呢?
最主要的用途就是在存储过程中使用。在MySQL5.0版本中,就可以在存储过程中使用绑定变量,其语法和前面介绍的SQL接口的绑定变量类似。这意味,可以在存储过程中构建并执行“动态”的SQL语句,这里的“动态”是指可以通过灵活地拼接字符串等参数构建SQL语句。例如,下面的示例存储过程中可以针对某个数据库执行OPTIMIZE TABLE的操作:
DROP PROCEDURE IF EXISTS optimize_tables; DELIMITER // CREATE PROCEDURE optimize_tables(db_name VARCHAR(64)) BEGIN DECLARE t VARCHAR(64); DECLARE done INT DEFAULT 0; DECLARE c CURSOR FOR SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = db_name AND TABLE_TYPE = 'BASE TABLE'; DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1; OPEN c; tables_loop: LOOP FETCH c INTO t; IF done THEN LEAVE tables_loop; END IF; SET @stmt_text := CONCAT("OPTIMIZE TABLE ", db_name, ".", t); PREPARE stmt FROM @stmt_text; EXECUTE stmt; DEALLOCATE PREPARE stmt; END LOOP; CLOSE c; END// DELIMITER ;
可以这样调用这个存储过程:
mysql> CALL optimize_tables('sakila');
另一种实现存储过程中循环的办法是:
REPEAT FETCH c INTO t; IF NOT done THEN SET @stmt_text := CONCAT("OPTIMIZE TABLE ", db_name, ".", t); PREPARE stmt FROM @stmt_text; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; UNTIL done END REPEAT;
这两种循环结构最重要的区别在于:REPEAT会为每个循环检査两次循环条件。在这个例子中,因为循环条件检査的是一个整数判断,并不会有什么性能问题,如果循环的判断条件非常复杂的话,则需要注意这两者的区别。
像这样使用SQL接口的绑定变量拼接表名和库名是很常见的,这样的好处是无须使用任何参数就能完成SQL语句。而库名和表名都是关键字,在二进制协议的绑定变量中是不能将这两部分参数化的。另一个经常需要动态设置的就是LIMIT子句,因为二进制协议中也无法将这个值参数化。
另外,编写存储过程时,SQL接口的绑定变量通常可以很大程度地帮助我们调试绑定变量,如果不是在存储过程中,SQL接口的绑定变量就不是那么有用了。因为SQL接口的绑定变量,它既没有使用二进制传输协议,也没有能够节省带宽,相反还总是需要增加至少一次额外网络传输才能完成一次査询。所有只有在某些特殊的场景下SQL接口的绑定变量才有用,比如当SQL语句非常非常长,并且需要多次执行的时候。
关于绑定变量的一些限制和注意事项如下:
不过使用绑定变量最大的障碍可能是:它是如何实现以及原理是怎样的,这两点很容易让人困惑。有时,很难解释如下三种绑定变量类型之间的区别是什么:
客户端模拟的绑定变量
客户端的驱动程序接收一个带参数的SQL,再将指定的值带入其中,最后将完整的査询发送到服务器端。
服务器端的绑定变量
客户端使用特殊的二进制协议将带参数的字符串发送到服务器端,然后使用二进制协议将具体的参数值发送给服务器端并执行。
SQL接口的绑定变量
客户端先发送一个带参数的字符串到服务器端,这类似于使用PREPARE的SQL语句,然后发送设置参数的SQL,最后使用EXECUTE来执行SQL。所有这些都使用普通的文本传输协议。