【问题标题】:What is the difference between laravel cursor and laravel chunk method?laravel cursor 和 laravel chunk 方法有什么区别?
【发布时间】:2018-01-09 21:44:15
【问题描述】:

我想知道 laravel 块和 laravel 游标方法有什么区别。哪种方法更适合使用?他们两个的用例是什么?我知道您应该使用游标来节省内存,但它在后端实际上是如何工作的?

详细的示例说明会很有用,因为我在 stackoverflow 和其他网站上进行了搜索,但没有找到太多信息。

这是来自 laravel 文档的代码 sn-p。

分块结果

Flight::chunk(200, function ($flights) {
    foreach ($flights as $flight) {
        //
    }
});

使用光标

foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
    //
}

【问题讨论】:

  • 来自api docschunk:将查询结果分块。 光标:获取给定查询的生成器。
  • 看看here 解释得很好:)

标签: php laravel large-data database-cursor


【解决方案1】:

我们有一个比较:chunk() vs cursor()

  • cursor():高速
  • chunk():恒定的内存使用率

10,000 条记录

+-------------+-----------+------------+
|             | Time(sec) | Memory(MB) |
+-------------+-----------+------------+
| get()       |      0.17 |         22 |
| chunk(100)  |      0.38 |         10 |
| chunk(1000) |      0.17 |         12 |
| cursor()    |      0.16 |         14 |
+-------------+-----------+------------+

100,000 条记录

+--------------+------------+------------+
|              | Time(sec)  | Memory(MB) |
+--------------+------------+------------+
| get()        |        0.8 |     132    |
| chunk(100)   |       19.9 |      10    |
| chunk(1000)  |        2.3 |      12    |
| chunk(10000) |        1.1 |      34    |
| cursor()     |        0.5 |      45    |
+--------------+------------+------------+
  • TestData:Laravel 默认迁移的用户表
  • 宅基地 0.5.0
  • PHP 7.0.12
  • MySQL 5.7.16
  • Laravel 5.3.22

