<rmcreative>

RSS

Yii2: batch

15 февраля 2014

При работе с большим количеством данных важно не использовать слишком много памяти. Сегодня Yii2 обзавёлся решением. Работает за раз не со всеми данными, а частями:

use yii\db\Query;
 
$query = (new Query)
    ->from('tbl_user')
    ->orderBy('id');
 
 
foreach ($query->each() as $user) {
    // $user — одна строка из tbl_user
}
 
foreach ($query->batch(10) as $users) {
    // $users — массив из 10 строк
}

То же работает с Active Record:

foreach (Customer::find()->batch() as $customers) {
    // $customers — массив из 10 или менее объектов Customer
}

Комментарии RSS

  1. №8809
    mmx
    mmx 15 февр. 2014 г., 21:36:02

    В последнем примере полагаю должно быть ->batch(10) ?

  2. №8810
    ORey
    ORey 15 февр. 2014 г., 22:22:30

    Да, это мегатема вообще.

    Кстати, не стоит ли такие штуки анонсировать в (прикрепленной?..) ветке англофорума? Ну, я понимаю что пока бета и всё такое, но все нововведения, в основном, втихую проходят.

    Те, кто на v1 до сих пор сидит - они же за гитхабом не следят особо: ну коммит и коммит, релиза нет же, значит сидим дальше ровно. А так народ будет видеть, что движуха крутейшая происходит, и всерьез задумаются об апгрейде до v2.

  3. №8811
    porcelanosa
    porcelanosa 15 февр. 2014 г., 22:39:11

    ну версию 2 боясно использовать на продакшене, а так слежу - и вот проект джаст фо фан планирую на новой версии делать

  4. №8812
    serg
    serg 15 февр. 2014 г., 23:04:01

    Эм, а может кто объяснить как это работает? Я что-то профита не понимаю...

  5. №8813
    Алексей
    Алексей 16 февр. 2014 г., 1:11:20

    serg, объяснить как работает или в чем профит?

    Как работает, смотри в последние коммиты на гитхаб.

    Профит - а вы SELECT * на табличку больше гигабайта когда делали в последний раз? А именно это и произойдет при работе с ->all()

    Как это решалось раньше: new Paginator -> new DataProvider и оп, вроде бы по памяти мы не течемм, но MySql начинает нам говорить - что мы делаем на каждую "страницу" отдельный запрос, который мускуль честно выполняет. Как это сделано сейчас: MySql выполняет запрос SELECT * но в PDO возвращает указатель на результат, и когда мы просим следующий ->batch() то PDO просто просит у MySql данные со следующей позиции указателя.

    Возможно немного сумбурно, но вроде так.

    Sam, следует ли нам ждать Batch Save?

  6. №8814
    Sam
    Sam 16 февр. 2014 г., 14:27:38

    Алексей, batch insert/update уже есть. Над save думали, но пока ничего нормально не придумали.

  7. №8815
    Александр
    Александр 16 февр. 2014 г., 15:15:42

    А почему было решено сделать именно отдельный метод, вместо параметра в each()?

    Ведь можно было сделать, чтобы each(10) возвращал Iterable объект, который скрывал бы в себе эту логику(отбирал бы по 10 записей и потом по одной бы их возвращал).

    В таком случае переход от полной и batch выборкой делался бы добавлением всего одного числа и не было бы необходимости в еще одном внутреннем цикле.

  8. №8816
    maleks
    maleks 16 февр. 2014 г., 16:52:25

    А возможность выбирать данные порциями нужна для какого то другого функционала фреймворка? Если да, то какого?
    А то не вижу особого профита от этой фичи по сравнению с :

    $query = (new Query)
        ->from('tbl_user')
        ->orderBy('id');
    $datareader = $query->createCommand()->query();
    while ($row = $datareader->read()){
        // если порциями, то проверка на целочисленное деление
    }

    Имхо оберточка над $query->createCommand()->query() больше бы не помешала для Query объекта.

  9. №8817
    Максим
    Максим 16 февр. 2014 г., 21:35:13

    Полезно, спасибо

  10. №8821
    Алексей
    Алексей 17 февр. 2014 г., 21:40:17

    maleks, так это вроде на уровне Query объекта и DataReader и сделано.

  11. №9258
    Андрей
    Андрей 07 окт. 2014 г., 14:02:57

    Алексей, batch insert/update уже есть. Над save думали, но пока ничего нормально не придумали.

    Для insert нашел batchInsert, а для update есть команда для пакетного обновления?

  12. №9264
    Sam
    Sam 08 окт. 2014 г., 0:30:30

    Андрей, для обновления нет потому как сильно специфично для разного рода баз. Например, для MySQL выглядит это так:

    UPDATE mytable
        SET myfield = CASE id
            WHEN 1 THEN 'value1'
            WHEN 2 THEN 'value2'
            WHEN 3 THEN 'value3'
        END 
    WHERE id IN (1,2,3)
  13. №9920
    Евгений
    Евгений 30 июля 2015 г., 6:39:21

    Здравствуйте, при попытки выполнить foreach (Item::find()->each() as $item) {} на табличке с 200 тысячами записей вываливается ошибка о нехватке памяти. Сам цикл ни разу не выполняется. PDO работает, памяти 1гб. Что я могу делать не так?

  14. №9921
    Sam
    Sam 30 июля 2015 г., 22:04:52

    Если разница только в PDO, то скорее всего вы делаете всё так и в Yii где-то недоработка.

  15. №10060
    Саня
    Саня 19 нояб. 2015 г., 17:38:33

    Вобщем, не работает магия batch() для больших резалт сетов. Судя по реализации yii\db\BatchQueryResult - и не должна была.

    Итак, есть таблица users размером 77 МиБ (240000 записей) и следующий код:

    $batch_size = 1000;
    $q = (new \yii\db\Query())->select('*')->from('users');
    foreach ($q->batch($batch_size) as $k) {
        // nothing
    }
    $usage = memory_get_peak_usage(true);
    print round($usage / 1024 / 1024, 2).' МиБ');

    Окружение: MySQL 5.5.44, PHP 5.5.9, memory_limit=128M, Yii 2.0.6

    Результаты запуска:

    #Драйвер$batch_sizeПамять
    1libmysql100021 МиБ
    2libmysql8000Fatal error: memory exhausted
    3mysqlndлюбойFatal error: memory exhausted

    Объяснение простое - buffered results. По умолчанию результаты всех запросов буфферизуются. Это значит, что результат всей выборки сначала целиком передаётся клиенту (в память PHP процесса), а уже потом производится работа с ним.

    Результат запроса хранится вунутри mysql модуля в собственной структуре данных. Разница в том, что в mysqlnd эта структура контролируется менеджером памяти PHP, и поэтому её размер учитывается в memory_get_[peak_]usage() и влияет на достижение memory_limit. Из-за этого #3 валится при любых настройках $batch_size (до цикла foreach выполнение даже не доходит).

    Это, кстати, одна из причин, по которой рекомендуется перейти с mysql на mysqlnd. В противном случае вы получите бомбу замедленного действия, потому что при увеличении размеров таблицы, PHP процесс следом будет потреблять всё больше памяти, но в memory_get_usage() этого видно не будет.

    Экономия памяти начинает работать только при установке настройки:

    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false

    Настройка отключает буфферизацию результатов. Это означает, что актуальные данные будут отправляться mysql сервером только по мере продвижения курсора по result set'у. Поштучно. В этом случае и libmysql, и mysqlnd будут показывать одинаковые результаты по памяти при одинаковых размерах $batch_size. При этом "утечки" памяти при использовании libmysql происходить не будут.

    С другой стороны, это налагает дополнительную нагрузку на mysql сервер и не позволяет совершать другие SQL запросы через данное соединение, не закрыв предварительно наш небуфферизованный результат. Последнее как раз сильно ограничивает меня при переносе одной легаси системы на yii. Старый код работает с буфферизованными результатами и MySQL сервер настраивался на эту модель использования.

    В чём же тогда экономия памяти, предоставляемая yii\db\Query::batch()? По сути, "экономия" происходит только при использовании драйвера libmysql и буфферизованных запросов. Когда только часть результата копируется из result set'а, хранящегося внутри драйвера, в PHP массив.

    Экономия в кавычках, потому что нет никакой экономии. Здесь присутствует утечка памяти и, описанная мной выше, бомба замедленного действия.

    Как верно заметил maleks, фича не особенно полезна. Более того, она сбивает с толку, потому что её описание из The Definitive Guide to Yii 2.0 намекает на автоматическое использование LIMIT/OFFSET окна размером $batch_size. По крайней мере, люди понимают эту фичу именно так, о чем свидетельствуют нагугленные мной треды на форумах и SO.

  16. №10061
    Sam
    Sam 20 нояб. 2015 г., 11:11:51

    Саня, весь смысл в том, что читается одновременно $batch_size элементов. Поставьте в сотню и попробуйте считать 10000 строк. По памяти пройдёт.

  17. №10062
    Саня
    Саня 20 нояб. 2015 г., 12:58:14

    Поясните подробнее, конкретно откуда читается одновременно $batch_size элементов? Из PDOStatement (если снять всю шелуху)?

    В процессе изучения компонентов yii\db я столкнулся с проблемой, описанной на форуме yiiframework.ru. Готового решения не нашел, поэтому пришлось проводить исследование самостоятельно, результаты которого и оформил комментарием к этому блог посту.

    Значение "любой" в приведённой ранее табличке означает действительно любое значение. Хоть 100, хоть 1.

    Попробуйте повторить тест самостоятельно.

    Я не стал бы так подробно распинаться, не считая это упущением фреймворка и не разобравшись с вопросом до конца. Свою изначальную проблему я уже решил другими средствами, однако, надеюсь, что Вы, как коммитер в yii framework, сможете разобраться и исправить сложившуюся ситуацию вокруг batch()/each().

  18. №10063
    yujin1st
    yujin1st 20 нояб. 2015 г., 13:10:41

    В моем случае, спасла последняя версия PDO и PHP, как и написал Александр в том топике. Суть как раз в том что pdo возвращает ссылку на данные, а уже yii выдергивает нужные порции.

  19. №10064
    Саня
    Саня 20 нояб. 2015 г., 13:25:45

    Если Вы явным образом не прописывали PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, то Ваш код обязательно сломается при переходе с libmysqlclient на mysqlnd. Это лишь вопрос времени. Потом будете сидеть и удивляться.

    Прям канонический пример проблемы "почему локально всё работает, а на сервере - нет?" :-D

  20. №10499
    Анатолий
    Анатолий 11 мая 2016 г., 16:44:06

    Саня, сталкивался с такой же проблемой. Решил пока дополнительным циклом для обхода моделей.

  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.