Исходная ситуация
Номенклатура, контрагенты, остатки, история заказов приходят файлом. Excel от клиента, выгрузка из старой системы, таблица от подрядчика. Дальше их нужно загрузить в учётную систему, CRM или базу.
Ход по умолчанию: файл приводят к нужным колонкам и запускают импорт. Дальше два сценария.
Первый — импорт падает. На двухсотой строке загрузчик встречает дату в другом формате и останавливается. Часть строк уже записана, отката нет. Дальше разбор, что успело зайти, ручная чистка, повторный запуск с середины и попытка вспомнить, где именно была середина.
Второй сценарий хуже. Импорт проходит «успешно». Через неделю выясняется, что часть цен уехала на три порядка, потому что в файле разделитель дробной части — запятая, а загрузчик ждал точку. Или что справочник единиц измерения не совпал и штуки стали килограммами. Или что позиции задвоились, потому что в артикулах поставщика были неразрывные пробелы.
Время теряется не на загрузке. Оно теряется на поиске последствий в живой системе, где загруженные данные уже перемешались с рабочими и разделить их нечем.
Что смотрели
Ошибки импорта делятся на три класса, и лечатся они по-разному.
Формальные. Тип не тот, формат даты не тот, обязательное поле пустое, число не парсится. Ловятся тривиально, если проверка вообще есть.
Ссылочные. Значение не находит соответствия в справочнике: категория, единица измерения, контрагент, склад. Загрузчик либо падает, либо молча создаёт новую запись справочника — и справочник разрастается копиями одного и того же.
Смысловые. Формально всё корректно: дата — дата, число — число, ссылка находится. При этом дата поставки раньше даты заказа, остаток отрицательный, одна позиция встречается дважды под разными артикулами, цена отличается от соседних на два порядка. Этот класс переживает любую типовую валидацию и всплывает в отчётности.
Что мерить до того, как что-то менять:
- Долю строк, отвергнутых загрузчиком. Видна сразу.
- Долю строк, которые загрузились и потом правились руками. Ищется по журналу изменений целевой системы: записи, созданные импортом и изменённые в первые дни после. Эта доля обычно больше первой и стоит дороже.
- Число повторных запусков импорта на один файл.
- Время от старта загрузки до момента, когда данным начали доверять.
Обязательный шаг перед написанием правил — профилирование файла. Скрипт проходит по каждой колонке и выдаёт: заполненность, число уникальных значений, топ значений с частотами, минимальную и максимальную длину, распознанные форматы дат и чисел, наличие ведущих и хвостовых пробелов, разнобой регистра. Заполненность меряют по файлу, а не по словам того, кто файл прислал: «там всё заполнено» и реальная доля пустых ячеек встречаются в одном разговоре.
Что поменяли
1. Контракт на файл. Одна страница: имя колонки, тип, обязательность, допустимые значения или справочник, формат, пример корректного значения. Документ отдаётся тому, кто готовит файл, до подготовки, а не после первого падения.
2. Профилирование как первый шаг конвейера. Отчёт по колонкам собирается автоматически на каждый новый файл и просматривается глазами. Половина проблем видна прямо в топе значений: три написания одного склада, единицы измерения на двух языках, артикулы с невидимыми символами.
3. Правила валидации как код. Отдельный файл: имя правила, выражение, уровень (ошибка или предупреждение), сообщение. Прогон даёт таблицу нарушений — номер строки, колонка, правило, фактическое значение. Таблица уходит поставщику данных как есть, без пересказа в переписке.
4. Стейджинг. Файл грузится в промежуточную таблицу или отдельную схему, с текстовыми колонками, без приведения типов. Там же выполняются ссылочные проверки: сопоставление со справочниками боевой системы на чтение. В целевые таблицы на этом этапе не пишется ничего.
5. Сухой прогон. Полный маппинг выполняется без записи и печатает итог: столько-то записей создастся, столько-то обновится, столько-то пропустится, столько-то попадёт под каждое правило. Числа сверяет человек, который знает предметную область. «Обновится 4 000 из 4 200» на первичной загрузке — сигнал, что ключ сопоставления выбран неверно, и ловится этот сигнал только глазами.
6. Идемпотентный ключ и batch_id. У каждой записи — внешний идентификатор из источника, у каждой загрузки — идентификатор партии, записанный в целевые записи или в служебную таблицу соответствия. Повторный запуск обновляет, а не дублирует. Партия целиком находится одним запросом.
7. Загрузка партиями с фиксацией прогресса. Не одна транзакция на сто тысяч строк и не построчно, а блоками с записью, до какого блока дошли. Остановка на середине оставляет понятное состояние.
8. Пост-проверки и контрольные суммы. После загрузки прогоняются те же смысловые правила уже по целевым данным, плюс сверка агрегатов с источником: количество записей, суммы по нескольким группам, минимумы и максимумы дат. Совпадение агрегатов — дешёвый способ поймать тихий сдвиг вроде поехавшего разделителя.
Где помогает модель: разбор неструктурированных полей (адрес одной строкой, наименование со слитыми характеристиками), сопоставление вольных названий со справочником, объяснение отчёта об ошибках человеческим языком. Решение при этом остаётся за правилами: модель предлагает соответствие с оценкой уверенности, правило пропускает только выше порога, остальное уходит человеку списком. Вывод о корректности данных на суждение модели не перекладывается.
Что не трогали
Боевые справочники. На этапе проверки они доступны только на чтение. Автосоздание недостающих позиций выключено: непопадание в справочник — вход для человека, не повод плодить сущности.
Схему целевой системы. Подгонять структуру под очередной файл — путь к колонкам вида «Примечание3».
Исходный файл. Он не правится на месте. Все преобразования живут в слое маппинга и правил, файл сохраняется как пришёл — иначе через месяц не воспроизвести, откуда взялось значение.
Права доступа. Загрузка идёт под учётной записью с правами ровно на нужные таблицы, а не под администратором «чтобы точно прошло».
Ручной ввод. Люди продолжают заводить данные как заводили; проверка встраивается только в путь массовой загрузки.
Обратимость
- batch_id даёт выборку всех записей партии одним условием. Откат сводится к удалению или пометке отменёнными по одному полю.
- Резервная копия до загрузки — снятая и проверенная на разворачивание. Копия, которую ни разу не восстанавливали, остаётся гипотезой.
- Партии позволяют остановиться после первого блока и посмотреть результат в интерфейсе живой системы, прежде чем пускать остальные.
- Стейджинг сохраняется после загрузки: исходные текстовые значения рядом с результатом маппинга. По ним разбирают любую претензию к данным без обращения к поставщику файла.
- Если целевая система не умеет удалять записи и не имеет пометки отмены, прогон делают на копии базы, а откат означает восстановление целиком — и знать это нужно до загрузки, а не после.
Границы
Система без внешних идентификаторов и без мягкого удаления. Откат превращается в восстановление всей базы, и загрузка в рабочее время исключается.
Данные, корректность которых нельзя проверить формально. Правильность цены, актуальность адреса, соответствие наименования реальному товару сверяются людьми, выборочно, до загрузки.
Поток вместо разовой загрузки. Для постоянной интеграции проверки нужны на приёмнике и постоянно, с карантином для отклонённых записей, а не одним прогоном перед стартом.
Источник, который меняется во время выгрузки. Файл, снятый с живой базы без фиксации среза, не соответствует ни одному моменту времени, и сверка агрегатов будет расходиться всегда.
Небольшие объёмы. Триста строк проверяются глазами и загружаются руками быстрее, чем собирается конвейер.
Срок, поставленный вместо проверки. Требование «загрузить сегодня» проверку не отменяет, а переносит на этап поиска последствий, где она обходится дороже.
Правило одной строкой: до первой записи в боевую систему — стейджинг, сухой прогон и batch_id, и только потом загрузка.
