准备好的声明没有速度优势?

时间:2012-03-22 15:42:22

标签: php mysql pdo prepared-statement

我听说如果查询多次完成,使用MySQL数据库的Prepared Statements可以提高速度,我认为我在项目中有一个理想的情况。但是我运行了一些基准测试,结果恰恰相反。我使用这些陈述是错误的(不是准备陈述的理想情况),还是它们没有我想象的那么快?

情况是锦标赛结果网格。有多所学校参加了多个活动,每所学校都有每个活动的分数。为了获得所有事件的单个学校的分数,它需要一个带有LEFT JOIN的SQL查询,如:

SELECT e.`id`, e.`name`, c.`competing`, c.`raw`, c.`final` FROM `events` e LEFT JOIN `scores` c ON e.`id`=c.`event_id` WHERE c.`school_id`=:school_id;

我使用原生PDO对象(prepare() / bindValue() / execute()query())编写了两个用于针对示例数据(200个事件)运行的PHP测试脚本:

编辑使用以下建议修改测试(vanilla查询需要获取,获取不同的ID,并在循环外绑定准备)。现在只为准备好的陈述提供适度的速度优势:

准备好的声明:

$start = microtime(true);
$sql = 'SELECT e.`id`, e.`name`, c.`competing`, c.`raw`, c.`final` FROM `events` e LEFT JOIN `scores` c ON e.`id`=c.`event_id` WHERE c.`school_id`=:school_id';
echo $sql."<br />\n";
$stmt = $db->prepare($sql);
$sid = 0;
$stmt->bindParam(':school_id', $sid);
for ($i=0; $i<$max; $i++) {
    $sid = rand(1,499);
    $stmt->execute();
    $rs = $stmt->fetchAll();
}
$delta = bcsub(microtime(true), $start, 4);
echo "<strong>Overall time:</strong> $delta<br />\n";
echo "<strong>Average time:</strong> ".($delta/$max)."<br />\n";

香草查询:

set_time_limit(15); // Add time for each run
$start = microtime(true);
$sql = 'SELECT e.`id`, e.`name`, c.`competing`, c.`raw`, c.`final` FROM `events` e LEFT JOIN `scores` c ON e.`id`=c.`event_id` WHERE c.`school_id`={$sid}';
echo $sql."<br />\n";
for ($i=0; $i<$max; $i++) {
    $sid = rand(1,499);
    $stmt = $db->query("SELECT e.`id`, e.`name`, c.`competing`, c.`raw`, c.`final` FROM `events` e LEFT JOIN `scores` c ON e.`id`=c.`event_id` WHERE c.`school_id`={$sid}");
    $rs = $stmt->fetchAll();
}
$delta = bcsub(microtime(true), $start, 4);
echo "<strong>Overall time:</strong> $delta<br />\n";
echo "<strong>Average time:</strong> ".($delta/$max)."<br />\n";

我一遍又一遍地获取相同学校的活动分数(学校ID#10),并将$max设置为10,000,我得到的结果显示香草查询快30%(25.72秒对36.79) 。我是做错了,还是这样准确,即使在重复的情况下,准备好的陈述也不会更快?

编辑更新后的测试现在准备33.95秒而不是34.10香草。 Huzzah,准备好的陈述更快。但是10,000次迭代只需要几分之一秒。可能是因为我的查询不是那么复杂(Prepared语句缓存解析树的优势)?或者还有更多优化吗?

3 个答案:

答案 0 :(得分:4)

vanilla查询每次执行完全相同的查询,因此您只是测试“从查询缓存中获取结果”次数,而不是实际的查询执行时间。这不是一个有效的测试。

您必须在循环中进行查询构建,因此每次都要强制执行新查询:

for ($i=0; $i<$max; $i++) {
    $sql = <<<EOL
SELECT e.id, e.name, c.competing, c.raw, c.final
FROM events e
LEFT JOIN scores c ON e.id=c.event_id
WHERE c.school_id= $i;
EOL;
    $rs = $db->query($sql);
}

答案 1 :(得分:1)

这些人中没有一个明显地指出:

PDO 默认使用MySQL 模拟准备好的语句,即使驱动程序支持它们。

这是PHP PDO manual page所说的完全相反,但是view the source code and you'll see they always, de facto, use emulated prepared statements(我认为这是一个编码错误,但当我提交报告时,它是被归类为WONT_FIX并且我有一些Zend dev仅仅声明,“我们的政策始终是模仿,只是因为。”这对我来说没有意义,但是很好。


要使用真正准备好的陈述,你必须跳过一个箍。如果你不喜欢这个,请责怪Zend开发者,因为修复看起来需要5分钟(只需移动一下)。

$pdo = new PDO($dsn, $user, $pass, array(ATTR::PDO_EMULATE_PREPARES => false));

如果您进行此更改,重新运行基准测试,并使用此代码更新我们的时间安排,我将非常感激。

答案 2 :(得分:-2)

看来你可能没有比较苹果和苹果。

PDO::query()执行SQL语句,将结果集作为PDOStatement对象返回。

要获得实际结果,您需要迭代返回的对象,或者与准备好的语句一样,调用fetchAll()将整个结果集加载到数组中

正确的vanilla查询循环应该是:

for ($i=0; $i<$max; $i++) {
    $stmt = $db->query($sql);
    $rs = $stmt->fetchAll();
}

或者从准备好的语句循环中删除fetchAll()调用。

您还可以使用bindParam()代替bindValue()

来减少预准备语句所需的方法调用
$school_id = null;
$stmt->bindParam(':school_id', $school_id);
for ($i=0; $i<$max; $i++) {
    $school_id = 10;
    $stmt->execute();
    $rs = $stmt->fetchAll();
}