【问题标题】:PDO/MySQL memory consumption with large result set具有大结果集的 PDO/MySQL 内存消耗
【发布时间】:2011-10-17 05:30:08
【问题描述】:

我在处理从大约 30,000 行的表中进行选择时遇到了奇怪的问题。

似乎我的脚本在简单的、仅向前遍历查询结果时使用了惊人的内存量。

请注意,这个例子有点做作,绝对是最小的例子,与真实代码几乎没有相似之处,不能用简单的数据库聚合代替。它旨在说明不需要在每次迭代中保留每一行。

<?php
$pdo = new PDO('mysql:host=127.0.0.1', 'foo', 'bar', array(
    PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
));
$stmt = $pdo->prepare('SELECT * FROM round');
$stmt->execute();

function do_stuff($row) {}

$c = 0;
while ($row = $stmt->fetch()) {
    // do something with the object that doesn't involve keeping 
    // it around and can't be done in SQL
    do_stuff($row);
    $row = null;
    ++$c;
}

var_dump($c);
var_dump(memory_get_usage());
var_dump(memory_get_peak_usage());

这个输出:

int(39508)
int(43005064)
int(43018120)

我不明白为什么每次几乎不需要保存任何数据时使用 40 meg 的内存。我已经计算出通过将“SELECT *”替换为“SELECT home, away”可以将内存减少大约 6 倍,但是我认为即使这种用法也非常高,而且表格只会变得更大。

是否有我遗漏的设置,或者我应该注意 PDO 中的某些限制?如果 PDO 不能支持这一点,我很高兴摆脱 PDO 以支持 mysqli,所以如果这是我唯一的选择,我将如何使用 mysqli 来执行此操作?

【问题讨论】:

  • 这正是 noSQL 和 MapReduce 的用例。由于 map 和 reduce 函数将在您的数据“附近”执行。你可以试试 MongoDB 或 CouchBase(或者 Hadoop 为什么不试试,其实很简单)。

标签: php mysql pdo


【解决方案1】:

创建连接后,需要将PDO::MYSQL_ATTR_USE_BUFFERED_QUERY设置为false:

<?php
$pdo = new PDO('mysql:host=127.0.0.1', 'foo', 'bar', array(
    PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
));
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

// snip

var_dump(memory_get_usage());
var_dump(memory_get_peak_usage());

这个输出:

int(39508)
int(653920)
int(668136)

无论结果大小如何,内存使用情况几乎都是静态的。

【讨论】:

  • 谢谢!这只是以相同的速度将我的脚本从使用 3.5gb 内存更改为 42mb! ☺️
【解决方案2】:

另一种选择是执行以下操作:

$i = $c = 0;
$query = 'SELECT home, away FROM round LIMIT 2048 OFFSET %u;';

while ($c += count($rows = codeThatFetches(sprintf($query, $i++ * 2048))) > 0)
{
    foreach ($rows as $row)
    {
        do_stuff($row);
    }
}

【讨论】:

  • 请注意,如果您正在读取的表可以在每个查询之间更改,则使用多个查询可能会导致严重问题。 (这有时可以通过自动递增键或创建日期的升序来缓解。)
  • 您不需要使用$c += count,而是使用$c = count,因为它需要在每个循环中进行评估。我无法编辑您的答案,因为更改至少需要 6 个字符......
【解决方案3】:

在您开始查看之前,整个结果集(全部 30,000 行)被缓冲到内存中。

您应该让数据库进行聚合,并且只要求它提供您需要的两个数字。

SELECT SUM(home) AS home, SUM(away) AS away, COUNT(*) AS c FROM round

【讨论】:

  • 您能否详细说明缓冲方面及其解决方法?我已经更新了这个问题,以反映该示例有些人为,不能像您建议的那样简单地用数据库内聚合替换。
  • 对不起,多年的经验告诉我不要相信你的话。我真的怀疑您是否需要检索所有 30,000 行。您可能会对 SQL 的表达能力感到惊讶。如果您可以提供您的实际代码或解释为什么您认为您需要全部 30,000 行,也许我们可以找到某个地方。
  • @DanGrossman 我正在将所有 30,000 行导出到一个制表符分隔的文本文件中。
  • 您可以直接从 SQL 执行此操作 -- 查询可以将行写入制表符分隔的文本文件 -- dev.mysql.com/doc/refman/5.7/en/select-into.html,或者您可以使用循环一次选择几千行查询中的 LIMIT 子句。
【解决方案4】:

实际情况是,如果您获取所有行并希望能够在 PHP 中遍历所有行,那么它们将立即存在于内存中。

如果您真的不认为使用 SQL 驱动的表达式和聚合是您可以考虑限制/分块数据处理的解决方案。而不是一次获取所有行,而是执行以下操作:

1)  Fetch 5,000 rows
2)  Aggregate/Calculate intermediary results
3)  unset variables to free memory
4)  Back to step 1 (fetch next set of rows)

只是一个想法......

【讨论】:

  • 这实际上是我最初所做的。我出于好奇问了这个问题,因为在我没有保留任何对象的情况下,我不能只使用 PDO 处理具有任意大小结果的查询,这对我来说似乎很愚蠢。事实上,使用 MySQLi 可以做到这一点,并且内存使用不会无限增长,但如果可能的话,我更愿意坚持使用 PDO。
【解决方案5】:

我以前在 PHP 中没有这样做过,但您可以考虑使用可滚动游标来获取行 - 请参阅 the fetch documentation 以获取示例。

它不会将查询的所有结果一次返回给您的 PHP 脚本,而是将结果保存在服务器端,您可以使用游标遍历它们,一次获取一个。

虽然我没有对此进行测试,但它肯定会存在其他缺点,例如使用更多的服务器资源,并且很可能由于与服务器的额外通信而降低了性能。

更改 fetch 样式也可能会产生影响,因为默认情况下,文档表明它将存储关联数组和数字索引数组,这必然会增加内存使用量。

正如其他人所建议的那样,如果可能的话,首先减少结果数量很可能是一个更好的选择。

【讨论】:

  • 我尝试将调用 prepare 更改为 $stmt = $pdo-&gt;prepare('SELECT * FROM round', array(PDO::ATTR_CURSOR =&gt; PDO::CURSOR_FWDONLY));,但内存使用量保持不变。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-04
  • 2022-08-17
  • 2015-08-23
  • 2011-03-11
相关资源
最近更新 更多