【问题标题】:Best way to INSERT many values in mysqli?在 mysqli 中插入许多值的最佳方法?
【发布时间】:2013-02-15 10:45:09
【问题描述】:

我正在寻找一种使用 PHP 和 MySQLi 一次插入大量行(约 2000 年)的 SQL 注入安全技术。
我有一个包含所有必须包含的值的数组。 目前我正在这样做:

<?php
$array = array("array", "with", "about", "2000", "values");

foreach ($array as $one) 
{
    $query = "INSERT INTO table (link) VALUES ( ?)";
    $stmt = $mysqli->prepare($query);
    $stmt ->bind_param("s", $one);
    $stmt->execute();
    $stmt->close();
}
?>

我尝试了call_user_func_array(),但它导致了堆栈溢出。

有什么更快的方法可以做到这一点(比如一次全部插入?),但仍然可以防止 SQL 注入(比如准备好的语句)和堆栈溢出?
谢谢!

【问题讨论】:

  • 1 次准备,N 次执行
  • 但是如果我在执行时设置循环真的会更快吗?

标签: php mysql mysqli


【解决方案1】:

您应该能够通过将插入内容放入事务中来大大提高速度。您还可以将您的准备和绑定语句移到循环之外。

$array = array("array", "with", "about", "2000", "values");
$query = "INSERT INTO table (link) VALUES (?)";
$stmt = $mysqli->prepare($query);
$stmt ->bind_param("s", $one);

$mysqli->query("START TRANSACTION");
foreach ($array as $one) {
    $stmt->execute();
}
$stmt->close();
$mysqli->query("COMMIT");

我在我的网络服务器上用 10,000 次迭代测试了这段代码。

无交易:226 seconds. 有交易:2 seconds. 或者two order of magnitude speed increase,至少对于那个测试。

【讨论】:

  • 魔法:SET GLOBAL innodb_flush_log_at_trx_commit = 0; 然后再次测试无交易;-)
  • @YourCommonSense 这确实将没有事务的时间缩短到 2 秒,尽管根据该设置的文档,它似乎不应该与事务的速度相同,不是默认,并且可能不是超级安全的。我读错了吗(或者还有其他问题?)
  • @YourCommonSense 另外感谢您的设置,就我而言,丢失一秒钟的交易并不是什么大风险,而且它加快了我服务器上的几件事,因为很少有应用程序明确使用交易看来。
  • 不要忘记将其设置为永久。这个设置确实slows down writes with innodb
  • @DanMetheus 能否为多列添加一个版本?
【解决方案2】:

您应该首先将您的数组转换为字符串。鉴于它是一个字符串数组(不是二维数组),您可以使用implode 函数。

请注意,每个值都应括在括号中并正确转义,以确保正确的INSERT 语句并避免 SQL 注入的风险。为了正确转义,您可以使用PDOConnectionquote 方法——假设您通过PDO 连接到MySQL。要对数组的每个条目执行此操作,您可以使用array_map

在转义每个值并将它们内爆成单个字符串后,您需要将它们放入INSERT 语句中。这可以通过sprintf 完成。

例子:

<?php
$connection = new PDO(/*...*/);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$dataToBeSaved = [
    'some',
    'data',
    'with "quotes"',
    'and statements\'); DROP DATABASE facebook_main; --'
];


$connection->query(
    sprintf(
        'INSERT INTO table (link) VALUES %s',
        implode(',',
            // for each entry of the array
            array_map(function($entry) use ($connection) { 
                // escape it and wrap it in parenthesis
                return sprintf('(%s)', $connection->quote($entry));
            }, $dataToBeSaved)
        )
    )
);

注意:根据您愿意插入数据库的记录数量,您可能需要将它们拆分为多个 INSERT 语句。

【讨论】:

    【解决方案3】:

    再试一次,我不明白为什么你的原始代码在稍作修改后就不能工作:

    $query = "INSERT INTO table (link) VALUES (?)";
    $stmt = $mysqli->prepare($query);
    $stmt->bind_param("s", $one);
    
    foreach ($array as $one) {
        $stmt->execute();
    }
    $stmt->close();
    

    【讨论】:

    • 不使用prepared statement,直接在SQL-Query中插入值,不绑定和使用mysql_real_escape_string怎么样?
    • @Copy Devil:你是在尝试解决一个真正的任务,还是只是在想最奇怪的解决方案?
    • 我同意@Mike。 mysql_real_escape_string 不会那么安全,并且您将安全性指定为首要问题。没有什么比参数化查询更好的了
    • 执行参数化查询没有安全优势。格式正确的查询与准备好的查询一样安全。只要您将only strings 添加到查询中,mysql_real_escape_string 就可以了。这个诚实函数的唯一问题是开发人员试图使用它来格式化不同类型的值,因为它完全没用
    • 这对我来说似乎也违反直觉,但请参阅 PHP 手册的 PHP 准备语句部分 php.net/manual/en/mysqli.quickstart.prepared-statements.php 的示例 #3。我还在自己的 Web 服务器上对其进行了测试,您甚至可以在实际初始化它将最终使用的变量之前准备绑定。
    【解决方案4】:

    是的,您可以手动构建一个大查询,例如:

    $query = "";
    foreach ($array as $curvalue) {
      if ($query)
        $query .= ",";
      $query .= "('" . $mysqli->real_escape_string($curvalue) . "')";
    }
    if ($query) {
      $query = "INSERT INTO table (link) VALUES " . $query;
      $mysqli->query($query);
    }
    

    【讨论】:

    • real_escape_string 有多脆弱?到目前为止,我一直使用准备好的语句。
    • 准备好的带有字符串的语句本质上与 real_escape_string 完全相同。不同之处在于,使用 real_escape_string 通常代码更多,更容易出错。
    • 所以?我现在对答案的cmets有点困惑。我不确定我接受了正确的答案^^ 两个答案哪个更快? real_escape 真的和prepared statements 一样安全吗?
    • 我可以告诉你,我使用大 INSERT 作为唯一来回使用服务器的方法将比单独执行 2000 多个插入要快得多。至于安全性,我不知道一个正确完成的转义字符串在任何情况下都比准备更不安全,只是更容易出错。如果有人知道的更好,请随时发表评论!
    • 您也可以两全其美,将单个查询用作动态绑定值的预准备语句。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-07
    相关资源
    最近更新 更多