【讨论】:

  • 你知道为什么块的内存使用率低于游标吗?这对我来说似乎有点奇怪。
  • @AnttiPihlaja 我认为这是因为cursor() 仍将结果集(100k 记录)保留在内存中并按需获取行作为对象(使用PDOStatement::fetchchunk() 使用LIMITOFFSET 使用PDOStatement::fetchAll 限制结果集大小并将整个结果集加载到每个块/查询(10k 行)的内存中。
  • @IonBazan 是的。但这对于 db 游标来说是非常出乎意料的行为。原因是 Laravel 将底层 PDO 连接配置为这样。
  • 似乎使用 cursor 总是比 get() 好,但事实并非如此。对于较大的数据集,游标性能比 get() 慢,因为游标使用 fetch 一次从缓冲区中获取一条记录,而 get 使用 fetchAll 返回所有内容。 fetchAll 已被证明比通过 fetch 循环更快。
  • @BernardWiesner 你可以测试你的场景并更新答案。
【解决方案2】:

确实,这个问题可能会吸引一些固执己见的答案,但简单的答案在Laravel Docs

仅供参考:

这是块:

如果您需要处理数千条 Eloquent 记录,请使用 chunk 命令。 chunk 方法将检索 Eloquent 模型的“块”,将它们提供给给定的 Closure 进行处理。使用chunk 方法将在处理大型结果集时节省内存:

这是光标:

cursor 方法允许您使用游标遍历数据库记录,该游标将只执行单个查询。在处理大量数据时,可以使用cursor方法来大大减少你的内存使用:

Chunk 从数据库中检索记录,并将其加载到内存中,同时在检索到的最后一条记录上设置游标,这样就不会发生冲突。

所以这里的好处是如果你想在发送之前重新格式化记录,或者你想每次对第n个记录执行操作,那么这很有用。一个例子是,如果您正在构建一个视图输出/excel 表,那么您可以将记录计数直到它们完成,这样它们就不会一次加载到内存中,从而达到内存限制。

光标使用 PHP 生成器,您可以查看 php generators 页面,但这里有一个有趣的标题:

生成器允许您编写使用foreach 来迭代一组数据的代码,而无需在内存中构建数组,这可能会导致您超出内存限制,或者需要大量的处理时间来产生。相反,您可以编写一个生成器函数,它与普通的function 相同,除了returning 一次之外,生成器可以根据需要多次yield,以便将值提供给被迭代。

虽然我不能保证我完全理解 Cursor 的概念,但对于 Chunk,chunk 以每个记录大小运行查询,检索它,并将其传递到闭包中以进一步处理记录。

希望这是有用的。

【讨论】:

  • 感谢您的诚实回答。虽然仍然,我不完全理解光标的概念。但你的回答说明了很多事情。
  • 如果它可以帮助你更好地理解,Laravel 的 select 使用 PHP 的 fetchAll 而 Laravel 的 cursor 使用 PHP 的 fetch。两者都执行相同数量的 SQL,但前者立即用整个数据构建一个数组,而后者一次获取一行数据,允许仅在内存中保存这一行,而不是前一行或后一行。
【解决方案3】:

Cursor()

  • 只有一个查询
  • 通过调用PDOStatement::fetch()获取结果
  • 默认情况下使用缓冲查询并一次获取所有结果。
  • 仅将当前行转换为 eloquent 模型

优点

  • 最小化 eloquent 模型内存开销
  • 易于操作

缺点

  • 巨大的结果导致内存不足
  • 缓冲或非缓冲是一种权衡

Chunk()

  • 将查询分块到带有限制和偏移量的查询中
  • 通过调用PDOStatement::fetchAll获取结果
  • 将结果批量转化为雄辩的模型

优点

  • 可控的已用内存大小

缺点

  • 将结果批量转换为 eloquent 模型可能会导致一些内存开销
  • 查询和内存使用是一个权衡

TL;DR

我曾经认为 cursor() 每次都会做查询,并且只在内存中保留一行结果。所以当我看到@mohammad-asghari 的比较表时,我真的很困惑。它一定是幕后的一些缓冲区

通过跟踪 Laravel 代码如下

/**
 * Run a select statement against the database and returns a generator.
 *
 * @param  string  $query
 * @param  array  $bindings
 * @param  bool  $useReadPdo
 * @return \Generator
 */
public function cursor($query, $bindings = [], $useReadPdo = true)
{
    $statement = $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
        if ($this->pretending()) {
            return [];
        }

        // First we will create a statement for the query. Then, we will set the fetch
        // mode and prepare the bindings for the query. Once that's done we will be
        // ready to execute the query against the database and return the cursor.
        $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
                          ->prepare($query));

        $this->bindValues(
            $statement, $this->prepareBindings($bindings)
        );

        // Next, we'll execute the query against the database and return the statement
        // so we can return the cursor. The cursor will use a PHP generator to give
        // back one row at a time without using a bunch of memory to render them.
        $statement->execute();

        return $statement;
    });

    while ($record = $statement->fetch()) {
        yield $record;
    }
}

我理解 Laravel 是通过包装 PDOStatement::fetch() 来构建这个功能的。 通过搜索buffer PDO fetchMySQL,我找到了这个文档。

https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php

查询默认使用缓冲模式。这意味着查询结果会立即从 MySQL Server 传输到 PHP,然后保存在 PHP 进程的内存中。

所以通过执行 PDOStatement::execute() 我们实际上获取 整个结果行存储在内存中,而不仅仅是一行。所以如果结果太大,就会导致内存不足异常。

虽然显示的文档我们可以使用$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); 来摆脱缓冲查询。但缺点应该是谨慎。

未缓冲的 MySQL 查询执行查询,然后在数据仍在 MySQL 服务器上等待获取时返回资源。这在 PHP 端使用较少的内存,但会增加服务器的负载。除非从服务器获取完整的结果集,否则不能通过同一连接发送进一步的查询。无缓冲查询也可以称为“使用结果”。

