Обход запроса по нескольким группировкам

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

Хочу поделиться с вами небольшой «зарисовкой», которая помогает мне обходить выборку запроса, как бы пропуская некоторые группировки.
Что я имею в виду. Предположим, у нас есть запрос, который выбирает из регистра движения по номенклатуре в разрезе склада, номенклатуры, характеристики номенклатуры и документа движения. Вот как выглядит «плоская» выгрузка результата этого запроса:

Если в запросе указать итоги по складу, номенклатуре и характеристике номенклатуры и написать следующий код:

то обход будет производиться следующим образом:

Т.е. по первой группировке «Склад» внешний цикл (голубой) совершит одну итерацию, по второй группировке «Номенклатура» («розовый» цикл) четыре итерации, в каждой из итераций «розового» цикла будет разное количество итераций «зеленого» цикла по группировке «ХарактеристикаНоменклатуры», и в конечном итоге будут выбраны все детальные записи («серый» цикл), которые на рисунке я отмечать не стал, ибо лениво.

Но иногда хочется выбрать записи вот таким образом:

Т.е. так, чтобы внешний (голубой) цикл выбирал, как и положено, по группировке «Склад», а внутренний (зеленый) выбирал по некой «агрегатной» группировке «Номенклатура+ХарактеристикаНоменклатуры». Ну и дальше по необходимости детальные записи. Это было бы удобно, если бы вы, например, создавали документы по группировке «Склад», а второй «метагруппировкой» заполняли табличную часть каким-либо образом. Конечно, код, который приведен выше, справляется с этой задачей, но лично моя печаль в том, что там есть один вложенный цикл (розовый), который делается совершенно ненужным с точки зрения «изящества кода» как минимум. К тому же, когда таких группировок становится больше, например девять, мы видим совершенно «потрясающую» картину из девяти вложенных циклов.
Еще можно просто пропустить группировку «Номенклатура» и обходить результат запроса по группировкам «Склад — ХарактеристикаНоменклатуры — ДетальныеЗаписи». Но вот беда, в этом случае на уровне характеристики нет самой номенклатуры. Смотрите сами:

 

Получается, что на уровне выборки по складу номенклатуры еще нет, а на уровне выборки по характеристике ее… все еще нет. Бида.

Я точно знаю, что я не один такой, но у меня и у других коллег по несчастью как-то сам собой напрашивается вот такой код:


Код прекрасен всем, кроме того… что он не работает. Если когда-нибудь разгадаю это тайное послание фирмы 1С о «списке группировок», я обязательно с вами поделюсь. Сейчас же я знаю только то, что этот код мне не удалось заставить работать ни под каким соусом.

Но мы не ждем милости от природы и от 1С. А берем и сами делаем. Вот такой код работает нормально вполне:

Пара слов о функциях.
Функция «зфВыбратьПоГруппировкам» применяется вместо «ВыборкаЛалала.Выбрать()». Ей передается выборка, из которой нужно выбирать, и перечень группировок через запятую. При этом она возвращает некую «метавыборку». Ничего военного, просто соответствие с необходимыми данными.
Функция «зфСледующийПоГруппировкам» применяется вместо «ВыборкаОлоло.Следующий()». Ей передается та самая, открытая на предыдущем шаге «метавыборка» и возвращает она истину или ложь, как и штатный метод «Следующий».
Да. Внутри цикла вы можете смело получать родную 1С-овскую выборку нижнего уровня группировок, обратившись к элементу соответствия «Выборка». Вот так:

РоднаяВыборка1С = МетаВыборка[«Выборка»];

Естественно, дальше родную выборку 1С можно хоть снова перебирать этими функциями, хоть выбирать штатными средствами.

Также не могу не сказать, что, в принципе, в функциях нет ничего нового и я тут не претендую на оригинальность или что-нибудь в таком духе. Думаю, что многие писали такие же функции, просто хотелось поделиться с уважаемым сообществом.
Они также не убирают вложенные циклы, просто немного иначе их организуют и скрывают. Т.е. в действительности все «лишние» циклы есть, но из кода это не очевидно.
Еще вполне понятно, что их использование будет менее производительным в смысле быстродействия, нежели использование вороха вложенных циклов. Я, конечно, старался написать пооптимальнее, но сами понимаете, накладные расходы будут.
Да, одна функция рекурсивная. По идее глубина рекурсии будет небольшой и равна количеству пропускаемых группировок плюс один, однако рекурсия есть рекурсия.
Ну и конечно, не исключен вариант, что я где-то накосячил. Если обнаружите — пишите, поправим.

