MySQLi:使用一个预准备语句插入多行

时间:2013-02-13 18:38:03

标签: mysql mysqli prepared-statement

我制作了一个脚本,用于创建原始查询字符串,然后在一个语句中插入数百行。它有效,但不提供准备好的声明所做的保护。然后,我修改了我的脚本以添加预准备语句。然而,它工作得很慢。由于脚本一次一行地运行每个准备好的insert语句而不是一次插入数百行,因此带有预处理语句的脚本比原始查询脚本需要更长的时间来插入行。

这是准备好的语句代码的片段:

for( $j = 0; $j < $abilitiesMax - 2; $j++ ){
  $stmtAbility->bind_param('iiiii', $abilityArray[$i]["match_id"] , $abilityArray[$i]["player_slot"],
  $abilityArray[$i][$j]["ability"], $abilityArray[$i][$j]["time"], $abilityArray[$i][$j]["level"] );

  if(  !($stmtAbility->execute()) ){      
   echo "<p>$db->error</p>";
   echo "<p>ERROR: when trying to insert abilities query</p>";
  }
}

它完成了工作,但只有数百次插入后才能完成。有没有办法将列表或数组绑定到bind_param()参数,只需运行$ stmtAbility-&gt;执行一次或其他一些可以提高性能的方法。

很抱歉,如果之前已经询问并回答过。我环顾了一会儿,发现了一些类似的问题,但没有回答我明确要求的问题。

1 个答案:

答案 0 :(得分:1)

通过动态构建批量插入语句查询是可能的,但需要一些技巧。最重要的位是使用str_pad()构造可变长度的查询字符串,并使用call_user_func_array()使用可变数量的参数调用bind_param()

function insertBulkPrepared($db, $table, $fields, $types, $values) {
    $chunklength = 500;
    $fieldcount = count($fields);
    $fieldnames = '`'.join('`, `', $fields).'`';
    $prefix = "INSERT INTO `$table` ($fieldnames) VALUES ";
    $params = '(' . str_pad('', 3*$fieldcount - 2, '?, ') . '), ';
    $inserted = 0;

    foreach (array_chunk($values, $fieldcount*$chunklength) as $group) {
        $length = count($group);
        if ($inserted != $length) {
            if ($inserted) $stmt->close();
            $records = $length / $fieldcount;
            $query = $prefix . str_pad('', 3*$length + 2*($records - 1), $params);
            #echo "\n<br>Preparing '" . $query . "'";
            $stmt = $db->prepare($query);
            if (!$stmt) return false;
            $binding = str_pad('', $length, $types);
            $inserted = $length;
        }

        array_unshift($group, $binding);
        #echo "\n<br>Binding " . var_export($group, true);
        $bound = call_user_func_array(array($stmt, 'bind_param'), $group);
        if (!$bound) return false;
        if (!$stmt->execute()) return false;
    }

    if ($inserted) $stmt->close();
    return true;
}

此函数将$db作为mysqli实例,表名,字段名称数组以及对值的引用的平面数组。它每个查询最多可插入500条记录,并尽可能重复使用预准备语句。如果所有插入成功,则返回true;如果任何插入失败,则返回false。注意事项:

  • 表格和字段名称未转义;我把它留给你,以确保它们不包含反引号。幸运的是,它们永远不应来自用户输入。
  • 如果$values的长度不是$fields长度的偶数倍,则最后的块可能会在准备阶段失败。
  • 同样,在大多数情况下,$types参数的长度应与$fields的长度相匹配,尤其是当其中一些参数不同时。
  • 它不区分三种失败方式。它也不会跟踪成功插入的数量,也不会在发生错误后继续尝试继续。

定义此函数后,您的示例代码可以替换为:

$inserts = array();
for ($j = 0; $j < $abilitiesMax - 2; $j++) {
    $inserts[] = &$abilityArray[$i]['match_id'];
    $inserts[] = &$abilityArray[$i]['player_slot'];
    $inserts[] = &$abilityArray[$i][$j]['ability'];
    $inserts[] = &$abilityArray[$i][$j]['time'];
    $inserts[] = &$abilityArray[$i][$j]['level'];
}

$fields = array('match_id', 'player_slot', 'ability', 'time', 'level');
$result = insertBulkPrepared($db, 'abilities', $fields, 'iiiii', $inserts);
if (!$result) {
    echo "<p>$db->error</p>";
    echo "<p>ERROR: when trying to insert abilities query</p>";
}

这些&符号很重要,因为mysqli_stmt::bind_param需要引用,而call_user_func_array在最近的PHP版本中不提供引用。

您没有向我们提供原始准备好的声明,因此您可能需要调整表格和字段名称。它看起来像你的代码位于$i的循环内;在这种情况下,只有for循环需要在外循环内。如果你将其他行放在循环之外,你将使用更多的内存来构造$inserts数组,以换取更有效的批量插入。

也可以重写insertBulkPrepared()以接受多维数组,消除一个潜在错误源,但这需要在分块后展平数组。