“Columna desconocida en 'lista de campos'” cuando el marcador de posición de la declaración preparada está en subconsulta

Estoy usando PHP 5.5.9 y MySQL 5.5.44 con mysqlnd 5.0.11-dev en Ubuntu 14.04 LTS. La siguiente declaración no se prepara:

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

Esto a pesar del hecho de que la siguiente declaración se prepara con éxito:

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

¿Qué causa esta diferencia?El manual dice "Los marcadores de parámetros solo se pueden usar donde deben aparecer valores de datos, no para palabras clave SQL, identificadores, etc." Pero esto es por un valor de datos.

No es culpa de PDO

Lo mismo sucede en el cliente independiente:

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
Mi motivación

Estoy tratando de agregar una fila a una tabla que tiene una clave primaria de incremento automático. En el modo de bloqueo de incremento automático predeterminado de InnoDB, queel manual llama "consecutivas", InnoDB omite un valor de incremento automático cuando se puede insertar una fila pero no lo es, como es el caso conINSERT IGNORE oON DUPLICATE KEY UPDATE que se encuentra con una fila existente cuyoUNIQUE los valores coinciden con los de la fila que se inserta. (Estos se denominan "inserciones de modo mixto" en el manual).

Cada pocas horas, importo un feed de mi proveedor. Esto tiene alrededor de 200,000 filas, y todas, pero en promedio 200 de estas filas tienen valores únicos que corresponden a valores ya presentes en la tabla. Entonces si tuviera que usarINSERT IGNORE oON DUPLICATE KEY UPDATE todo el tiempo, me quemaría 199.800 identificaciones cada pocas horas. Entonces no quiero usarINSERT IGNORE oON DUPLICATE KEY UPDATE por temor a agotar el límite de 4.200 millones deINTEGER UNSIGNED con inserciones repetidas a lo largo del tiempo en una mesa con el mismoUNIQUE llave. No quiero cambiar la columna aBIGINT escriba porque PHP de 32 bits no tiene tipo con la misma semántica que MySQLBIGINT. El administrador del servidor no está dispuesto a cambiar a PHP de 64 bits o cambiarinnodb_autoinc_lock_mode para todos los usuarios del servidor.

Entonces, en cambio, decidí probarINSERT INTO ... SELECT, creando una tabla de 1 fila con las columnas de clave única en una subconsulta y uniéndola a la tabla principal para rechazar valores de clave únicos que ya existen. (El manual diceINSERT INTO ... SELECT es una "inserción masiva", que no quema ID.) La intención es hacer algo como esto:

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

Esto falló, dando el error PDO:["42S22",1054,"Unknown column 'settlement_id' in 'field list'"]

<?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";
}

Que esta causando este error? ¿Y hay una mejor manera de insertar una fila solo si la clave principal no existe sin agotar las ID de incremento automático?

Respuestas a la pregunta(1)

Su respuesta a la pregunta