【讨论】:

  • 很好的解释。我对游标如何导致大型数据集上的内存不足问题感到困惑。你的回答对我很有帮助。
【解决方案4】:

chunk是基于分页的,它维护一个页码,并为你做循环。

例如DB::table('users')->select('*')->chunk(100, function($e) {})会进行多次查询,直到结果集小于块大小(100):

select * from `users` limit 100 offset 0;
select * from `users` limit 100 offset 100;
select * from `users` limit 100 offset 200;
select * from `users` limit 100 offset 300;
select * from `users` limit 100 offset 400;
...

cursor 基于PDOStatement::fetch 和生成器。

$cursor = DB::table('users')->select('*')->cursor()
foreach ($cursor as $e) { }

会发出一个查询:

select * from `users`

但驱动程序不会立即获取结果集。

【讨论】:

    【解决方案5】:

    游标方法使用惰性集合,但只运行一次查询。

    https://laravel.com/docs/6.x/collections#lazy-collections

    但是,查询构建器的游标方法返回一个 LazyCollection 实例。这允许您仍然只对数据库运行一个查询,而且一次只在内存中加载一个 Eloquent 模型。

    Chunk 多次运行查询,一次将块的每个结果加载到 Eloquent 模型中。

    【讨论】:

      【解决方案6】:

      假设您在 db 中有一百万条记录。 可能这会给出最好的结果。 你可以使用类似的东西。这样,您将使用分块的 LazyCollections。

      User::cursor()->chunk(10000);
      

      【讨论】:

        【解决方案7】:

        最好看一下源代码。

        select() 或 get()

        https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Connection.php#L366

        return $statement->fetchAll();
        

        它使用fetchAll 将所有记录加载到内存中。这速度很快,但会消耗大量内存。

        光标()

        https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Connection.php#L403

        while ($record = $statement->fetch()) {
           yield $record;
        }
        

        它使用fetch,它一次只将 1 条记录从缓冲区加载到内存中。请注意,它只执行一个查询。内存较低但速度较慢,因为它会一一迭代。 (请注意,根据您的 php 配置,缓冲区可以存储在 php 端或 mysql 上。阅读更多here

        chunk()

        https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Concerns/BuildsQueries.php#L30

        public function chunk($count, callable $callback)
        {
            $this->enforceOrderBy();
            $page = 1;
            do {
                $results = $this->forPage($page, $count)->get();
                $countResults = $results->count();
        
                if ($countResults == 0) {
                    break;
                }
        
                if ($callback($results, $page) === false) {
                    return false;
                }
        
                unset($results);
        
                $page++;
            } while ($countResults == $count);
        
            return true;
        }
        

        使用许多较小的 fetchAll 调用(通过使用 get()),并尝试通过使用 limit 根据您指定的块大小将大查询结果分解为较小的查询来保持低内存。在某种程度上它试图利用 get() 和 cursor() 的好处。

        根据经验,如果可以的话,我会说使用块,或者甚至更好的块。 (chunk 在大表上性能很差,因为它使用 offset,chunkBy id 使用 limit)。

        懒惰()

        在 laravel 8 中也有lazy(),它类似于块,但语法更简洁(使用生成器)

        https://laravel.com/docs/8.x/eloquent#streaming-results-lazily

        foreach (Flight::lazy() as $flight) {
            //
        }
        

        In 和 chunk() 一样,只是你不需要回调,因为它使用 php Generator。你也可以使用类似于块的lazyById()。

        【讨论】:

          【解决方案8】:

          我使用光标和位置做了一些基准测试

          foreach (\App\Models\Category::where('type','child')->get() as $res){
          
          }
          
          foreach (\App\Models\Category::where('type', 'child')->cursor() as $flight) {
              //
          }
          
          return view('welcome');
          

          结果如下:

          【讨论】:

          猜你喜欢
          • 2014-05-02
          • 2019-03-27
          • 2015-06-26
          • 2015-07-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-07-27
          • 2016-10-01
          相关资源
          最近更新 更多