"'字段列表中的未知列'"准备好的语句的占位符在子查询中

时间:2015-08-20 17:32:56

标签: php mysql pdo prepared-statement

我在Ubuntu 14.04 LTS上使用PHP 5.5.9和MySQL 5.5.44以及mysqlnd 5.0.11-dev。以下陈述未能准备:

$db->prepare("SELECT nr.x FROM (SELECT ? AS x) AS nr")

尽管以下声明成功准备,但仍然如此:

$db->prepare("SELECT nr.x FROM (SELECT '1337' AS x) AS nr")

造成这种差异的原因是什么? The manual表示"参数标记只能在数据值出现的地方使用,而不能用于SQL关键字,标识符等。"但这是一个数据值。

不是PDO的错误

同样的事情发生在独立客户端:

mysql -uredacted -predacted redacted
-- Type 'help;' or '\h' for help.
SELECT nr.x FROM (SELECT '1337' AS x) AS nr;
-- x
-- 1337
-- 1 row in set (0.00 sec)
PREPARE workingstmt FROM 'SELECT nr.x FROM (SELECT ''1337'' AS x) AS nr';
-- Query OK, 0 rows affected (0.00 sec)
-- Statement prepared
DEALLOCATE PREPARE workingstmt;
-- Query OK, 0 rows affected (0.00 sec)
PREPARE brokenstmt FROM 'SELECT nr.x FROM (SELECT ? AS x) AS nr';
-- ERROR 1054 (42S22): Unknown column 'nr.x' in 'field list'
^D
-- Bye

我的动机

我试图在具有自动递增主键的表中添加行。在InnoDB的默认自动增量锁定模式中,the manual调用"连续",InnoDB在插入行时跳过自动增量值,但不是,就像这样INSERT IGNOREON DUPLICATE KEY UPDATE的{​​{1}}或UNIQUE会遇到INSERT IGNORE值与要插入的行匹配的现有行。 (这些被称为"混合模式插入"在手册中。)

每隔几个小时,我就会从供应商处导入一个Feed。这有大约200,000行,并且这些行中平均有200行具有与表中已存在的值对应的唯一值。因此,如果我一直使用ON DUPLICATE KEY UPDATEINSERT IGNORE,我会每隔几小时刻录199,800个ID。因此,我不想使用ON DUPLICATE KEY UPDATEINTEGER UNSIGNED,因为我担心随着时间的推移重复插入UNIQUE可能耗尽42亿的限制BIGINT BIGINT 1}}键。我不想将列切换到innodb_autoinc_lock_mode类型,因为32位PHP没有与MySQL INSERT INTO ... SELECT具有相同语义的类型。服务器管理员不愿意为服务器的所有用户切换到64位PHP或更改INSERT INTO ... SELECT

因此,我决定尝试INSERT INTO the_table (uniquecol, othercol1, othercol2) SELECT nr.uniquecol, :o1 AS othercol1, :o2 AS othercol2 FROM ( SELECT ? AS uniquecol ) AS nr LEFT JOIN the_table ON nr.settlement_id = the_table.settlement_id WHERE the_table.row_id IS NULL ,创建一个包含子查询中唯一键列的1行表,并将其连接到主表以拒绝已存在的唯一键值。 (手册中说["42S22",1054,"Unknown column 'settlement_id' in 'field list'"]是"批量插入",它不会刻录ID。)目的是做这样的事情:

<?php // MCVE follows

/* Connect to database */
$pdo_dsn = 'mysql:host=127.0.0.1;dbname=redacted';
$pdo_username = 'redacted';
$pdo_password = 'redacted';
$pdo_options = [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',];
$db = new PDO($pdo_dsn, $pdo_username, $pdo_password, $pdo_options);
$pdo_dsn = $pdo_username = $pdo_password = 'try harder';

// ensure that PDO doesn't convert everything to strings
// per http://stackoverflow.com/a/15592818/2738262
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);

/* Create mock data with which to test the statements */
$prep_stmts = ["
CREATE TEMPORARY TABLE sotemp (
  file_id INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  settlement_id VARCHAR(30) NOT NULL,
  num_lines INTEGER UNSIGNED NOT NULL DEFAULT 0,
  UNIQUE (settlement_id)
)
","
INSERT INTO sotemp (settlement_id, num_lines) VALUES
('15A1', 150),
('15A2', 273),
('15A3', 201)
"];
foreach ($prep_stmts as $stmt) $db->exec($stmt);

/* Now the tests */

$working_stmt = $db->prepare("
SELECT nr.settlement_id
FROM (
  -- change this to either a value in sotemp or one not in sotemp
  -- and re-run the test program
  SELECT '15A3' AS settlement_id
) AS nr
LEFT JOIN sotemp ON nr.settlement_id = sotemp.settlement_id
WHERE sotemp.file_id IS NULL
");
if ($working_stmt) {
  $working_stmt->execute();
  $data = $working_stmt->fetchAll(PDO::FETCH_ASSOC);
  echo "Working: ".json_encode($data)."\n";
} else {
  echo "Working statement failed: ".json_encode($db->errorInfo())."\n";
}

$broken_stmt = $db->prepare("
SELECT nr.settlement_id
FROM (
  SELECT ? AS settlement_id
) AS nr
LEFT JOIN sotemp ON nr.settlement_id = sotemp.settlement_id
WHERE sotemp.file_id IS NULL
");
if ($broken_stmt) {
  $broken_stmt->execute(['15A4']);
  $data = $broken_stmt->fetchAll(PDO::FETCH_ASSOC);
  echo "Broken: ".json_encode($data)."\n";
} else {
  echo "Broken statement failed: ".json_encode($db->errorInfo())."\n";
}

这失败了,给出了PDO错误: Dog

class Dog(object):
    def __init__(self, name, module_name):
        self.name = name
        # imports module
        module = __import__("mylib.tricks.{mod}".format(mod=module_name),
                            from_list=['arbitrary argument'])
        # Dog object gets all tricks from module as bound methods
        tricks = module.__all__
        for trick in tricks:
             exec("self." + trick + "=types.MethodType(" + trick + ", self)")

导致此错误的原因是什么?并且只有在主键不存在且没有耗尽自动增量ID的情况下才有更好的方法来插入行吗?

1 个答案:

答案 0 :(得分:5)

您的最新修改使问题非常明确,因此我将尝试回答: 造成这种差异的原因是占位符。

如记录here所示,占位符只能在查询中的某些位置使用。特别是:

  

参数标记只能用于显示数据值的位置,而不能用于SQL关键字,标识符等。

现在您可能已经注意到SELECT ? as x准备好了,但不是SELECT nr.x FROM (SELECT ? AS x) AS nr。这是为什么?好吧,an anonymous author on PHP's doc最好解释一下,所以让我复制/粘贴:

  

关于预处理语句中的占位符如何工作存在一个常见的误解:它们不是简单地替换为(转义的)字符串,而是执行生成的SQL。相反,DBMS要求&#34;准备&#34;一个语句提供了一个完整的查询计划,用于说明它将如何执行该查询,包括它将使用哪些表和索引,无论你如何填写占位符,它都是相同的。

所以简单地说:因为你在FROM子句中的子查询中使用占位符,MySQL无法计算查询的执行计划。

换句话说,因为您的查询将始终更改,所以没有&#34;模板&#34;可以为它做好准备。

因此,如果您确实想要使用此查询,则需要使用普通(未准备好的)查询,或者返回PDO的模拟预准备语句。

话虽如此,请考虑评论部分提供的各种备选方案。对于您想要实现的目标,有更好的解决方案。