Наконец сами функции:

Функция зфВыбратьПоГруппировкам(Выборка, Группировки, СИерархией = Ложь)

       МетаВыборка = Новый Соответствие;

       врОбходРезультата = ОбходРезультатаЗапроса.ПоГруппировкам;
       Если
СИерархией Тогда
              
врОбходРезультата = ОбходРезультатаЗапроса.ПоГруппировкамСИерархией;
       КонецЕсли;
      
МетаВыборка.Вставить(«ОбходРезультата», врОбходРезультата);

       МассивГруппировок = Новый Массив;
      
врСтрГруппировки = Группировки;
       Пока Истина Цикл
              
Поз = Найти( врСтрГруппировки, «,» );
               Если
Поз = 0 Тогда
                      
МассивГруппировок.Добавить(СокрЛП(врСтрГруппировки));
                       Прервать;
               КонецЕсли;
              
МассивГруппировок.Добавить( СокрЛП( Лев(врСтрГруппировки,Поз1) ) );
              
врСтрГруппировки = Сред( врСтрГруппировки, Поз+1 );
       КонецЦикла;

       МетаВыборка.Вставить(«Группировки», МассивГруппировок);

       врВыборка = Выборка;
       Для
пц=0 По МассивГруппировок.Количество()-2 Цикл
              
врВыборкаУровня = врВыборка.Выбрать(врОбходРезультата, МассивГруппировок[пц]);
              
МетаВыборка.Вставить(«_Выборка»+Строка(пц), врВыборкаУровня);
               Если не
врВыборкаУровня.Следующий() Тогда
                       Прервать;
               КонецЕсли;
              
врВыборка = врВыборкаУровня;
       КонецЦикла;
      
врВыборкаУровня = врВыборка.Выбрать(врОбходРезультата, МассивГруппировок[пц]);
      
МетаВыборка.Вставить(«Выборка», врВыборкаУровня);
      
МетаВыборка.Вставить(«_Выборка»+Строка(пц), врВыборкаУровня);

       Возврат МетаВыборка;

КонецФункции // зфВыбратьПоГруппировкам

Функция зфСледующийПоГруппировкам(МетаВыборка, Уровень = Неопределено)

       Если Уровень = Неопределено Тогда
              
Уровень = МетаВыборка[«Группировки»].Количество()-1;
       КонецЕсли;

       Если Уровень < 0 Тогда
               Возврат Ложь;
       КонецЕсли;

       врВыборка = МетаВыборка[«_Выборка»+Строка(Уровень)];

       Если врВыборка.Следующий() Тогда
               Возврат Истина;
       КонецЕсли;

       Если зфСледующийПоГруппировкам(МетаВыборка, Уровень1) Тогда
              
МассивГруппировок = МетаВыборка[«Группировки»];
              
врВыборкаРодитель = МетаВыборка[«_Выборка»+Строка(Уровень1)];
              
врВыборка = врВыборкаРодитель.Выбрать(МетаВыборка[«ОбходРезультата»],МассивГруппировок[Уровень]);
              
МетаВыборка[«_Выборка»+Строка(Уровень)] = врВыборка;
               Если
Уровень = МассивГруппировок.Количество()-1 Тогда
                      
МетаВыборка[«Выборка»] = врВыборка;
               КонецЕсли;
               Возврат
зфСледующийПоГруппировкам(МетаВыборка, Уровень);
       Иначе
               Возврат Ложь;
       КонецЕсли;

КонецФункции // зфСледующийПоГруппировкам

Спасибо за внимание, а я желаю вам хорошего дня и хорошего кода.

Оригинал статьи в блоге автора

