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

Небольшое дополнение к рекомендации от 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 не будет опубликован. Обязательные поля помечены *