【问题标题】:Preparing SQL Statements with PDO使用 PDO 准备 SQL 语句
【发布时间】:2010-11-18 06:44:39
【问题描述】:

我的代码如下所示:

// Connect to SQLite DB
DB('/path/to/sqlite.db');

DB('BEGIN TRANSACTION;');

// These loops are just examples.
for ($i = 1; $i <= 10000; $i++)
{
    for ($j = 1; $j <= 100; $j++)
    {
        DB('INSERT INTO "test" ("id", "name") VALUES (?, ?);', $i, 'Testing ' . $j);
    }
}

DB('END TRANSACTION;');

这里是 DB() 函数:

function DB($query)
{
    static $db = null;

    if (is_file($query) === true)
    {
        $db = new PDO('sqlite:' . $query, null, null, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING));
    }

    else if (is_a($db, 'PDO') === true)
    {
        $result = $db->prepare($query);

        if (is_a($result, 'PDOStatement') === true)
        {
            if ($result->execute(array_slice(func_get_args(), 1)) === true)
            {
                if (stripos($query, 'INSERT') === 0)
                {
                    return $db->lastInsertId();
                }

                if (stripos($query, 'SELECT') === 0)
                {
                    return $result->fetchAll(PDO::FETCH_ASSOC);
                }

                return $result->rowCount();
            }
        }

        return false;
    }

    return true;
}

问题是内部循环中的 DB() 调用需要很长时间才能完成,我认为这样做可以大大加快执行速度:

DB('BEGIN TRANSACTION;');

for ($i = 1; $i <= 10000; $i++)
{
    $queries = array();

    for ($j = 1; $j <= 100; $j++)
    {
        $queries[] = 'INSERT INTO "test" ("id", "name") VALUES (?, ?);' /*, $i, 'Testing ' . $j;*/
    }

    DB(implode("\n", $queries));
}

DB('END TRANSACTION;');

问题是我不知道如何用相应的变量准备(替换和转义)所有问号,有什么办法可以做到这一点吗?

【问题讨论】:

    标签: php database sqlite pdo


    【解决方案1】:

    如果您在循环中“准备”它们,为什么要使用准备好的语句? (在 DB 函数中)

    在循环之前做一个准备,在循环中只给出值并执行。 当然,这需要重写您的 DB 方法。

    哦,顺便说一句。您的 ID 列是主键吗?如果是这样,您还有另一个问题,因为“i”将与 100 个“j”相同:)

    例如:

    $sth = $dbh->prepare('INSERT INTO "test" ("id", "name") VALUES (:id, :name)');
    $j=0;
    for ($i = 1; $i <= 10000; $i++){
       $j = ($j==100) ? 0 : $j++;
       $sth->execute(array(':id' => $i, ':name' => 'Testing ' . $j));     
    }
    

    【讨论】:

      【解决方案2】:

      如果您要向表中插入大量数据,请尝试在一个查询中插入数据。

      $query = 'INSERT INTO "test" ("id", "name") VALUES ';
      $data = array();
      for ($i = 1; $i <= 10000; $i++) {
        for ($j = 1; $j <= 100; $j++) {
          $query .= '(?,?),';
          $data[] = $i;
          $data[] = 'Testing '.$j;
        }
      }
      
      $query = substr($query, 0, -1);
      DB($query, $data);
      

      这应该可以消除单个插入查询的开销。 不过查询长度是有限制的,如果您遇到查询长度问题,请尝试在 for 循环中更频繁地发出 DB() 调用。

      【讨论】:

      • 这就是我一直在寻找的,但是我在查询 INSERT OR REPLACE INTO "test" VALUES (?, ?), (?, ?), (?, ? ); = "警告:PDO::prepare(): SQLSTATE[HY000]: 一般错误:1 附近 ",": 语法错误" 知道为什么吗?
      • PS:INSERT INTO "test" ("id", "name") VALUES (?, ?), (?, ?), (?, ?);
      • 正确的 SQL 是 INSERT INTO "test" ("id", "name") SELECT ?, ?联合选择?,? UNION SELECT ?, ?;
      【解决方案3】:

      很遗憾,我认为问题可能出在您的代码结构上。

      在您的 INSERT 语句循环中,这些语句都是相同的,无需每次都调用 $db->prepare。准备好的语句背后的想法是您调用 $db->prepare() 一次,并且可以在同一个语句对象上多次调用 execute()。您每次都在调用 $db->prepare(),这会导致解析 SQL 语句和创建新对象的开销。

      考虑像这样重写你的 DB() 函数:

      function do_query($db, $pdo_statement, $query, $args)
      {
          if ($pdo_statement->execute($args) === true)
          {
              if (stripos($query, 'INSERT') === 0)
              {
                return $db->lastInsertId();
              }
              if (stripos($query, 'SELECT') === 0)
              {
                return $result->fetchAll(PDO::FETCH_ASSOC);
              }
              return $result->rowCount();
          }
      }
      
      function DB($query)
      {
          static $db = null;
      
          if (is_file($query) === true)
          {
            $db = new PDO('sqlite:' . $query, null, null, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING));
          }
      
          else if (is_a($db, 'PDO') === true)
          {
            $result = $db->prepare($query);
      
            if (is_a($result, 'PDOStatement') === true)
            {
              $args = func_get_args();
              if (is_array($args[1])) {
                  $ret = array();
                  foreach ($args[1] as $args) {
                      $ret[] = do_query($db, $query, $result, $args);
                  }
                  return $ret;
              }
      
              return do_query($db, $query, $result, array_slice(func_get_args(), 1));
            }
      
            return false;
          }
      
          return true;
      }
      

      因此,如果您想使用大量值运行相同的查询,您可以创建要插入的值的二维数组,然后调用DB('INSERT INTO....', $values)。 DB() 函数检查函数的第二个参数(在 $query 之后)是否是一个数组,如果是,它会循环运行 $query 对数组中的值。这样,循环就不需要每次都重新准备 SQL 语句,只需使用不同的值重新执行它。该函数的返回值将是每个查询结果的数组。

      【讨论】:

        【解决方案4】:

        最初发布的 DB 函数在每次运行时都会发出一个文件系统 stat() 系统调用,以检查查询字符串是否为文件。虽然这不仅是导致执行缓慢的唯一原因,但它也是造成这种情况的原因。

        【讨论】:

        • 我明白,但是我在编写这个函数时考虑到了可移植性(能够执行所有常见数据库操作的单个函数),而不是速度。我使用 michal kralik 的建议解决了这个问题,我还将所有已经准备好的查询添加到一个静态数组中,这节省了很多执行时间。