¿Cuál es la diferencia entre el cursor de laravel y el método de fragmento de laravel?

11 minutos de lectura

avatar de usuario
Suraj

Me gustaría saber cuál es la diferencia entre laravel chunk y el método de cursor laravel. ¿Qué método es más adecuado para usar? ¿Cuáles serán los casos de uso para ambos? Sé que deberías usar el cursor para ahorrar memoria, pero ¿cómo funciona realmente en el backend?

Sería útil una explicación detallada con un ejemplo porque he buscado en stackoverflow y otros sitios, pero no encontré mucha información.

Aquí está el fragmento de código de la documentación de laravel.

Resultados de fragmentación

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

Uso de cursores

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

  • desde el documentos api: pedazo: fragmentar los resultados de la consulta. cursor: Obtenga un generador para la consulta dada.

    – en línea Tomás

    2 de agosto de 2017 a las 15:16


  • Echar un vistazo aquí esta bien explicado 🙂

    – Maraboc

    2 de agosto de 2017 a las 15:19

avatar de usuario
seyed mohammad asghari

Tenemos una comparación: trozo() vs cursor()

  • cursor (): alta velocidad
  • chunk (): uso de memoria constante

10,000 registros:

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

100.000 registros:

+--------------+------------+------------+
|              | 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: tabla de usuarios de la migración predeterminada de Laravel
  • Granja 0.5.0
  • PHP 7.0.12
  • MySQL 5.7.16
  • Laravel 5.3.22

  • ¿Tiene alguna idea de por qué los fragmentos tienen un uso de memoria más bajo que el cursor? Eso me parece un poco extraño.

    – Antti Pihlaja

    21 de febrero de 2019 a las 11:52

  • @AnttiPihlaja Creo que esto es porque cursor() todavía mantiene el conjunto de resultados (100k registros) en la memoria y obtiene las filas como objetos a pedido (usando PDOStatement::fetch. chunk() usos LIMIT y OFFSET para limitar el tamaño del conjunto de resultados y cargar todo el conjunto de resultados en la memoria para cada fragmento/consulta (10k filas) usando PDOStatement::fetchAll.

    – Ion Bazán

    31 de julio de 2019 a las 7:49

  • @IonBazan Sí. Pero ese es un comportamiento muy inesperado para el cursor db. La razón es que Laravel configura la conexión PDO subyacente para que se comporte así.

    – Antti Pihlaja

    2 de agosto de 2019 a las 3:38

  • parece que usar el cursor siempre es mejor que get(), pero esto no es cierto. El rendimiento del cursor es más lento que el de get() con conjuntos de datos más grandes, porque el cursor obtiene los registros del búfer de uno en uno usando fetch, mientras que get devuelve todo usando fetchAll. Se ha demostrado que fetchAll es más rápido que recorrer en bucle fetch.

    –Bernardo Wiesner

    18 de noviembre de 2021 a las 5:45

  • @BernardWiesner puede probar sus escenarios y actualizar la respuesta.

    – seyed mohammad asghari

    19 de noviembre de 2021 a las 7:48

avatar de usuario
Oluwatobi Samuel Omisakin

De hecho, esta pregunta podría atraer alguna respuesta obstinada, sin embargo, la respuesta simple está aquí en Documentos de Laravel

Solo para referencia:

Este es un trozo:

Si necesita procesar miles de registros Eloquent, use el chunk dominio. los chunk recuperará un “trozo” de modelos Eloquent, alimentándolos a un determinado Closure para procesar. Utilizando el chunk El método conservará la memoria cuando trabaje con grandes conjuntos de resultados:

Este es Cursor:

los cursor El método le permite iterar a través de los registros de su base de datos usando un cursor, que solo ejecutará una sola consulta. Al procesar grandes cantidades de datos, el cursor El método puede usarse para reducir en gran medida el uso de la memoria:

Chunk recupera los registros de la base de datos y los carga en la memoria mientras coloca un cursor en el último registro recuperado para que no haya conflictos.

Así que la ventaja aquí es que si desea volver a formatear el largo registro antes de que se envíen, o si desea realizar una operación en un número n de registros por vez, entonces esto es útil. Un ejemplo es si está creando una vista de salida/hoja de Excel, por lo que puede tomar el registro en conteos hasta que finalicen para que no se carguen todos en la memoria a la vez y, por lo tanto, alcancen el límite de memoria.

El cursor usa generadores de PHP, puede verificar el generadores php página sin embargo aquí hay un título interesante:

Un generador le permite escribir código que utiliza para cada para iterar sobre un conjunto de datos sin necesidad de crear una matriz en la memoria, lo que puede hacer que exceda un límite de memoria o requiera una cantidad considerable de tiempo de procesamiento para generar. En su lugar, puede escribir una función de generador, que es lo mismo que una función normal funciónexcepto que en lugar de devolveruna vez, un generador puede rendir tantas veces como sea necesario para proporcionar los valores que se van a iterar.

Si bien no puedo garantizar que entiendo completamente el concepto de Cursor, pero para Chunk, chunk ejecuta la consulta en cada tamaño de registro, recuperándolo y pasándolo al cierre para trabajos adicionales en los registros.

Espero que esto sea útil.

  • Gracias por la respuesta honesta. Aunque todavía, no entiendo completamente el concepto de cursor. Pero tu respuesta explica muchas cosas.

    – Suraj

    2 de agosto de 2017 a las 16:41

  • Si te puede ayudar a entender mejor, Laravel’s select usa PHP fetchAll mientras que el de Laravel cursor usa PHP fetch. Ambos ejecutan la misma cantidad de SQL, pero el primero crea inmediatamente una matriz con todos los datos, mientras que el segundo obtiene los datos una fila a la vez, lo que permite mantener en la memoria solo esta fila, no la anterior ni la siguiente.

    – Hierba Doble

    5 de diciembre de 2018 a las 5:28

avatar de usuario
劉恒溫

Cursor()

  • solo una sola consulta
  • obtener resultado por llamada PDOStatement::fetch()
  • por defecto, se utiliza la consulta almacenada en búfer y se obtienen todos los resultados a la vez.
  • convirtió solo la fila actual en un modelo elocuente

ventajas

  • minimizar la sobrecarga de memoria del modelo elocuente
  • fácil de manipular

Contras

  • grandes resultados conduce sin memoria
  • amortiguado o no amortiguado es una compensación

Chunk()

  • fragmentar la consulta en consultas con límite y compensación
  • obtener resultado por llamada PDOStatement::fetchAll
  • convirtió los resultados en modelos elocuentes por lotes

ventajas

  • tamaño de memoria usada controlable

Contras

  • convertir los resultados en modelos elocuentes por lotes puede causar cierta sobrecarga de memoria
  • las consultas y el uso de la memoria son una compensación

TL;DR

solía pensar cursor() hará una consulta cada vez y solo mantendrá el resultado de una fila en la memoria. Así que cuando vi la tabla de comparación de @mohammad-asghari me confundí mucho. debe ser algo buffer entre bastidores.

Al rastrear el código Laravel como se muestra a continuación

/**
 * 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;
    }
}

Entendí que Laravel construye esta característica por envoltura. PDOStatement::fetch(). y por búsqueda recuperación de PDO de búfer y mysqlencontré este documento.

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

Las consultas utilizan el modo de búfer de forma predeterminada. Esto significa que los resultados de la consulta se transfieren inmediatamente del servidor MySQL a PHP y luego se guardan en la memoria del proceso de PHP.

entonces al hacer PDOStatement::execute() en realidad obtenemos filas de resultados completos en unos y almacenado en la memoria, no solo una fila. Entonces, si el resultado es demasiado grande, esto conducir a fuera de la memoria excepción.

Aunque el Documento mostrado podríamos usar $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); para deshacerse de la consulta almacenada en búfer. Pero el inconveniente debe ser la precaución.

Las consultas MySQL sin búfer ejecutan la consulta y luego devuelven un recurso mientras los datos aún están esperando en el servidor MySQL para ser recuperados. Esto usa menos memoria en el lado de PHP, pero puede aumentar la carga en el servidor. A menos que el conjunto de resultados completo se obtenga del servidor, no se pueden enviar más consultas a través de la misma conexión. Las consultas sin búfer también se pueden denominar “resultado de uso”.

  • muy buena explicacion Estaba confundido acerca de cómo el cursor conduciría a un problema de falta de memoria en un conjunto de datos grande. Tu respuesta realmente me ayudó.

    – Anuj Shrestha

    18 de abril de 2020 a las 7:17

chunk se basa en la paginación, mantiene un número de página y realiza el bucle por usted.

Por ejemplo, DB::table('users')->select('*')->chunk(100, function($e) {}) hará varias consultas hasta que el conjunto de resultados sea más pequeño que el tamaño del fragmento (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 está basado en PDOStatement::fetch y Generador.

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

emitirá una sola consulta:

select * from `users`

Pero el controlador no obtiene el conjunto de resultados de inmediato.

El método del cursor usa Lazy Collections, pero solo ejecuta la consulta una vez.

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

Sin embargo, el método de cursor del generador de consultas devuelve una instancia de LazyCollection. Esto le permite ejecutar solo una sola consulta en la base de datos, pero también mantener solo un modelo Eloquent cargado en la memoria a la vez.

Chunk ejecuta la consulta varias veces y carga cada resultado del fragmento en modelos Eloquent a la vez.

avatar de usuario
oguz463

Suponiendo que tiene un millón de registros en db. Probablemente esto daría el mejor resultado. Puedes usar algo así. Con eso, usará LazyCollections fragmentadas.

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

Lo mejor es echar un vistazo al código fuente.

seleccionar() u obtener()

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

return $statement->fetchAll();

Usa buscar todo que carga todos los registros en la memoria. Esto es rápido pero consume mucha memoria.

cursor()

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

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

Usa buscar, carga solo 1 registro en la memoria desde el búfer a la vez. Sin embargo, tenga en cuenta que solo ejecuta una consulta. Menos memoria pero más lento, ya que itera uno a uno. (Tenga en cuenta que dependiendo de su configuración de php, el búfer puede almacenarse en el lado de php o en mysql. Leer más aquí)

pedazo()

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;
}

Usa muchas llamadas más pequeñas de fetchAll (usando get()) e intenta mantener la memoria baja dividiendo el resultado de una consulta grande en consultas más pequeñas usando límite dependiendo del tamaño del fragmento que especifique. De alguna manera, está tratando de usar el beneficio de get() y cursor().

Como regla general, diría que vaya con chunk, o incluso mejor con chunkById si puede. (chunk tiene un mal rendimiento en mesas grandes ya que usa compensarchunkBy id utiliza límite).

perezoso()

En laravel 8 también hay lazy(), es similar a chunk pero la sintaxis es más limpia (usa generadores)

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

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

Hace lo mismo que chunk(), solo que no necesita una devolución de llamada, ya que usa php Generator. También puede usar lazyById() similar a chunk.

¿Ha sido útil esta solución?