Обработка элементов ссылочных типов порциями

Небольшое дополнение к рекомендации от 1С по оптимизации использования оперативной памяти (https://its.1c.ru/db/v8std#content:2149184374:hdoc).

В рекомендации от 1С Оптимизация использования оперативной памяти предлагается получать данные порциями при потенциально неограниченных выборках.

В качестве примера приводится такой участок кода:

ВсеОбработано = Ложь;
Пока Истина Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| <условие выборки необработанных записей>";

РезультатЗапроса = Запрос.Выполнить();
ВсеОбработано = РезультатЗапроса.Пустой();
Если ВсеОбработано Тогда
Прервать;
КонецЕсли;

// Обход порции результата запроса
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Обработка элемента выборки
// ...
КонецЦикла;

КонецЦикла;

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

Я предлагаю простое решение для выборки ссылочных данных: использовать в качестве указателя на порцию данных ссылку.

Для этого необходимо внести следующие изменения:

  • Перед погружением в цикл запросов получаем пустую ссылку (она всегда меньше любой непустой ссылки) и записываем ее в переменную указывающую на последнюю полученную ссылку (ПоследняяСсылка).
  • В запросе упорядочиваем выборку по ссылке (не включая автоупорядочивание, т.к. это приведет к сортировке по представлению) и добавляем отбор по ссылке (Ссылка > &ПоследняяСсылка).
  • На каждом проходе выборки обновляем переменную ПоследняяСсылка, присваивая ей текущую обрабатываемую ссылку.

Получим следующий код:

ВсеОбработано = Ложь;
ПоследняяСсылка = Справочники.Номенклатура.ПустаяСсылка();
Пока Истина Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка > &ПоследняяСсылка
| И <условия отбора требуемые для прикладной задачи>
|
|УПОРЯДОЧИТЬ ПО
| Ссылка";
Запрос.УстановитьПараметр("ПоследняяСсылка",ПоследняяСсылка);
РезультатЗапроса = Запрос.Выполнить();
ВсеОбработано = РезультатЗапроса.Пустой();
Если ВсеОбработано Тогда
Прервать;
КонецЕсли;

// Обход порции результата запроса
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
ПоследняяСсылка = ВыборкаДетальныеЗаписи.Ссылка;
// Обработка элемента выборки
// ...
КонецЦикла;

КонецЦикла;

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

П.С. Я намеренно не стал проводить рефакторинг кода с ИТС, чтобы добавленный код был более заметен. На мой взгляд создавать запрос стоит перед циклом, а не внутри него и нет необходимости создавать переменную ВсеОбработано.

Код после рефакторинга

 

24 Comments

  1. A_Max

    Это конечно хорошо, но ВНЕЗАПНО может появиться объект с УИД меньше последнего обработанного.

    Почему:

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

    2. Обмены

    Поэтому и создаются регистры сведений ОчередьХХХХХХХХ

    Reply
  2. triviumfan

    (1) такие вещи делают в монопольном режиме имхо

    Reply
  3. acanta

    То есть сравнение на больше меньше работает по Гуиду как по строке?

    Reply
  4. lunjio

    Боже мой, уже весь мир давно использует, посмотрите процедуры корректировки стоимости списания в УПП, ПЕРВЫЕ 3000 ГДЕ &ПоследнийОбъектОчереди = НЕОПРЕДЕЛЕНО ИЛИ Объект.Ссылка > &ПоследнийОбъектОчереди УПОРЯДОЧИТЬ ПО Объект.Ссылка.

    Reply
  5. lunjio

    (3) Запросы в цикле не всегда зло, есть таблица 10 гб размером, сервак просто помирает при попытке выбрать из нее, как сделать как ни порционно и в цикле ? Убивать хочется за тупую архитектуру и кривую логику, когда люди вместо того чтобы попытаться найти фунционал или взять удачный пример из типовых или других решений, придумывают велосипеды, да причем очень кустарные.

    Reply
  6. lunjio

    (1) Речь просто о необходимости порционно обрабатывать информацию, в случае что вы описываете, есть ещё необходимость последовательности, ну хорошо, в этом случае используем МоментВремени() ссылки и вопрос решен. Регистры же, как правило создают для отложенной обработки, в статье же в первом предложении говорится, что порционно при неограниченно больших данных.

    Reply
  7. Eleepod

    (1)Согласен, в случае если во время обработки в системе могут появится новые данные они могут быть не обработаны. Данный вариант стоит рассматривать как альтернативу запросу, который получает все данные за одну выборку.

    Reply
  8. Eleepod

    (4) Примерно, но только не в том представлении как оно выводится методом УникальныйИдентификатор. Например ГУИД dee6e178-55bc-11d9-848a-00112f435cbd при ошибке «объект не найден» будет выведен как 848a00112f435cbd11d955bcdee6e178, т.е. вид A-B-C-D-E превращается в DECBA. Думаю в таком виде они и сравниваются.

    Reply
  9. Shmell

    (3) такой подход очень оправдан. При этом у Вас не засоряется кэш объектов вследствии использования ПолучитьОбъект(), и немаловажный момент — что Вы можете параллелить обработку порций объектов разными фоновыми заданиями.

    Reply
  10. Darklight

    (1)Просто так такие UID не появляются. У них есть стандарт генерирования (у 1С он свой UUID, у мелкомягких свой — GUID) — и он хронологический. Вариантов появления младших UID только два:

    1. Обмен данных с другой системой, формирующих UID на другом компьютере, выполненный параллельно с обработкой

    2. UID задаётся вручную строкой не по тем же правилам его формирования — опять таки каким-то параллельным процессом

    Для данного примера вероятен только п.1. (п.2. почти невероятен — но кто его знает, что там пишу криворукие программисты на местах)

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

    Ну или, хотя бы блокировку данных надо наложить (увы — в данном примере — на всю таблицу)!

    Да, кстати, замечу, что если вообще не заморачиваться — и выбрать все Ссылки разом а потом их обрабатывать — то это так же ни гарантирует, что в процессе обработки не появятся новые Ссылки (в любой «хронологии»), которые останутся не обработанными — проблема не повторяемого чтения (или наоборот — будут удалены — проблема фантомов). Вот для этого и существуют блокировки данных!

    Reply
  11. Darklight

    (5)Думаю не весь мир. Решение очевидно понятное (если ссылки сортируются, значит они могут сравниваться), да неприглядное 😉 не привычно вот таки воспринимать ссылки большинству!

    Reply
  12. Darklight

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

    Зачем так людей пугаете. Конечно же можно — если поля сортировки будут ниже поля упорядочивания по ссылеке — то никаких проблем (но это бывает редко — когда ссылка это и есть ключевой обрабатываемый объект)

    А вот, если поля для сортировки стоят раньше ссылки — то тогда их так же придётся включить в контроллируемые (по классическому подходу) — и пример несколько усложнится, потеряв своё изначальное изящество — но будет работать. Хоршо бы и такой пример привести тоже.


    Так же следует учитывать, что сортировка по ссылке не означает сортировку по моменту времени.

    Если речь про момент времени документов — то да, Ссылка <> МоментВремени — но там как раз Ссылку лучше заменить на МоментВремени — он так же уникален (и не повторяем) как и Ссылка.

    Если же имелся в виду какой-то другой момент времени — то надо уточнить — какой и в чём проблема!

    Reply
  13. Darklight

    (3)Не понимаю к чему Вы так предрались. Запросы в цикле — это не есть абсолютное зло, особенно когда это цикли порционной обработки. При работе с Big Data так вообще без этого НИКАК не обойтись!

    Вот создавать каждый раз запрос в цикле — не очень красиво, но для данного примера — абсолютно не критично!

    Reply
  14. lunjio

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

    Reply
  15. Darklight

    (15)У Вас пример, немного из другой оперы, хотя автор в теме статьи не ограничил её только обработкой источников, являющихся только объектными (аля справочников и документов). Да, там где обрабатываемый объект не является ключевым (как ссылка) для выборки — эта техника не сгодится. Но ничего особо сложного — делаете структуру со свойствами — где все ключевые измерения и сохраняеете текующую позицию туда. В запросе по ним всем сортируете, а условие будет на «ИЛИ» по этим измерениям (и так же на «>»), а в цикле обновляете их все из данных выборки в структуре через ЗаполнитьЗначенияСвойств — как -то так — вроде ничего особенного — код сложнее где-то на (10 + количество ключевых полей) процентов.

    Reply
  16. Eleepod

    (13) Похоже про сортировку я действительно не прав. А насчет ссылок и моментов времени хотел предостеречь от заблуждения что сортировка по ссылке равна сортировке по моменту времени. Уберу пока эти моменты. Спасибо за комментарии!

    Reply
  17. kuzyara

    (9) гуиды имеют тип binary(16) и сравниваются побитово как бинарные данные: binary bit-for-bit comparison behavior

    Функция ПолучитьGUIDПоУникальномуИдентификатору(UUID1)
    UUID=ВРЕГ(UUID1);
    ч1 = Сред(UUID,20,4);
    ч2 = Сред(UUID,25,12);
    ч3 = Сред(UUID,15,4);
    ч4 = Сред(UUID,10,4);
    ч5 = Сред(UUID,1,8);
    Возврат «0x» + ч1 + ч2 + ч3 + ч4 + ч5;
    КонецФункции

    Показать

    А если кто-то как Darklight думает что гуиды формируются хронологически — почитайте статью Как формируется GUID?

    Гуиды выдаются пулом по 32 штуки для каждого сеанса отдельно, за исключением random based uuid получаемых через Новый УникальныйИдентификатор()

    Reply
  18. Glebis

    Предлагаю ещё немного рефакторинга:

    Запрос = Новый Запрос;
    Запрос.Текст =
    «ВЫБРАТЬ ПЕРВЫЕ 1000
    | Номенклатура.Ссылка КАК Ссылка,
    | Номенклатура.Наименование КАК Наименование,
    | Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры
    |ИЗ
    | Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    | Номенклатура.Ссылка > &ПоследняяСсылка
    | И <условие выборки необработанных записей>
    |
    |УПОРЯДОЧИТЬ ПО
    | Ссылка»;
    Запрос.УстановитьПараметр(«ПоследняяСсылка», Справочники.Номенклатура.ПустаяСсылка());
    РезультатЗапроса = Запрос.Выполнить();
    Пока НЕ РезультатЗапроса.Пустой() Цикл
    // Обход порции результата запроса
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    // Обработка элемента выборки
    // …
    КонецЦикла;
    Запрос.УстановитьПараметр(«ПоследняяСсылка», ВыборкаДетальныеЗаписи.Ссылка);
    РезультатЗапроса = Запрос.Выполнить();
    КонецЦикла;
    

    Показать

    Reply
  19. Glebis

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

    Запрос = Новый Запрос;
    Запрос.Текст =
    «ВЫБРАТЬ ПЕРВЫЕ 1000
    | Номенклатура.Ссылка КАК Ссылка,
    | Номенклатура.Наименование КАК Наименование,
    | Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры
    |ИЗ
    | Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    | НЕ Номенклатура.Ссылка В (&МассивОбработанныхСсылок)
    | И <условие выборки необработанных записей»;
    МассивОбработанныхСсылок = Новый Массив;
    Запрос.УстановитьПараметр(«МассивОбработанныхСсылок», МассивОбработанныхСсылок);
    РезультатЗапроса = Запрос.Выполнить();
    Пока НЕ РезультатЗапроса.Пустой() Цикл
    // Обход порции результата запроса
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    МассивОбработанныхСсылок.Добавить(ВыборкаДетальныеЗаписи.Ссылка);
    // Обработка элемента выборки
    // …
    КонецЦикла;
    Запрос.УстановитьПараметр(«МассивОбработанныхСсылок», МассивОбработанныхСсылок);
    РезультатЗапроса = Запрос.Выполнить();
    КонецЦикла;
    

    Показать

    Reply
  20. Glebis

    Если предыдущий вариант со складированием ссылок обработанных объектов в массив не подходит, то можно попробовать ещё один особо извращенский способ: переделать запрос на НЕ вхождение порции в таблицу изменений справочника. Но здесь имеются подводные камни, которые нужно проверять: наложение общей транзакции, отдельный план обмена с ручной регистрацией и ещё куча других вопросов.

    Reply
  21. VmvLer

    если немного подумать то АВТОНОМЕРЗАПИСИ() решает задачу разбиения выборки на порции

    размер порции 1000, 10 000,… задаем параметром и дальше простая математика в SQL без шаманских пассов получает порционную выборку

    Reply
  22. markers

    (8) А что изменится если запросить сразу все? При запросе всей информации данный риск даже выше. И данный способ настолько прост и от этого хорош! При этом не надо добавлять реквизиты и пр.

    Reply
  23. user970589

    Картинка зачотная!!! пацсталом. ссылочные элементы. порциями. аххах..

    Reply
  24. dsdred

    Старый способ…

    уже описывал в статье https://infostart.ru/public/893304/

    А вообще способ «декоративный» — не надежный!

    Не дураки придумали ПланОбмена и номера сообщений — этот вариант тоже показан в статье https://infostart.ru/public/893304/

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *