0

我已将我的问题简化为这个简单的 SP。列名最后被缓存在 SELECT * 中。我不知道为什么或如何阻止它。我尝试添加 SQL_NO_CACHE 但这没有什么区别。

DROP TABLE IF EXISTS foo;
CREATE TABLE foo(
col1 int,
col2 int);
INSERT INTO foo VALUES(1,2),(3,4),(5,6);
DROP PROCEDURE IF EXISTS mysp;
DELIMITER ;;
CREATE DEFINER=root@localhost PROCEDURE mysp(c INT)
BEGIN
   DROP TABLE IF EXISTS mydata;

   SET @mycol='col1';

   IF c > 0 THEN SET @mycol:='col2';
   END IF;

   SET @s=CONCAT('CREATE TEMPORARY TABLE mydata AS SELECT ', @mycol, ' FROM foo');
   PREPARE stmt FROM @s;
   EXECUTE stmt;
   DEALLOCATE PREPARE stmt;

-- The following select call fails on 2nd and subsequent executions of the SP
   SELECT SQL_NO_CACHE * FROM mydata;
   SELECT "Please see new temp table mydata" as Result;
END ;;
DELIMITER ;

版本

mysql> SELECT VERSION();
+------------+
| VERSION()  |
+------------+
| 5.5.15-log |
+------------+
1 row in set (0.00 sec)

第一次运行按预期工作正常

mysql> CALL mysp(0);
+------+
| col1 |
+------+
|    1 |
|    3 |
|    5 |
+------+
3 rows in set (0.17 sec)

+----------------------------------+
| Result                           |
+----------------------------------+
| Please see new temp table mydata |
+----------------------------------+
1 row in set (0.17 sec)

Query OK, 0 rows affected (0.17 sec)

现在,如果我尝试使用另一列再次运行它

mysql> CALL mysp(1);
ERROR 1054 (42S22): Unknown column 'qlgqp1.mydata.col1' in 'field list'
mysql> SELECT @mycol;
+--------+
| @mycol |
+--------+
| col2   |
+--------+
1 row in set (0.00 sec)

如果我再次重新创建存储过程它的作品

mysql> CALL mysp(1);
+------+
| col2 |
+------+
|    2 |
|    4 |
|    6 |
+------+
3 rows in set (0.18 sec)

+----------------------------------+
| Result                           |
+----------------------------------+
| Please see new temp table mydata |
+----------------------------------+
1 row in set (0.18 sec)

Query OK, 0 rows affected (0.18 sec)

但是,如果我尝试切换回第一列 - 即使我尝试先删除临时表 - 它仍然不起作用

mysql> CALL mysp(0);
ERROR 1054 (42S22): Unknown column 'qlgqp1.mydata.col2' in 'field list'
mysql> DROP TABLE mydata;
Query OK, 0 rows affected (0.03 sec)

mysql> CALL mysp(0);
ERROR 1054 (42S22): Unknown column 'qlgqp1.mydata.col2' in 'field list'
mysql>

* Eggyal 要求的其他信息。我也在另一个 mysql 版本上尝试了这个,结果相同。*

mysql> CALL mysp(1);
+------+
| col2 |
+------+
|    2 |
|    4 |
|    6 |
+------+
3 rows in set (0.20 sec)

+----------------------------------+
| Result                           |
+----------------------------------+
| Please see new temp table mydata |
+----------------------------------+
1 row in set (0.20 sec)

Query OK, 0 rows affected (0.20 sec)

mysql> describe mydata;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| col2  | int(11) | YES  |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
1 row in set (0.00 sec)

mysql> CALL mysp(0);
ERROR 1054 (42S22): Unknown column 'test.mydata.col2' in 'field list'
mysql> describe mydata;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| col1  | int(11) | YES  |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
1 row in set (0.00 sec)

有趣的修复开发 - 将最后几行更改为准备好的语句 - 但使用与以前完全相同的查询。

-- The following select call fails on 2nd and subsequent executions of the SP
   PREPARE stmt FROM 'SELECT SQL_NO_CACHE * FROM mydata';
   EXECUTE stmt;
   DEALLOCATE PREPARE stmt;
   SELECT "Please see new temp table mydata" as Result;
4

2 回答 2

3

MySQL 正在重用在前一次执行时准备的语句。这不是真正的“缓存”列名;它的“缓存”(如果你愿意的话)是准备好的语句。

简单的解决方法是使用动态 SQL 语句来控制行为,并避免重复使用先前准备的语句:

SET @s=CONCAT('SELECT ',@mycol,' FROM mydata');
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

列名被“缓存”或查询结果被缓存都无关紧要。这是一个性能优化;问题是在你的会议中已经准备好该声明。


通过使用动态 SQL,您可以控制何时准备语句(即解析 SQL 文本的语法(语句结构、关键字等)、检查语义(存在对象名称、列名称、用户具有所需的权限等)。 ),并准备执行计划。

使用静态 SQL,所有这些都发生在第一次执行时,然后 MySQL 会继续执行准备好的语句。

出于性能原因,我们不希望每次执行静态语句时都产生“硬解析”的开销。对于从 SQL 语句多次调用的函数尤其如此。

注意: Oracle 做同样的事情,但是,只要引用的对象被更改或删除,Oracle 就会很好地将准备好的语句标记为 INVALID。)

MySQL 选择不这样做,可能是因为跟踪所有依赖项的开销。而且,在绝大多数情况下,不需要这种开销。

我认为这里的教训是,如果您要使用动态 SQL 创建一个包含不同列的表,您将不得不使用动态 SQL 来查询该表。


我的建议是避免使用SELECT *,除非您的语句完全控制要返回的列,例如,从内联视图。否则,使用SELECT *的 SQL 语句从根本上被破坏......它们现在可以工作,但是对表的更改(例如添加一列)将破坏应用程序。


问:请解释它为什么不是错误。

这不是错误,因为存储过程中的 SELECT 语句实际上只是实际发生的事情的简写。

在您的过程第一次执行时,MySQL 正在解析您的查询文本,并准备和执行一条语句。基本上,相当于:

PREPARE s1 FROM 'SELECT * FROM mydata';
EXECUTE s1;

在该过程的第二次执行中,MySQL 只是执行先前准备好的语句。基本上,相当于:

EXECUTE s1;

在第二次执行时,您似乎期望 MySQL 运行相当于:

DEALLOCATE PREPARE s1;
PREPARE s1 FROM 'SELECT * FROM mydata';
EXECUTE s1;

你可以证明这是 MySQL在第二次执行时应该做的事情。您可能会争辩说,在先前执行过程期间准备的语句应该被丢弃,并在随后的执行中重新解析和重新准备。

DBMS 这样做并没有错。但是,与往常一样,会考虑对性能的影响。

您还可以假设 MySQL 应该跟踪特定准备好的语句所依赖的所有数据库对象。您可能会争辩说,每当这些数据库对象之一被删除或更改时,MySQL 应该使依赖于更改或删除对象的所有准备好的语句(以及所有其他对象)无效。同样,DBMS 这样做并没有错。一些 DBMS(如 Oracle)在这方面做得很好。但同样,DBMS 的开发人员在做出这些设计和实现决策时也会考虑性能。

最重要的是,MySQL确实为您提供了一种实现您想要发生的事情的方法。只是您的过程中的语法,您期望实现它的内容,并没有真正实现它。


首先它是一个临时表,所以真的不应该在那里,第二个 - 它被删除了

我认为您正在阅读"TEMPORARY"与规范中定义的关键字不同的内容。一个TEMPORARY表实际上就像一个常规表,只是它只对创建它的会话可见,并且在 MySQL 会话结束时会自动删除。(我们还注意到,一个TEMPORARY表不会由 SHOW TABLES 命令显示,也不会出现在 information_schema 视图中。)

至于哪些表(TEMPORARY 或其他)MySQL 应该期望“在那里”,我不相信文档真的解决了这个问题,除了注意到当 SQL 语句被执行时,并且该语句引用了一个不存在的对象, MySQL 会抛出异常。

您在 TEMPORARY 表中观察到的相同行为,您也会在非 TEMPORARY 表中观察到。该问题与表是否定义为 TEMPORARY 无关。


在哪里SELECT *比较 PREPARE s1 FROM SELECT *

这两种形式有效地遵循相同的代码路径。静态的第一次执行SELECT *实际上等效于:

PREPARE s1 FROM 'SELECT *';
EXECUTE s1;

(请注意执行后没有DEALLOCATE语句。)在随后的执行中,该语句已经准备好,因此它实际上等效于:

EXECUTE s1;

这类似于在 PHP mysqli 中编码时会发生的情况

$s1 = $mysqli->prepare("SELECT * FROM mydata");
$mysqli->execute($s1);
/* rename the columns in the mydata table */
$mysqli->execute($s1);
于 2012-07-06T23:56:26.167 回答
3

我知道这是相对较旧的(+6 个月),但我在准备好的语句中遇到了这个问题,我解决它的唯一方法是连接字段名称并使用有效调用“select *”但使用实际的准备好的语句字段名称。我正在使用准备好的语句来创建一个临时表,唯一的标准字段是第一个,而其余的则导致“ Select *”上的缓存问题。

  • 选择我们要在临时表中使用的字段
  • 遍历字段名称的表行,并在每次迭代中:

    设置 sql01 = concat(sql01,', ',sFieldName,''); (只是字段)和: set sql02 = concat(sql02,', ',sFieldName,'varchar(50) '); (仅限字段 + 字段类型,用于创建表语句)

  • 创建输出表:

    set @sql = concat('CREATE TEMPORARY TABLE tOutput(FirstField varchar(50), ',sql02,');'); 从@sql 准备STMT;执行STMT;取消分配准备 STMT;

  • 在最后:

    set @sql = concat('SELECT FirstField,',sql01,' FROM tOutput;'); 从@sql 准备STMT;执行STMT;取消分配准备 STMT;

于 2013-03-15T15:48:28.863 回答