Выборка Activerecord По Частям

Стали внедрять на проекте Yii. Переписали основные модели на AR. Все супер, рутинного кода ушло сразу очень много и появилась большая гибкость в разнообразных выборках.

Но у нас по крону ночью должно обрабатываться около 100 тысяч записей из БД.

Если делать выборку такого числа записей с помощью AR, то это сжирает больше гигабайта оперативки и вообще как-то не хорошо, ведь findAll() сначала выберет всю инфу, потом нагенерирует все 100 тыс. моделей и все это будет жить в памяти.

В голову приходит несколько вариантов оптимизации.

  1. Отказаться в системных операциях от AR, написать запросы вручную, обработать вручную. Будет относительно экономично, но теряется вся прелесть AR.

  2. Выбирать по частям (например, с LIMIT или с BETWEEN ID), обрабатывать по частям. Тогда за раз будет создаваться только некоторая часть моделей, которые в цикле будут обработаны и потом убиты сборщиком мусора. В итоге пиковое потребление памяти упадет.

Есть ли в Yii уже какие-либо готовые способы решения задачи?

Скорее всего лучше будет обрабатывать по частям используя LIMIT.

А в чем состоит обработка? Можно ведь получать данные в виде массива (без создания AR).

Там обработка пользователей с выборкой по многим условиям и relations. Причем все условия, фильтры и параметры заданы, как и положено, во всевозможных scope.

Или я не правильно Вас понял?

Ну а кто вас заставляет делать findAll()? - это во первых.

Во вторых AR не предназначен для highload, он изначально в разы медленнее и прожорливее. Делайте с помощью DAO или просто в массив queryAll(). Если вы обрабатываете 100к записей за ночь - это значит что нужно было думать хорошенько перед тем как цеплять все к AR.

Тут вступает в силу требования которые вам нужны:

а) скорость и оптимальная нагрузка - DAO или просто выборка данных в массив.

б) меньше скорости и больше памяти - AR и выборка в массив.

в) меньше скорости и минимальные затраты памяти - выборка по 1 записи с помощью DAO.

г) самая маленькая скорости и небольшие затраты памяти - выборка по 1 записи AR.

Сам совершил похожую ошибку в прошлом проекте. Пришлось перековырять половину AR для оптимизаций, с некоторыми таблицами пришлось совсем убрать. В тяжелых крон задачах - AR нет, кроме одной по сбору статистики, которая выполняется 1 раз в день приблизительно за 2 минуты с выборкой findAll() ~80к записей. Почему - пока не дошли руки до нее.

Чем выше уровень абстракции - тем больше затраты ресурсов.

Да и кстати 1гб это очень много, мой скрипт сбора статистики с выборкой на 80к и последующей обработкой с выборкой еще из 2 таблиц укладывается в 150-200 мб.

Оно и понятно. Я с Вами не спорю.

Задача выполняется по крону раз в сутки и время ее выполнения не очень критично. Нужно просто, что бы она не клала сервер на время своего выполнения.

И если я обрабатываю 100к записей за ночь - это не значит, что нужн оотказаться от AR в проекте. 99% операций в системе происходят 1-2 записями и их связями, что явно укладывается в AR.

А есть ли способ на основе Scopes в AR создать кастомную выборку?

Самое большое, чего я опасаюсь - это дублирования кусков SQL по всему проекту. В этом случае правка бизнес-логики превращается в ад и именно решения этой проблемы ждешь от AR.

По поводу 1Гб - там просто много колонок нужно выбирать в каждой таблице, которая связана с моделью.

Дробите на куски - уменьшите потребление памяти.

Проведите эксперимент с выборкой по 1 - через find(). В этом случае нагрузка перейдет к БД, посмотрите устроит ли вас это.

По поводу scopes - что значит кастомная выборка? Вы записываете туда то что часто используете - например статус или роль, и потом делаете выборку по типу:




$users=User::model()->status()->role()->findAll();



Для меня это и есть уже кастомной выборкой. Тут вы действительно избегаете дублирования, например если изменятся константы статуса - не нужно их менять везде.

Если честно я предпочитаю немного другие методы для избежания таких проблем. Я делаю мелкие функции в моделях для выборок из БД. Но это личное мнение.

Да и еще по личному опыту - избегайте join-ов, особенно на больших таблицах - они часто и есть причина непомерного расходования памяти и времени.

Бд сейчас на старом проэкте выросла до 20 гб, и 50 миллионов записей в 1 таблице. Это сравнительно немного, но когда нативный AR уже на 30м начинает захлебываться и делать выборки по 5 минут - заставляет задуматься об его уместности.

Да, я измеряю сейчас производительность и думаю, что найду золотую середину для конкретной задачи.

Собственно я спрашивал, есть ли в yii, например что-то готовое для реализации, например "постраничной" выборки. Как-то так:




$ModelIterator = Model::model()->mySuperScopes()->findAllLimited(200); // вернет итератор, возвращающий по 200 моделей

foreach($ModelIterator as $models) {

    // Получили новый кусок моделей и работаем с ними

}



Да, Scopes примерно так используются, только внтури более сложные условия и scope с параметрами.

Получается практически тоже самое, что делать статические методы вида User::getNeededForMeUsers();

Только у scope преимущество - позволяет использовать себя в качестве параметров при выборке relations, что очень гибко

Я всегда проверяю все запросы, которые генерирует AR вручную с помощью EXPLAIN и слежу, что бы везде джойны были по индексам. Так что это не слишком большая проблема. И я всегда представляю, какой запрос сгенерирует AR ;)

Ну что касается 50 миллионов записей… выбирать 30 миллионов с помощью AR точно не следует =)

Пользуясь случаем, попеарю Yii2, в котором можно так:


$list = MyModel::find()->select(array('id', 'name')->where(..)->limit(...)->asArray()->all();

И никакого AR! :)

К великому моему сожалению, Yii2, как говорится, сейчас not for production use ;(

Кстати, заметил, что если я выберу через голое PDO те же 100к записей и , например, сохраню все строки в массив, то это займет ~600 Mb. Отсюда вывод, что большую часть оперативки жрет PDO, который хранит в себе полный результат выборки из мускуля.

Ну да. Это значит что ваше полотно с джоинами немного больше структур activerecord :rolleyes:

Если вам скорость неважна делайте выборку по 1. Нагрузка на БД должна быть приемлима, а затраты памяти минимальны.

Yii 2 действительно сыроват.

Ранее я писал, что хорошо бы, что б был итератор по моделям:

О боже мой! Оказывается в версии 1.13 это было сделано. Разработчики yii опередили меня =(

Кто столкнется с подобной задачей, велкам: http://yiiframework.ru/doc/cookbook/ru/model.data.provider.iterator

Плюс к итераторам - если в запросах не используются джоины их можно легко превратить в sql и получить результат в виде простого массива, что конечно быстрее и ест меньше памяти.

Я надеюсь, что все понимают разницу между "… LIMIT 100" и "… LIMIT 99900, 100" для MySQL.