35 Comments

  1. zfilin

    В основном блоге коллега советует для данного примера применить к полю «Номенклатура» агрегатную функцию МАКСИМУМ.

    Не пробовал, но почему ему не работать?

    Reply
  2. Новиков

    Непонятно мне, а почему нельзя подготовить текст запроса таким образом чтобы получить простую выборку для простого обхода вот без этих всех квадратных треуголок? )

    Reply
  3. zfilin

    (2) Новиков, Да, почему ж нельзя? Можно, конечно! Более того, это было бы даже лучше и быстрее.

    Но иногда приходится и с треуголками. =)

    А предложите свой вариант для такого обхода?

    Reply
  4. Новиков

    (3) я признаться, уже забыл, когда руками обходил результат запроса с группировками. Я обычно делаю базовую схему компоновки, и при помощи настроек компоновки (двигаю группировки вверх и вниз) получаю нужный резалт. И уже его как-то обрабатываю. Т.е. даже текст запроса никакого не нужно собирать. А Ваш вариант — он наверное крут, я не спорю. И наверное даже где-то находит применение. Вот и хотелось бы узнать — где! :%)

    Reply
  5. zfilin

    (4) Новиков, А! СКД. Да, я тоже люблю скормить данные СКД, а потом получить их в нужно виде и по ним ходить. Но, вот недавно решил выбрать просто запросом и просто ходить по группировкам без СКД. Как-раз документы создавал. Так и применял.

    Глупо было бы утверждать, что это единственный и уникальный способ и я сейчас выдам прям такую исключительную ситуацию, где запрос применить можно, а СКД нельзя.

    С другой стороны какое-то внутреннее стремление к простоте протестует против того, чтобы «городить ЦЕЛУЮ СКД», когда надо просто обойти группировки простого запроса. Если бы это было возможно реализовать штатными средствами (как в не работающем примере), это был бы win. А поскольку сами функции получились достаточно «мозгачными», то это, конечно не win, а так — посокрушаться.

    Reply
  6. Yashazz

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

    Да, и СКД рулит, конечно. Чего её «городить», дело-то простое, а выигрыш иной раз нехилый.

    Reply
  7. dandrontiy

    Круто, но применение непонятно.

    И мне почему то удобней не выборками пользоваться, а ТаблицаЗначений = Запрос.Выполнить().Выгрузить();

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

    Reply
  8. karakozov

    Решение конечно для конкретной задачи, такой подход возможно имеет смысл в ситуации когда таблица имеет очень большой объем.И выбирать ее несколько раз для каждой выборки в рамках одной процедуры очень долго.Может быть конечно так разъяснено что не совсем понятно цель.В общем все от ситуации.

    Reply
  9. Yashazz

    (8) Вопрос знатокам — выборка, полученная из результата запроса методом «Выбрать», или сам результат, кэшируется где-либо или болтается в оперативке? Если да, то в клиентской или где?

    Reply
  10. zfilin

    Друзья, но когда мне нужно обойти плоскую таблицу, мне же нужно понимать момент, когда у меня появляется другой склад в первой колонке, чтобы, если взять уже приведенный пример, создать новый документ с ДРУГИМ складом. Как же это реализовать в плоской выборке?

    Т.е. тот случай, когда на разных «уровнях группировок» реализуется разное поведение, разный код.

    Reply
  11. zfilin

    Т.е. я сейчас не защищаю свои функции, но интересен сам принцип.

    Reply
  12. romansun

    я последнее продолжительное время возвращаю запросом дерево, да, с составным ключом

    циклы упрощаются до невозможности

    и самое главное масштабирование кода также упрощается в разы! Достаточно изменить состав ключа в запросе — и всё, никакой 1С код в цикле менять не приходится

    особенно помогла в этом метода «конвертации» даты в строку прям в запросе для успешного склеивания

    Reply
  13. MGraf

    (12) romansun, напишите пожалуйста публикацию! Хотя бы пару строк с примером 🙂

    Reply
  14. zfilin

    (12) romansun, Хм. Насколько я понимаю, в запросе можно создать составной ключ склеиванием строк. Но поддерживается ли уникальность при конвертации ссылочных типов в строку? Т.е. вы получаете представление ссылки или просто представление, как наименование, код, НомерДок и т.д?

    Reply
  15. i132

    Если нет пустых характеристик, можно сделать запрос без группирповки итогов и воспользоваться методом Выборка.СледующийПоЗначениюПоля()

    Описание метода на ИТС

    либо как предложил Новиков в (2) прямой запрос, без группировок остортировать по списку полей шапки:

    СтруктураУникальнойШапки = Новый Структура(«Поле1,Поле2,Поле3»);
    
    Док = Неопределено; //Лучше объявить переменную до цикла, заодно проверим первый вход
    Пока выборка.Следующий() Цикл
    ФлНоваяШапка =Ложь;
    Для каждого ПолеШапки из СтруктураУникальнойШапки Цикл
    Если НЕ ПолеШапки.Значение= выборка[ПолеШапки.Ключ] Тогда
    Если Док = Неопределено тогда
    Док.Записать();
    КонецЕсли;
    ЗаполнитьЗначенияСвойств(СтруктураУникальнойШапки,Выборка);
    Док = СоздатьДокПоШапке(СтруктураУникальнойШапки);
    Прервать;
    КонецЕсли;
    КонецЦикла;
    НоваяСтрока = Док.(ТабЧасть).Добавить();
    Док = ЗаполнитьЗначенияСвойств(НоваяСтрока,Выборка);
    КонецЦикла;
    
    Если НЕ Док = Неопределено тогда
    Док.Записать();
    КонецЕсли;

    Показать

    Reply
  16. i132

    (1) Максимум при группировке по Характеристики не поможет, если есть пустые характеристики.

    Reply
  17. Созинов

    (6) Yashazz,

    Согласен с вами, но при большом объеме иногда удобнее прогуливаться по иерархии.

    Как раз на прошлой неделе столкнулся с такой проблемой, делал перенос с 7.7 на 8.2 (да знаю, что есть куча обработок и Конвертация данных, которую пока не освоил к сожалению), каждый раз делать запросы или сортировать/обходить таблицу со всей номенклатурой, контрагентами и т.д. накладно (хотя какая разница, один раз перенос будет).

    Автору + — еще одно интересное решение в копилку, думаю пригодится, хотя использовать буду редко.

    (12) romansun,

    Спасибо за наводку.

    Reply
  18. zfilin

    (15) i132, За «СледующийПоЗначениюПоля()» спасибо, нужно будет посмотреть.

    А по поводу кода. Мне кажется или у вас там первая строка проглатывается? Когда вы в выборке дважды в подряд «Следующий()» вызываете?

    Ну в целом, после первого «Следующий» выборку можно и на начало сбросить, так что принцип понятен.

    Но вот именно от такого кода и хочется избавиться в первую очередь. Собственно, эти процедуры я и «затеял», чтобы уйти от «мониторинга момента смены ключа».

    Чем мне не нравится такой подход, это во-первых тем, что логика алгоритма несколько «размазывается» по коду. Мы имеем заполнение внутри цикла, создание и запись документа там же, а еще такие же блоки за пределами цикла. Причем, когда мы записываем документ внутри цикла, имеется документ относящийся к предыдущей итерации выборки, а данные в выборке соответствуют уже новому. Все это, что ни говори, повышает сложность кода. Да, что я говорю, думаю все отписавшиеся не раз писали код, который мониторил бы смену одного из полей и в зависимости от этого что-то происходил.

    А еще мне очень не нравится то, что код дублируется. Хорошо, когда это одна строка «ЗаполнитьЗначенияСвойств», но когда по смене ключа предусматриваются более обширные действия, блок в начале (до начала цикла) и блок в цикле нужно не забывать обновлять одновременно. Так же как и «Записать()», если нужна не просто запись, а еще какие-то финализирующие действия.

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

    Reply
  19. i132

    (18) >первая строка проглатывается

    упс, спасибо, не заметил действительно обработку первой строки пропустил. старый пост (15) переписал.

    Reply
  20. romansun
    Reply
  21. DoctorRoza

    Достаточно интересная статья, написано доходчиво и наглядно, сразу чувствуется высокий уровень автора! Предложенная идея больше смахивает на «лабораторную» раскопку 1С.

    Reply
  22. RainyAugust22

    хорошая статья,спасибо за инфу.

    Reply
  23. xomaq

    Огромное спасибо автору!!

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

    Респект и уважуха!!!

    Reply
  24. kit

    Хорошая статья. Тоже сижу разбираюсь с обходом группировок в запросе, а тут как раз в тему.

    Reply
  25. Evgen.Ponomarenko

    Действительно, актуально!

    Reply
  26. dimk@a

    Спасибо, хорошая статья. И в обсуждении как всегда много важного!

    Reply
  27. olbu

    А если переделать запрос та вот так:

    ВЫБРАТЬ
    ТоварыНаСкладахОстатки.Склад КАК Склад,
    ТоварыНаСкладахОстатки.Номенклатура,
    ТоварыНаСкладахОстатки.ХарактеристикаНоменклатуры,
    ТоварыНаСкладахОстатки.КоличествоОстаток КАК КоличествоОстаток
    ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки
    ИТОГИ
    СУММА(КоличествоОстаток)
    ПО
    Склад
    

    Показать

    Верхняя группировка будет по складу, а нижняя — по номенклатуре + характеристике.

    Или я не верно понял?

    Reply
  28. zfilin

    В таком простом случае, да сработает. Но это все же немного не то.

    А если две составных группировки?

    Например склад+контрагент и товар+характеристика.

    Reply
  29. EvgeniuXP

    Вот вам применение: есть документы, там 13 реквизитов, надо выявить одинаковые документы, если их два и более то один провести, остальные пометить на удаление. Методом группировок мы отсеиваем одинаковые, оставляем только одинаковые два и более документа, потом группируем по всем 13 реквизитам и на выходе имеем на самом нижнем уровне одинаковые записи двух и более документов. При первом входе проводим, при втором и последующих — метим на удаление.

    Reply
  30. iott

    Полезная статья по группировкам! Мне оказалась полезной!

    Получается, что на уровне выборки по складу номенклатуры еще нет, а на уровне выборки по характеристике ее… все еще нет. Бида.

    «Бида.»….тут опечатка или так задумано? =)

    Reply
  31. zfilin

    (30) Так и задумано. =)

    Reply
  32. i.kovtun

    Саша, спасибо 🙂 Быстро загуглилось и пригодилось!

    Reply
  33. zfilin

    (32) О, приветствую! Ну, вот, хорошо что пригодилось. Я когда-то полголовы себе об это сломал. Значит не зря.

    Reply
  34. andrey1508

    Посмотрите как это реализовано на диске ИТС

    https://its.1c.ru/db/pubqlang#content:64:hdoc

    &НаСервереБезКонтекста

    Процедура ВыполнитьЗапрос()

    Запрос = Новый Запрос;

    Запрос.Текст =

    «ВЫБРАТЬ

    | ПриходнаяНакладнаяСостав.Товар КАК Товар,

    | ПриходнаяНакладнаяСостав.Количество КАК Количество

    |ИЗ

    | Документ.ПриходнаяНакладная.Состав КАК ПриходнаяНакладнаяСостав

    |УПОРЯДОЧИТЬ ПО

    | Товар

    |ИТОГИ

    | СУММА(Количество)

    |ПО

    | Товар ИЕРАРХИЯ»;

    РезультатЗапроса = Запрос.Выполнить();

    СпособВыборки = ОбходРезультатаЗапроса.ПоГруппировкамСИерархией;

    ВыборкаЗапроса = РезультатЗапроса.Выбрать(СпособВыборки);

    ВыдатьВсеВложения(ВыборкаЗапроса);

    КонецПроцедуры

    &НаСервереБезКонтекста

    Процедура ВыдатьВсеВложения(ИерархическаяВыборка)

    Сообщение = Новый СообщениеПользователю;

    Пока ИерархическаяВыборка.Следующий() Цикл

    Сообщение.Текст = «Товар: » + ИерархическаяВыборка.Товар.Наименование +

    » Количество: » + ИерархическаяВыборка.Количество() +

    » Тип записи: » + ИерархическаяВыборка.ТипЗаписи() +

    » Уровень: » + ИерархическаяВыборка.Уровень() +

    » Группировка: » + ИерархическаяВыборка.Группировка();

    Сообщение.Сообщить();

    // Продолжим выборку подчиненных записей

    СпособВыборки = ОбходРезультатаЗапроса.ПоГруппировкамСИерархией;

    Если ИерархическаяВыборка.ТипЗаписи() = ТипЗаписиЗапроса.ИтогПоИерархии Тогда

    ДочерняяВыборка = ИерархическаяВыборка.Выбрать(СпособВыборки, ИерархическаяВыборка.Группировка());

    Иначе

    ДочерняяВыборка = ИерархическаяВыборка.Выбрать(СпособВыборки);

    КонецЕсли;

    ВыдатьВсеВложения(ДочерняяВыборка);

    КонецЦикла;

    КонецПроцедуры

    Reply
  35. basuga

    Спасибо огромное! Решил задачу над которой мучался несколько часов за 2 минуты!

    Reply

Leave a Comment

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