Пакеты и чанки
insertBatch() и upsertBatch() превращают большой массив строк в несколько
многострочных запросов вместо одного запроса на строку. Эта страница объясняет,
как эти запросы строятся — группировку по форме, чанкование и суффиксацию
плейсхолдеров на каждую строку. Всё описанное работает одинаково на PostgreSQL,
MySQL, MariaDB и SQLite.
Зачем вообще чанковать
Базы данных ограничивают число связанных параметров и размер пакета одного
запроса. Наивный «один гигантский INSERT» на 100 тыс. строк вылетел бы за оба
предела. Чанкование выдаёт по одному многострочному запросу на пакет
фиксированного размера, держа каждый запрос комфортно внутри лимитов.
| Метод | chunkSize по умолчанию |
|---|---|
insertBatch |
1000 |
upsertBatch |
500 |
Пустой вход коротко замыкается в no-op ещё до построения любого SQL.
Потоковая вставка: генератор вместо массива
Оба метода принимают iterable, а не array. Разница практическая: строки
не накапливаются целиком — метод идёт по входу, копит их в буфере и отправляет
запрос, как только буфер заполнился. Поэтому пиковая память определяется размером
чанка, а не размером задачи.
Это значит, что миллион строк можно вставить, ни разу не собрав их в массив:
$inserted = $cdo->insertBatch('users', (function () use ($csv) {
while (($line = fgetcsv($csv)) !== false) {
yield ['name' => $line[0], 'email' => $line[1]];
}
})());Тот же приём работает с любым генератором — построчным чтением файла, курсором по другой таблице, ответами внешнего API постранично. Массив тоже принимается: он просто уже целиком в памяти к моменту вызова.
Точная граница памяти
Буфер заводится на каждую форму строки (см. группировку ниже), поэтому предел —
chunkSize × число различных форм, а не просто chunkSize. При однородных строках
форма одна и разницы нет; при сильно разнородных стоит либо уменьшить chunkSize,
либо подавать строки одной формы подряд.
Возвращаемые значения
insertBatch() возвращает int — общее число вставленных строк, которое можно
захватить как $inserted = $cdo->insertBatch(...). Для обычного INSERT это
число сообщается одинаково на PostgreSQL, MySQL, MariaDB и SQLite.
upsertBatch() возвращает void намеренно: число затронутых строк апсерта не
сопоставимо между драйверами (MySQL/MariaDB считают 2 на обновлённую строку,
PostgreSQL — 1, а IGNORE / DO NOTHING считают только реальные вставки), поэтому
стабильного числа для возврата нет.
Суффиксация плейсхолдеров
Внутри одного чанка каждой строке нужны собственные плейсхолдеры — нельзя
привязать :name дважды с разными значениями. Поэтому ключи каждой строки
суффиксируются индексом строки в чанке (_0, _1, …):
строки: [{name:'A', email:'a@x'}, {name:'B', email:'b@x'}]
columns: (name, email)
values: (:name_0, :email_0), (:name_1, :email_1)
binds: :name_0 => 'A', :email_0 => 'a@x',
:name_1 => 'B', :email_1 => 'b@x'Список колонок берётся из ключей строки; секция значений повторяет один
суффиксированный кортеж на строку; все привязки сливаются в один execute.
upsertBatch строит те же кортежи значений, затем дописывает
драйвер-специфичную секцию конфликта — синтаксис ON CONFLICT в стиле PostgreSQL
на PostgreSQL и SQLite, ON DUPLICATE KEY UPDATE / INSERT IGNORE на
MySQL/MariaDB (см.
Определение драйвера).
Группировка строк по форме
У порождённого INSERT один список колонок, построенный из строк в чанке, и
суффиксированные плейсхолдеры каждой строки выдаются в VALUES. Чтобы запрос
сошёлся, все строки в чанке должны иметь одни и те же колонки.
Обеспечивать это вручную не нужно. Перед чанкованием оба метода группируют вход
по сигнатуре колонок каждой строки — отсортированному набору её ненулевых
колонок. Строки с одинаковой сигнатурой объединяются в один многострочный запрос;
строки другой формы образуют собственный запрос. Это повторяет поведение
@DynamicInsert из Hibernate.
Группировка исправляет баг, при котором один чанк со строками с разными паттернами
null порождал несовпадение числа колонок и значений — некорректный SQL вида
(a, b) VALUES (1, 2), (3). Теперь каждый запрос видит только строки одной формы,
поэтому список колонок и каждый кортеж значений всегда сходятся.
Строки группируются по форме
Поскольку строки группируются по сигнатуре, они переупорядочиваются — все строки первой формы записываются раньше следующей. Для обычной пакетной вставки это безвредно, но не полагайтесь на то, что автоинкрементные id идут в порядке вашего входного массива, когда строки имеют разную форму.
Поштучное отбрасывание NULL
Как и в одиночном insert(), значение null удаляется из этой строки — колонка
падает к значению по умолчанию из базы. Именно это определяет сигнатуру строки:
две строки с разными null-колонками имеют разные формы и попадают в разные
группы (а значит, в разные запросы).
Строка со всеми NULL отклоняется
Строка, все колонки которой равны null, не содержит ничего для вставки и бросает
CDOException.
Компромиссы размера чанка
- Меньшие чанки — меньше памяти на запрос, больше обращений к базе, безопаснее для очень широких строк (много колонок × много строк быстрее подходит к пределу параметров).
- Большие чанки — меньше обращений, выше пропускная способность, больше памяти и больше связанных параметров на запрос.
Отталкивайтесь от умолчаний, если строки необычно широкие или узкие. Грубый
ориентир: держите chunkSize × колонокНаСтроку заметно ниже лимита связанных
параметров вашего драйвера.
Атомарность
Каждый чанк — это отдельный запрос. insertBatch / upsertBatch не открывают
транзакцию поверх чанков — если пятый чанк упадёт, первые четыре уже
зафиксированы. Когда нужна семантика «всё-или-ничего» на весь пакет, оберните
вызов в transaction(): он открывает транзакцию, коммитит по возвращении из
замыкания и откатывает при исключении.
$cdo->transaction(function () use ($cdo, $rows) {
$cdo->insertBatch('users', $rows);
});Транзакция удерживает всё до конца
Транзакция на весь пакет означает, что база держит блокировки и незафиксированные данные до последнего чанка. Для по-настоящему больших загрузок это может оказаться дороже, чем возможность докатить сбойный кусок: выбирайте осознанно, а не по умолчанию.
Связанное
- Вставка записей — руководство на уровне задачи
- Апсерты — использование пакетного апсерта
- Определение драйвера — секция конфликта по драйверам
- Привязка параметров — как типизируется каждое суффиксированное значение