Импорт больших CSV в PHP: как не упереться в память и таймаут

Импорт большого CSV в PHP часто ломается из-за простого решения: загрузить весь файл в массив, пройти его циклом и в конце записать данные в базу. На маленьком файле это работает. На файле в сотни тысяч строк появляются memory exhausted, timeout, дубли, частично записанные данные и невозможность понять, где импорт остановился.

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

Не читать весь файл сразу

Плохой вариант:

$rows = file('products.csv');

foreach ($rows as $row) {
    // обработка
}

file() загружает весь файл в память. Для больших CSV лучше использовать fgetcsv и читать построчно.

$handle = fopen('products.csv', 'rb');

while (($row = fgetcsv($handle, 0, ';')) !== false) {
    // обработка строки
}

fclose($handle);

Проверить заголовки

Перед импортом нужно проверить, что в файле ожидаемые колонки. Иначе одна перестановка столбцов может испортить данные.

$headers = fgetcsv($handle, 0, ';');

$expected = ['sku', 'name', 'price', 'amount'];

if ($headers !== $expected) {
    throw new RuntimeException('Неверная структура CSV');
}

В реальном проекте можно проверять не строгий порядок, а наличие нужных колонок. Главное — не импортировать файл с неизвестной структурой молча.

Batch-обработка

Записывать каждую строку отдельным INSERT может быть медленно. Лучше собирать пакет и сохранять его пачкой.

$batch = [];
$batchSize = 1000;

while (($row = fgetcsv($handle, 0, ';')) !== false) {
    $batch[] = [
        'sku' => trim($row[0]),
        'name' => trim($row[1]),
        'price' => (float)$row[2],
        'amount' => (int)$row[3],
    ];

    if (count($batch) >= $batchSize) {
        saveBatch($batch);
        $batch = [];
    }
}

if (!empty($batch)) {
    saveBatch($batch);
}

Размер пакета подбирается по проекту. Слишком маленький — медленно, слишком большой — снова риск памяти и долгих блокировок.

Валидация строк

Импорт не должен падать от одной плохой строки, если бизнес допускает пропуск ошибок. Лучше записать ошибку в лог и продолжить.

if ($sku === '') {
    $errors[] = [
        'line' => $lineNumber,
        'error' => 'Пустой артикул',
    ];

    continue;
}

Для критичных импортов можно остановить весь процесс при первой ошибке. Но это должно быть осознанное правило.

Транзакции

Одна большая транзакция на весь файл может держать блокировки слишком долго. Полное отсутствие транзакций может оставить базу в частично испорченном состоянии. Компромисс — транзакция на пакет.

$db->beginTransaction();

try {
    saveBatch($batch);
    $db->commit();
} catch (Throwable $e) {
    $db->rollBack();
    throw $e;
}

Если нужно строго “всё или ничего”, можно использовать отдельную временную таблицу: загрузить данные туда, проверить, затем переключить.

Повторный запуск

Импорт должен быть идемпотентным: повторный запуск не должен создавать дубли. Для товаров обычно нужен стабильный ключ: sku, barcode, external_id или связка supplier_id + article.

INSERT INTO products (sku, name, price)
VALUES (:sku, :name, :price)
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    price = VALUES(price);

Перед этим в базе должен быть уникальный индекс по ключу, который определяет товар.

Лог результата

После импорта нужен понятный итог:

  • сколько строк прочитано;
  • сколько создано;
  • сколько обновлено;
  • сколько пропущено;
  • сколько ошибок;
  • где лежит файл ошибок.

Без такого отчёта сложно понять, импорт прошёл успешно или просто “не упал”.

Чек-лист

  1. Читать CSV через fgetcsv, а не file().
  2. Проверять заголовки и структуру файла.
  3. Обрабатывать строки пакетами.
  4. Валидировать данные перед записью.
  5. Логировать ошибки строк.
  6. Использовать транзакции на разумном уровне.
  7. Добавить уникальный ключ для повторного запуска.
  8. Формировать итоговый отчёт импорта.

Большой импорт должен быть не только быстрым, но и контролируемым. Если понятно, какая строка дала ошибку, сколько данных записано и можно ли безопасно запустить процесс повторно, импорт становится рабочим инструментом, а не аварийной процедурой.

Комментарии (0)

Пока нет комментариев. Будьте первым!