При разработке отчетов и обработок на платформе 1С:Предприятие мы часто сталкиваемся с необходимостью извлечения и объединения данных из различных источников. На начальных этапах освоения платформы нередко возникает соблазн решать задачи простым, но неэффективным путем, например, строя громоздкие и плохо читаемые запросы. В этом руководстве мы разберем типичный сценарий неоптимального запроса и рассмотрим, как его можно улучшить, применяя лучшие практики и эффективные методы 1С-программирования.
Предположим, у нас есть задача получить данные о ячейках склада с адресным хранением, отсортированных по проездам, и отобразить их в табличном документе. Изначальный подход, как показано в исходной теме, заключается в создании множества временных таблиц для каждого проезда и последующем их объединении через многочисленные ПОЛНОЕ СОЕДИНЕНИЕ. Такой подход, хотя и может дать результат, является крайне неэффективным, сложным для чтения и поддержки.
Давайте проанализируем исходный код и выясним, какие его части можно улучшить.
// Пример исходного неоптимального запроса (частично)
ТекстЗапроса = "";
Для Каждого Проезд Из МассивПроездов Цикл
ИмяВременнойТаблицы = "Вт_проезды" + Проезд;
ТекстЗапроса = ТекстЗапроса + "ВЫБРАТЬ
| усЯчейки.Помещение,
| усЯчейки.Проезд,
| усЯчейки.Код,
| усЯчейки.Ярус,
| усЯчейки.Стеллаж
|ПОМЕСТИТЬ " + ИмяВременнойТаблицы + "
|ИЗ
| Справочник.усЯчейки КАК усЯчейки
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Перечисление.усСтатусыЯчеек КАК усСтатусыЯчеек
| ПО усЯчейки.СтатусЯчейки = усСтатусыЯчеек.Ссылка
|ГДЕ
| усЯчейки.Помещение.Наименование = &Помещение1
| И усЯчейки.Проезд = &Проезд" + Проезд + "
| И усЯчейки.СтатусЯчейки = &Блок1;
|";
КонецЦикла;
ТекстЗапроса = ТекстЗапроса + "ВЫБРАТЬ
| A.Помещение,
| A.Проезд КАК Проезд,
| A.Ярус КАК Ярус,
| A.Стеллаж КАК Стеллаж,
| A.Код КАК Код1,
// ... и так далее до Код8
| H.Код КАК Код8
|ИЗ
| Вт_проездыA КАК A
| ПОЛНОЕ СОЕДИНЕНИЕ Вт_проездыB КАК B
| ПО A.Ярус = B.Ярус И A.Стеллаж = B.Стеллаж
// ... и так далее для всех временных таблиц
";
В первоначальном запросе мы видим конструкцию ВНУТРЕННЕЕ СОЕДИНЕНИЕ Перечисление.усСтатусыЯчеек КАК усСтатусыЯчеек ПО усЯчейки.СтатусЯчейки = усСтатусыЯчеек.Ссылка в сочетании с условием И усЯчейки.СтатусЯчейки = &Блок1. Давайте выясним, насколько это эффективно.
ГДЕ, и нам не нужны другие поля из перечисления (например, его наименование), то само соединение с таблицей перечисления Перечисление.усСтатусыЯчеек может быть избыточным или неэффективным. Платформа 1С достаточно умна, чтобы понять, что усЯчейки.СтатусЯчейки является ссылкой на перечисление. Мы можем напрямую фильтровать по этой ссылке.ЗНАЧЕНИЕ(): Для фиксированных значений перечислений (таких как статус ячейки) вместо параметра рекомендуется использовать функцию ЗНАЧЕНИЕ() напрямую в тексте запроса. Это делает запрос более явным, улучшает его читаемость и позволяет конструктору запросов лучше его анализировать и оптимизировать.
Таким образом, блок с соединением и параметром можно значительно упростить:
// Было:
// ВНУТРЕННЕЕ СОЕДИНЕНИЕ Перечисление.усСтатусыЯчеек КАК усСтатусыЯчеек
// ПО усЯчейки.СтатусЯчейки = усСтатусыЯчеек.Ссылка
// ГДЕ ... И усЯчейки.СтатусЯчейки = &Блок1;
// Запрос.УстановитьПараметр("Блок1", Перечисления.усСтатусыЯчеек.Доступна);
// Стало:
// Просто в секции ГДЕ указываем прямое значение
ГДЕ
усЯчейки.Помещение.Наименование = &Помещение1
И усЯчейки.Проезд = &ПроездА
И усЯчейки.СтатусЯчейки = ЗНАЧЕНИЕ(Перечисление.усСтатусыЯчеек.Доступна);
Мы видим, что соединение с таблицей перечисления убрано, а значение статуса указано напрямую. Это устраняет ненужные операции соединения и делает запрос более прозрачным.
Наиболее существенная неэффективность исходного запроса заключается в создании отдельной временной таблицы для каждого проезда (например, Вт_проездыA, Вт_проездыB и так далее) и последующем их объединении через многочисленные ПОЛНЫЕ СОЕДИНЕНИЯ. Этот подход является ресурсоемким и крайне нечитаемым, особенно когда количество проездов может меняться. Мы можем значительно упростить этот процесс, используя оператор В.
Вместо того чтобы делать восемь отдельных запросов для каждого проезда, мы можем собрать все необходимые "Проезды" в массив до выполнения запроса и передать этот массив в запрос как один параметр. Запрос будет выглядеть примерно так:
// В коде 1С формируем массив проездов:
МассивПроездов = Новый Массив;
Для КодСимвола = КодСимвола("A") По КодСимвола("Z") Цикл
МассивПроездов.Добавить(Символ(КодСимвола));
КонецЦикла;
// В тексте запроса используем оператор "В":
Запрос.Текст = "ВЫБРАТЬ
| усЯчейки.Помещение,
| усЯчейки.Проезд,
| усЯчейки.Код,
| усЯчейки.Ярус,
| усЯчейки.Стеллаж
|ИЗ
| Справочник.усЯчейки КАК усЯчейки
|ГДЕ
| усЯчейки.Помещение.Наименование = &Помещение1
| И усЯчейки.Проезд В (&МассивПроездов)
| И усЯчейки.СтатусЯчейки = ЗНАЧЕНИЕ(Перечисление.усСтатусыЯчеек.Доступна)
|УПОРЯДОЧИТЬ ПО
| усЯчейки.Проезд,
| усЯчейки.Ярус,
| усЯчейки.Стеллаж
|;
";
// Устанавливаем параметры:
Запрос.УстановитьПараметр("Помещение1", "Камера №12");
Запрос.УстановитьПараметр("МассивПроездов", МассивПроездов);
// Параметр &Блок1 больше не нужен, т.к. значение указано напрямую
Этот подход позволяет выбрать все необходимые данные одним запросом, без создания множества временных таблиц и выполнения ресурсоемких полных соединений. Значительно улучшается производительность, читаемость и универсальность кода, так как количество проездов теперь не влияет на структуру запроса.
В исходном варианте, после создания восьми временных таблиц, они объединялись через серию ПОЛНЫХ СОЕДИНЕНИЙ. Оператор ПОЛНОЕ СОЕДИНЕНИЕ может значительно снижать производительность, особенно при работе с СУБД PostgreSQL и при использовании нескольких таких конструкций в одном запросе, поскольку он пытается найти совпадения с обеих сторон и сохраняет все строки, даже те, для которых нет совпадений. Часто такие сложные соединения приводят к избыточным или труднообрабатываемым результатам.
Если нам все же необходимо объединить данные из нескольких источников, и при этом они имеют одинаковую или схожую структуру, вместо множественных ПОЛНЫХ СОЕДИНЕНИЙ мы можем рассмотреть использование оператора ОБЪЕДИНИТЬ ВСЕ.
Оператор ОБЪЕДИНИТЬ ВСЕ объединяет результаты нескольких запросов без автоматической группировки дублирующихся строк, что часто бывает быстрее, чем ОБЪЕДИНИТЬ (который удаляет дубликаты). Это может быть полезно, если вы в дальнейшем планируете обрабатывать эти данные в коде 1С, а не только средствами запроса. Однако в нашем случае, когда все данные выбираются из одной таблицы Справочник.усЯчейки, самый оптимальный вариант — это использовать оператор В, как было описано выше.
Если бы у нас были, например, данные о ячейках из разных складов, и мы хотели бы объединить их в один список без сложных связей, то ОБЪЕДИНИТЬ ВСЕ мог бы выглядеть так:
// Пример использования ОБЪЕДИНИТЬ ВСЕ (не для данного конкретного случая, а для демонстрации)
ВЫБРАТЬ
усЯчейки.Помещение,
усЯчейки.Проезд,
усЯчейки.Код,
усЯчейки.Ярус,
усЯчейки.Стеллаж
ИЗ
Справочник.усЯчейки КАК усЯчейки
ГДЕ
усЯчейки.Помещение.Наименование = &Помещение1
И усЯчейки.СтатусЯчейки = ЗНАЧЕНИЕ(Перечисление.усСтатусыЯчеек.Доступна)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
усЯчейки2.Помещение,
усЯчейки2.Проезд,
усЯчейки2.Код,
усЯчейки2.Ярус,
усЯчейки2.Стеллаж
ИЗ
Справочник.усЯчейки КАК усЯчейки2
ГДЕ
усЯчейки2.Помещение.Наименование = &Помещение2
И усЯчейки2.СтатусЯчейки = ЗНАЧЕНИЕ(Перечисление.усСтатусыЯчеек.Доступна)
Для случая, когда нам нужно вывести данные о ячейках по проездам в колонках, как это было в авторском примере, после выполнения одного запроса с оператором В, мы переходим к обработке полученных данных уже на стороне 1С. Мы получаем плоскую выборку данных и затем уже в коде 1С можем сгруппировать их или разложить по колонкам табличного документа. Это будет гораздо эффективнее и гибче, чем пытаться реализовать эту логику исключительно на языке запросов через сложные ПОЛНЫЕ СОЕДИНЕНИЯ.
В приведенном коде текст запроса формируется путем многократной конкатенации строк внутри цикла (ТекстЗапроса = ТекстЗапроса + "..."). Это считается плохой практикой по нескольким причинам:
Вместо конкатенации мы рекомендуем:
В, большинство параметров можно передать через Запрос.УстановитьПараметр().СтрЗаменить или СтрШаблон (для ограниченного динамизма): Если все же есть небольшие части запроса, которые должны быть динамическими (например, названия временных таблиц, что является плохой практикой само по себе), можно использовать функции СтрЗаменить() или СтрШаблон() для замены предопределенных "меток" в статическом тексте запроса. Однако, по возможности, стоит стремиться к тому, чтобы даже такие динамические части не мешали открытию запроса в конструкторе.
// Пример использования СтрЗаменить для очень ограниченной динамики (не рекомендуется для больших блоков)
ТекстШаблонаЗапроса = "ВЫБРАТЬ
| Поле1, Поле2
|ИЗ
| #ИмяИсточника# КАК Таблица1
|ГДЕ
| Таблица1.Поле1 = &Параметр1";
ТекстЗапроса = СтрЗаменить(ТекстШаблонаЗапроса, "#ИмяИсточника#", "Документ.РеализацияТоваровУслуг.Товары");
Запрос = Новый Запрос(ТекстЗапроса);
Запрос.УстановитьПараметр("Параметр1", ЗначениеПоиска);
Однако в большинстве случаев, как и в нашем примере с ячейками, динамическое формирование запроса через конкатенацию абсолютно излишне и может быть полностью заменено одним статическим запросом с передачей параметров.
Временные таблицы (ВТ) могут быть полезным инструментом для решения сложных задач, но их следует использовать обдуманно. ВТ могут повысить производительность и сделать сложные запросы более легкими для восприятия, когда требуется разбить сложный запрос на несколько логических этапов.
Давайте рассмотрим, когда временные таблицы действительно нужны и как их использовать эффективно:
ДОБАВИТЬ: С версии платформы 8.3.25 появилась возможность добавлять данные в уже существующую временную таблицу с помощью предложения ДОБАВИТЬ. Это может сократить количество "однотипных" временных таблиц и улучшить читаемость запроса, если вам нужно собрать данные из разных источников в одну ВТ. Например, если бы вам все же понадобилось собирать данные по проездам в ВТ, но из разных справочников/документов, это выглядело бы так:
// Пример использования ДОБАВИТЬ (для другого сценария, не для усЯчейки)
// ВТ1_СкладА
ВЫБРАТЬ
Документ.ПоступлениеТоваровУслуг.Товары.Номенклатура КАК Номенклатура,
Документ.ПоступлениеТоваровУслуг.Товары.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Товары
ИЗ
Документ.ПоступлениеТоваровУслуг.Товары КАК ПоступлениеТоваровУслугТовары
ГДЕ
ПоступлениеТоваровУслугТовары.Ссылка.Склад = &СкладА
;
// ДОБАВИТЬ данные из ВТ2_СкладБ
ДОБАВИТЬ
ВЫБРАТЬ
Документ.ПеремещениеТоваров.Товары.Номенклатура КАК Номенклатура,
Документ.ПеремещениеТоваров.Товары.Количество КАК Количество
ИЗ
Документ.ПеремещениеТоваров.Товары КАК ПеремещениеТоваровТовары
ГДЕ
ПеремещениеТоваровТовары.Ссылка.СкладОтправитель = &СкладБ
;
// Итоговый запрос
ВЫБРАТЬ
ВТ_Товары.Номенклатура,
СУММА(ВТ_Товары.Количество) КАК ОбщееКоличество
ИЗ
ВТ_Товары КАК ВТ_Товары
СГРУППИРОВАТЬ ПО
ВТ_Товары.Номенклатура
;
ИНДЕКСИРОВАТЬ ПО.
ВЫБРАТЬ
усЯчейки.Помещение,
усЯчейки.Проезд,
усЯчейки.Код,
усЯчейки.Ярус,
усЯчейки.Стеллаж
ПОМЕСТИТЬ ВТ_Ячейки
ИЗ
Справочник.усЯчейки КАК усЯчейки
ГДЕ
усЯчейки.Помещение.Наименование = &Помещение1
И усЯчейки.СтатусЯчейки = ЗНАЧЕНИЕ(Перечисление.усСтатусыЯчеек.Доступна)
ИНДЕКСИРОВАТЬ ПО
Проезд, Ярус, Стеллаж
;
// Далее используем ВТ_Ячейки для соединений или других операций
ВЫБРАТЬ
ВТ_Ячейки.Проезд,
ВТ_Ячейки.Ярус,
ВТ_Ячейки.Стеллаж,
ВТ_Ячейки.Код
ИЗ
ВТ_Ячейки КАК ВТ_Ячейки
УПОРЯДОЧИТЬ ПО
ВТ_Ячейки.Проезд,
ВТ_Ячейки.Ярус,
ВТ_Ячейки.Стеллаж
В нашем случае, поскольку мы можем получить все необходимые данные одним запросом с оператором В, создание временных таблиц для каждого проезда является избыточным и неэффективным.
Для создания эффективных и поддерживаемых запросов в 1С всегда следует придерживаться следующих принципов:
ГДЕ и ПО. Это значительно ускоряет выполнение запроса, поскольку СУБД может быстро находить нужные строки, не сканируя всю таблицу. Поля ссылок, даты, номера, коды, булевы значения часто бывают индексированы по умолчанию.ИЛИ для неиндексированных полей: Логический оператор ИЛИ в секции ГДЕ может помешать СУБД эффективно использовать индексы, если поля, объединенные через ИЛИ, не являются частью одного индекса или единственного поля индекса. Это может привести к полному сканированию таблиц. Если ИЛИ нельзя заменить на В, рассмотрите возможность разбиения запроса на несколько более мелких и объединения их результатов с помощью ОБЪЕДИНИТЬ ВСЕ.ПОЛНОЕ СОЕДИНЕНИЕ: Используйте его только тогда, когда это действительно необходимо. В большинстве случаев достаточно ВНУТРЕННЕЕ или ЛЕВОЕ/ПРАВОЕ СОЕДИНЕНИЕ.ТаблицаЗначений, Массив, Соответствие или Структура в коде 1С.Применяя эти подходы, мы сможем создавать не только рабочие, но и эффективные, читаемые и легко поддерживаемые запросы в 1С. Мы видим, что даже очень "плохой" код можно существенно улучшить, если понимать принципы работы СУБД и особенности языка запросов 1С.