Как размножить строки в запросе 1С заданное количество раз?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Представим практическую ситуацию, аналогичную той, что обсуждалась на форуме. У нас есть документ, в шапке которого присутствует числовой реквизит (например, ЦелевоеКоличество), и табличная часть. Нам необходимо вывести в отчете ссылку на данный документ ровно столько раз, сколько составляет разница между этим числовым реквизитом и реальным количеством строк в его табличной части. Обозначим это число как N. Если N = 5, то строка с документом должна повториться в результате запроса ровно 5 раз. Как решить эту задачу максимально эффективно, красиво и без использования постобработки выборки на стороне встроенного языка (например, в процедуре ПриКомпоновкеРезультата)? Давайте разбираться!

Анализ решения с форума: соединение таблицы самой с собой

Автор темы в итоговом сообщении пришел к следующему варианту генерации последовательности чисел:


ВЫБРАТЬ ПЕРВЫЕ 1000
    Номенклатура.Ссылка КАК Ссылка
ПОМЕСТИТЬ Спр
ИЗ
    Справочник.Номенклатура КАК Номенклатура
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВложенныйЗапрос.Количество КАК Числа
ИЗ
    (ВЫБРАТЬ
        Спр.Ссылка КАК Ссылка,
        КОЛИЧЕСТВО(Спр1.Ссылка) КАК Количество
    ИЗ
        Спр КАК Спр
        ЛЕВОЕ СОЕДИНЕНИЕ Спр КАК Спр1 ПО Спр.Ссылка >= Спр1.Ссылка
    СГРУППИРОВАТЬ ПО
        Спр.Ссылка) КАК ВложенныйЗапрос
УПОРЯДОЧИТЬ ПО
    Числа

Давайте проанализируем эту ситуацию и выясним, почему такое решение категорически не рекомендуется использовать в реальных рабочих базах. Выделим три ключевых недостатка:

  1. Зависимость от наполненности базы данных. Если мы запустим этот отчет в новой, пустой или тестовой базе (например, при развертывании нового узла РИБ), где в справочнике Номенклатура содержится менее 100 или вовсе 0 записей, наш алгоритм не сможет сгенерировать числовую последовательность нужной длины. В результате отчет выдаст некорректные или пустые данные.
  2. Крайне низкая производительность. Операция левого соединения таблицы самой с собой по условию «больше или равно» (ПО Спр.Ссылка >= Спр1.Ссылка) порождает декартово произведение огромного масштаба. Для получения всего 1000 чисел СУБД сначала построит в оперативной памяти виртуальную таблицу из 500 000 строк, а уже затем начнет выполнять группировку. На реальных базах это приведет к зависанию запроса и колоссальной нагрузке на процессор сервера СУБД.
  3. Лишнее чтение физических таблиц. Обращаться к реальной таблице базы данных ради получения абстрактного числового ряда — неоптимальное решение, увеличивающее нагрузку на дисковую подсистему.

Вариант 1. Генерация последовательности в памяти через декартово произведение (Оптимальный для СКД)

Чтобы не зависеть от наполнения справочников и не нагружать дисковую подсистему, мы можем сгенерировать числовую последовательность полностью в оперативной памяти СУБД. Для этого используется объединение маленьких статических выборок с последующим перемножением (декартовым произведением без условий связи или по условию ПО ИСТИНА).

Рассмотрим подробнее пример запроса, генерирующего числа от 0 до 99:


ВЫБРАТЬ
    Десятки.Цифра * 10 + Единицы.Цифра КАК Число
ПОМЕСТИТЬ ВТ_ПоследовательностьЧисел
ИЗ
    (ВЫБРАТЬ 0 КАК Цифра ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9) КАК Единицы,
    (ВЫБРАТЬ 0 КАК Цифра ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9) КАК Десятки
;

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

Вариант 2. Передача готовой таблицы чисел через параметры запроса (Самый лаконичный)

Если наш запрос или отчет формируется программно (например, в процедуре модуля объекта или внешней обработки), самым элегантным и лаконичным решением будет перенести генерацию числового ряда на сторону встроенного языка 1С. Для этого мы создаем вспомогательную таблицу значений, заполняем её в цикле нужным количеством строк и передаем в запрос в качестве параметра.

Разберем этот вариант по шагам:


ТаблицаЧисел = Новый ТаблицаЗначений;
ТаблицаЧисел.Колонки.Добавить("Число", Новый ОписаниеТипов("Число"));

// Заполняем таблицу числами от 1 до нашего максимума (например, 100)
Для Шаг = 1 По 100 Цикл 
    НоваяСтрока = ТаблицаЧисел.Добавить();
    НоваяСтрока.Число = Шаг;
КонецЦикла;

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ВТ_Параметр.Число КАК Число
    |ПОМЕСТИТЬ ВТ_ПоследовательностьЧисел
    |ИЗ
    |	&ТаблицаЧисел КАК ВТ_Параметр
    |;";
    
Запрос.УстановитьПараметр("ТаблицаЧисел", ТаблицаЧисел);

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

Итоговая сборка: размножение строк документов

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

Шаг 1. Сначала подготовим таблицу документов с вычисленным динамически количеством повторений. Обязательно используем функцию ЕСТЬNULL, так как у документа может не быть строк в табличной части, и соединение вернет значение NULL, что сломает математические операции.


ВЫБРАТЬ
    Док.Ссылка КАК Ссылка,
    Док.ВашЧисловойРеквизит - ЕСТЬNULL(ВТ_ПодсчетСтрок.КоличествоСтрок, 0) КАК ТребуемоеКоличество
ПОМЕСТИТЬ ВТ_ДокументыСКоличеством
ИЗ
    Документ.ИмяВашегоДокумента КАК Док
    ЛЕВОЕ СОЕДИНЕНИЕ (
        ВЫБРАТЬ
            ТЧ.Ссылка КАК Ссылка,
            КОЛИЧЕСТВО(ТЧ.НомерСтроки) КАК КоличествоСтрок
        ИЗ
            Документ.ИмяВашегоДокумента.ИмяТабличнойЧасти КАК ТЧ
        СГРУППИРОВАТЬ ПО
            ТЧ.Ссылка
    ) КАК ВТ_ПодсчетСтрок ПО Док.Ссылка = ВТ_ПодсчетСтрок.Ссылка
ГДЕ
    // Отсекаем документы, для которых дублирование не требуется (N <= 0)
    Док.ВашЧисловойРеквизит - ЕСТЬNULL(ВТ_ПодсчетСтрок.КоличествоСтрок, 0) > 0
;

Шаг 2. Выполняем внутреннее соединение подготовленных документов с нашей последовательностью чисел по условию сравнения. Каждая строка документа продублируется ровно столько раз, каково значение вычисленного поля ТребуемоеКоличество:


ВЫБРАТЬ
    ВТ_Документы.Ссылка КАК Ссылка
ИЗ
    ВТ_ДокументыСКоличеством КАК ВТ_Документы
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследовательностьЧисел КАК ВТ_Числа
        ПО ВТ_Числа.Число <= ВТ_Документы.ТребуемоеКоличество

Благодаря этому элегантному подходу мы полностью решили задачу на уровне СУБД. Наш отчет будет работать максимально быстро, стабильно и предсказуемо на базах данных любого объема!

← На главную