При разработке функционала в 1С часто возникает задача по получению комбинированных данных из различных источников, таких как регистры сведений и регистры накопления, для последующего заполнения табличных частей документов. В этой ситуации мы сталкиваемся с необходимостью корректного связывания этих данных в запросе. Неправильно построенный запрос может привести к неожиданным результатам, таким как многократное дублирование записей, что затрудняет обработку информации и некорректно заполняет документы. Давайте подробно разберем, как избежать подобных проблем и сформировать запрос правильно.
Наш коллега столкнулся с распространенной ошибкой: данные остатков из регистра накопления складывались и записывались в каждую строчку табличной части при объединении с регистром сведений. Рассмотрим представленный им код запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НоменклатураСрезПоследних.Поставщик КАК Поставщик,
| НоменклатураСрезПоследних.Номенклатура КАК Номенклатура,
| НоменклатураСрезПоследних.МинимальноеКоличество КАК МинимальноеКоличество,
| ОстаткиНоменклатурыОстатки.ФактКоличествоОстаток КАК ФактКоличествоОстаток
|ИЗ
| РегистрСведений.Номенклатура.СрезПоследних КАК НоменклатураСрезПоследних,
| РегистрНакопления.ОстаткиНоменклатуры.Остатки КАК ОстаткиНоменклатурыОстатки";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
НовСтр = Объект.Остатки.Добавить();
НовСтр.ФактКоличество = ВыборкаДетальныеЗаписи.ФактКоличествоОстаток;
НовСтр.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
НовСтр.Поставщик = ВыборкаДетальныеЗаписи.Поставщик;
НовСтр.ПланКоличество = ВыборкаДетальныеЗаписи.МинимальноеКоличество;
КонецЦикла;
Проблема этого запроса заключается в том, что он объединяет два источника данных, РегистрСведений.Номенклатура.СрезПоследних и РегистрНакопления.ОстаткиНоменклатуры.Остатки, просто через запятую в секции ИЗ, без явного указания условия связи. В языке запросов 1С, как и в SQL, это приводит к формированию так называемого декартова произведения (перекрестного соединения). Это означает, что каждая запись из первого источника соединяется с каждой записью из второго источника. Если в регистре сведений 100 записей, а в регистре накопления 500 записей, то результат запроса будет содержать 100 * 500 = 50000 записей, большинство из которых будут лишними и неверными. Именно это и вызвало дублирование и некорректное "складывание" данных в табличной части.
Для того чтобы правильно связать данные и получить желаемый результат, нам нужно последовательно продумать структуру источников, требуемый результат и применить соответствующие операторы соединения.
Определяем источники данных и требуемый результат
Прежде всего, давайте уточним структуру наших регистров и что мы хотим получить:
Поставщик, НоменклатураМинимальноеКоличествоНоменклатураФактКоличество (предположим, что в виртуальной таблице оно называется ФактКоличествоОстаток)Нам необходимо заполнить табличную часть документа, где для каждой позиции номенклатуры из регистра сведений мы хотим получить связанного поставщика, минимальное количество и актуальный фактический остаток из регистра накопления.
Выбираем правильный тип соединения
Поскольку мы хотим получить все записи из регистра сведений (наш "основной" источник) и, если есть, соответствующие им остатки из регистра накопления, идеальным выбором будет ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN). Этот оператор гарантирует, что все записи из левой таблицы (в нашем случае, НоменклатураСрезПоследних) попадут в результат. Если для какой-либо номенклатуры не будет найден соответствующий остаток в правой таблице (ОстаткиНоменклатурыОстатки), то поля из правой таблицы будут заполнены значением NULL (или пустым значением соответствующего типа).
Ключевым моментом здесь является указание условия связи после оператора ПО. В нашем случае, оба регистра имеют измерение Номенклатура, по которому мы и будем их связывать:
ИЗ
РегистрСведений.Номенклатура.СрезПоследних КАК НоменклатураСрезПоследних
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиНоменклатуры.Остатки КАК ОстаткиНоменклатурыОстатки
ПО НоменклатураСрезПоследних.Номенклатура = ОстаткиНоменклатурыОстатки.Номенклатура
Это условие связывает записи только по идентичным значениям Номенклатура, предотвращая декартово произведение.
Параметризация запроса для отбора данных
Часто нам нужно не просто получить все данные, а отфильтровать их, например, по определенному поставщику или складу. Использование параметров в запросе — это лучшая практика. Она не только делает запрос гибким, но и значительно повышает производительность, позволяя платформе эффективнее использовать индексы и предотвращая SQL-инъекции.
Допустим, нам нужно получить данные только для конкретного поставщика, который указан в шапке нашего документа. Мы можем передать этот параметр прямо в виртуальную таблицу СрезПоследних:
РегистрСведений.Номенклатура.СрезПоследних(,Поставщик = &ПоставщикДокумента) КАК НоменклатураСрезПоследних
Или, если отбор должен быть по номенклатуре:
РегистрСведений.Номенклатура.СрезПоследних(,Номенклатура В (&СписокНоменклатуры)) КАК НоменклатураСрезПоследних
Перед выполнением запроса мы устанавливаем значение этого параметра:
Запрос.УстановитьПараметр("ПоставщикДокумента", Объект.Поставщик); // Предполагаем, что в документе есть реквизит "Поставщик"
Собираем все воедино, учитывая правильное соединение и параметризацию:
// Создаем новый запрос
Запрос = Новый Запрос;
// Формируем текст запроса
Запрос.Текст =
"ВЫБРАТЬ
| НоменклатураСрезПоследних.Поставщик КАК Поставщик,
| НоменклатураСрезПоследних.Номенклатура КАК Номенклатура,
| НоменклатураСрезПоследних.МинимальноеКоличество КАК МинимальноеКоличество,
| ЕСТЬNULL(ОстаткиНоменклатурыОстатки.ФактКоличествоОстаток, 0) КАК ФактКоличествоОстаток
|ИЗ
| РегистрСведений.Номенклатура.СрезПоследних(
| &МоментВремени, // Период, на который получаем срез. Можно указать ТЕКУЩАЯДАТА
| Поставщик = &ПоставщикДокумента // Пример отбора по поставщику
| ) КАК НоменклатураСрезПоследних
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиНоменклатуры.Остатки(
| &МоментВремени, // Период, на который получаем остатки
| Склад = &СкладДокумента // Пример отбора по складу, если есть такое измерение
| ) КАК ОстаткиНоменклатурыОстатки
| ПО НоменклатураСрезПоследних.Номенклатура = ОстаткиНоменклатурыОстатки.Номенклатура
|ГДЕ
| НоменклатураСрезПоследних.МинимальноеКоличество > 0"; // Пример дополнительного отбора
// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("МоментВремени", ТекущаяДата()); // Или Объект.Дата, если документ на определенную дату
Запрос.УстановитьПараметр("ПоставщикДокумента", Объект.Поставщик); // Предполагаем реквизит "Поставщик" в шапке документа
Запрос.УстановитьПараметр("СкладДокумента", Объект.Склад); // Предполагаем реквизит "Склад" в шапке документа
// Выполняем запрос
РезультатЗапроса = Запрос.Выполнить();
// Очищаем табличную часть перед заполнением, если нужно
Объект.Остатки.Очистить();
// Получаем выборку и заполняем табличную часть документа
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
НовСтр = Объект.Остатки.Добавить();
НовСтр.ФактКоличество = ВыборкаДетальныеЗаписи.ФактКоличествоОстаток;
НовСтр.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
НовСтр.Поставщик = ВыборкаДетальныеЗаписи.Поставщик;
НовСтр.ПланКоличество = ВыборкаДетальныеЗаписи.МинимальноеКоличество;
КонецЦикла;
Обратите внимание на использование функции ЕСТЬNULL(). Так как мы используем ЛЕВОЕ СОЕДИНЕНИЕ, для номенклатуры, по которой нет остатков, поле ФактКоличествоОстаток будет NULL. ЕСТЬNULL(ОстаткиНоменклатурыОстатки.ФактКоличествоОстаток, 0) позволяет заменить NULL на 0, что обычно желательно для количественных показателей.
СрезПоследних и ОстаткиДля эффективной работы с запросами в 1С мы должны понимать, как работают виртуальные таблицы регистров.
Виртуальные таблицы
СрезПоследних (для регистров сведений) и Остатки (для регистров накопления) — это не физические таблицы базы данных, а виртуальные таблицы, которые платформа 1С генерирует на лету. Они предназначены для получения агрегированных данных на определенный момент времени без необходимости ручной обработки всех движений.
СрезПоследних
Эта таблица предоставляет актуальные значения измерений и ресурсов регистра сведений на заданную дату (или текущий момент, если дата не указана). Она возвращает только одну, самую последнюю запись для каждой уникальной комбинации измерений, существовавшей на указанный момент времени. У нее есть параметр Период для указания даты среза и параметры отбора по измерениям.
Остатки
Эта виртуальная таблица предназначена для получения итоговых значений ресурсов (остатков) регистра накопления по его измерениям на определенную дату. Она оперирует только оборотами регистра и таблицами остатков (если регистр ведет остатки). У нее также есть параметры: Период (или НачалоПериода/КонецПериода в других виртуальных таблицах, таких как ОстаткиИОбороты) и параметры отбора по измерениям.
Параметры виртуальных таблиц
Критически важно правильно использовать параметры виртуальных таблиц. Если мы передаем параметры Период или отборы по измерениям непосредственно в скобках после имени виртуальной таблицы, платформа 1С может значительно оптимизировать выполнение запроса, поскольку она будет выполнять агрегацию и фильтрацию уже на уровне временной таблицы или даже на уровне СУБД, что гораздо эффективнее, чем фильтровать весь результат после его получения.
При работе со сложными запросами, объединяющими несколько виртуальных таблиц или обрабатывающими большие объемы данных, мы всегда должны думать об оптимизации:
Вложенные запросы и производительность
Платформа 1С при обращении к виртуальным таблицам генерирует вложенные SQL-запросы. Соединения с такими вложенными запросами могут замедлять выполнение, если СУБД выбирает неоптимальный план. Особенно это заметно, когда виртуальные таблицы не имеют прямых параметров отбора, и СУБД приходится обрабатывать большой объем данных.
Использование временных таблиц
Для повышения производительности, особенно в сложных запросах с несколькими виртуальными таблицами, рекомендуется выносить обращения к виртуальным таблицам в отдельные запросы с сохранением их результатов во временные таблицы. Это позволяет СУБД сначала сформировать небольшие, уже отфильтрованные и агрегированные наборы данных, а затем уже соединять эти временные таблицы. Такой подход часто бывает быстрее, чем однократный сложный запрос.
// Пример использования временных таблиц
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НоменклатураСрезПоследних.Поставщик КАК Поставщик,
| НоменклатураСрезПоследних.Номенклатура КАК Номенклатура,
| НоменклатураСрезПоследних.МинимальноеКоличество КАК МинимальноеКоличество
|ПОМЕСТИТЬ ВТ_Номенклатура
|ИЗ
| РегистрСведений.Номенклатура.СрезПоследних(
| &МоментВремени,
| Поставщик = &ПоставщикДокумента
| ) КАК НоменклатураСрезПоследних;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ОстаткиНоменклатурыОстатки.Номенклатура КАК Номенклатура,
| ОстаткиНоменклатурыОстатки.ФактКоличествоОстаток КАК ФактКоличествоОстаток
|ПОМЕСТИТЬ ВТ_Остатки
|ИЗ
| РегистрНакопления.ОстаткиНоменклатуры.Остатки(
| &МоментВремени,
| Склад = &СкладДокумента
| ) КАК ОстаткиНоменклатурыОстатки;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Номенклатура.Поставщик,
| ВТ_Номенклатура.Номенклатура,
| ВТ_Номенклатура.МинимальноеКоличество,
| ЕСТЬNULL(ВТ_Остатки.ФактКоличествоОстаток, 0) КАК ФактКоличествоОстаток
|ИЗ
| ВТ_Номенклатура КАК ВТ_Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Остатки КАК ВТ_Остатки
| ПО ВТ_Номенклатура.Номенклатура = ВТ_Остатки.Номенклатура";
Такой подход делает запрос более читаемым и часто более производительным, особенно на больших базах данных.
Хотя построчное заполнение табличной части через цикл Пока Выборка.Следующий() является вполне рабочим вариантом, в 1С есть и более быстрые методы, особенно если поля результата запроса совпадают по именам и типам с полями табличной части документа.
Метод Загрузить()
Мы можем использовать метод Загрузить() объекта табличной части. Этот метод позволяет загрузить все данные из выгруженного результата запроса одной операцией, что значительно быстрее, чем построчное добавление строк в цикле, особенно при большом количестве записей.
// ... (Запрос и его выполнение остаются прежними) ...
// Выполняем запрос
РезультатЗапроса = Запрос.Выполнить();
// Очищаем табличную часть перед заполнением
Объект.Остатки.Очистить();
// Выгружаем результат запроса в ТаблицуЗначений и загружаем в табличную часть
Если РезультатЗапроса.Количество() > 0 Тогда
Объект.Остатки.Загрузить(РезультатЗапроса.Выгрузить());
КонецЕсли;
Для корректной работы метода Загрузить() имена колонок в результате запроса должны совпадать с именами реквизитов табличной части документа. Например, если в запросе поле называется ФактКоличествоОстаток, а в табличной части реквизит ФактКоличество, то придется либо переименовать поле в запросе (ФактКоличествоОстаток КАК ФактКоличество), либо использовать вспомогательную таблицу значений для маппинга полей.
Автоматическое заполнение по кнопке
Как было упомянуто в исходной задаче, автоматическое заполнение табличной части через кнопку — это удобное решение для пользователя. Вся описанная логика с запросом и заполнением табличной части должна быть помещена в обработчик команды, привязанной к кнопке на форме документа.
Таким образом, мы видим, что для успешного решения задачи объединения данных из разных регистров в 1С критически важно правильно использовать операторы соединения (такие как ЛЕВОЕ СОЕДИНЕНИЕ) с корректными условиями связи ПО, а также эффективно применять параметризацию запросов. Эти подходы позволяют получать точные данные, оптимизировать производительность и создавать удобный функционал для пользователей.