chunk() гортає сторінки через OFFSET. Якщо в циклі змінювати колонку, за якою фільтрується запит, записи «з'їжджають», і частину буде пропущено.
// Погано: після першої порції активних стало на 100 менше,
// а друга порція бере OFFSET 100 - і перескакує через 100 записів
User::where('active', false)->chunk(100, function ($users) {
$users->each->update(['active' => true]);
});
// Добре: курсор за id, а не номер сторінки
User::where('active', false)->chunkById(100, function ($users) {
$users->each->update(['active' => true]);
});
chunkById() бере наступну порцію за id > останній, тож зміни в уже обробленому не впливають.
Як обирати інструмент для великих обсягів:
chunkById()/lazyById()- порціями з постійною пам'яттю;lazyById()віддає LazyCollection, з якою зручніше працювати як з потоком;cursor()- один запит і по одній моделі в пам'яті, але драйвер бази часто буферизує весь результат, а жадібне завантаження зв'язків неможливе;- масовий
update()- якщо логіка вміщається в SQL, один запитUPDATE ... WHEREу сотні разів швидший за будь-який цикл (але без подій моделей).
Що ще важливо на мільйонах рядків:
- обробку виносять у чергу порціями, щоб падіння на 900-тисячному записі не починало все спочатку;
- запит між порціями має йти за індексом;
- вимкнений журнал запитів (
DB::disableQueryLog()), щоб пам'ять не росла.