При разработке сложных отчетов в системе «1С:Предприятие» программисты часто сталкиваются с необходимостью объединения разнородных данных: остатков на определенную дату, оборотов за период и дополнительных аналитических разрезов (например, исполнителей или сотрудников). Если архитектура запроса изначально спроектирована неверно, попытка добавить временной интервал или новый отбор может завести разработчика в тупик. Именно это произошло в обсуждаемой теме на форуме, где автор в итоге принял компромиссное и некорректное решение — полностью отказаться от использования периода.
В этой статье мы подробно разберем, почему у автора возникли сложности, проанализируем критические ошибки в его коде и построим правильный, оптимизированный запрос с использованием временных таблиц, который позволит корректно накладывать отборы и по периоду, и по сотруднику.
Для начала проанализируем финальный вариант запроса, который автор предоставил в сообщении 13. В нем можно выделить несколько фундаментальных проблем, которые приводят как к неверным результатам расчета, так и к колоссальному падению производительности СУБД.
Ошибка 1. Несогласованность временных интервалов в виртуальных таблицах
В тексте запроса автор использует виртуальную таблицу остатков без указания параметров даты:
РегистрНакопления.РасчетУкомплектованныхИзделий.Остатки(, )
Когда параметры виртуальной таблицы остатков не заполнены, платформа 1С рассчитывает итоги на самую актуальную дату в системе (на текущий момент времени). В то же время во вложенных запросах автор обращается к таблице остатков и оборотов за конкретный период:
РегистрНакопления.РасчетУкомплектованныхИзделий.ОстаткиИОбороты(&НачПериода, &КонПериода, , , )
В результате СУБД пытается соединить актуальные текущие остатки с историческими оборотами за прошлый месяц. Это методологическая ошибка: данные теряют сопоставимость, а итоговые расчетные поля (такие как «Недокроено») будут возвращать неверные значения.
Ошибка 2. Обращение к физической таблице регистра накопления без отборов
В попытке получить сотрудника в сообщении 13 автор пишет следующий подзапрос:
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
РасчетУкомплектованныхИзделий.Регистратор.Пользователь.Сотрудник КАК РегистраторПользовательСотрудник,
РасчетУкомплектованныхИзделий.Изделие КАК Изделие,
...
ИЗ
РегистрНакопления.РасчетУкомплектованныхИзделий КАК РасчетУкомплектованныхИзделий
) КАК ВложенныйЗапрос3
Обращение напрямую к таблице движения регистра РегистрНакопления.РасчетУкомплектованныхИзделий (вместо виртуальных таблиц) заставляет СУБД сканировать абсолютно все записи в базе данных с самого начала ведения учета. На реальной базе это приведет к зависанию системы и превышению времени ожидания блокировок.
Ошибка 3. Разыменование ссылок через «две точки» от Регистратора
Конструкция Регистратор.Пользователь.Сотрудник является классическим примером антипаттерна производительности в 1С. Поле Регистратор имеет составной тип данных. При выполнении такого запроса СУБД вынуждена строить неявные левые соединения (LEFT JOIN) со всеми таблицами документов, которые могут выступать регистраторами для данного регистра, затем соединять их с таблицей справочника «Пользователи» и далее со справочником «Сотрудники». Число соединений на уровне SQL возрастает лавинообразно, уничтожая любую оптимизацию плана запроса.
Ошибка 4. Глубокая вложенность запросов
Использование конструкций вида ВложенныйЗапрос1, который соединяется с ВложенныйЗапрос2, внутри которого находится еще один ВложенныйЗапрос, делает код нечитаемым и лишает оптимизатор СУБД возможности построить эффективный план выполнения. Единственный верный способ избавиться от этой проблемы — использовать механизм временных таблиц.
---
Давайте разработаем надежное и быстрое решение. Наша задача — получить остатки и обороты в разрезе изделий, видов тканей и размеров за определенный период времени, а также наложить отбор по сотруднику, который выполнял операции укомплектования.
Для начала выделим уникальные комбинации изделий, размеров и видов тканей, которые нас интересуют. Ограничим выборку конкретным изделием, переданным в параметрах. Поместим результат во временную таблицу и проиндексируем ее для быстрого соединения на следующих этапах.
ВЫБРАТЬ
ИзделиеВидыТкани.Ссылка КАК Изделие,
ИзделиеВидыТкани.ВидТкани КАК ВидТкани,
Размеры.Ссылка КАК Размер
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
Справочник.Изделие.ВидыТкани КАК ИзделиеВидыТкани
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Размеры КАК Размеры
ПО ИзделиеВидыТкани.Ссылка = Размеры.Владелец
ГДЕ
ИзделиеВидыТкани.Ссылка = &Изделие
ИНДЕКСИРОВАТЬ ПО
Изделие,
ВидТкани,
Размер
;
Поскольку «Сотрудник» в нашей структуре данных является реквизитом документа-регистратора (или реквизитом автора документа), мы не можем получить его напрямую из виртуальной таблицы остатков. Нам необходимо обратиться к оборотам за выбранный период. Чтобы избежать сканирования всего регистра, мы наложим жесткий отбор по периоду и по нашему списку изделий.
Чтобы минимизировать негативный эффект от разыменования поля Регистратор, мы предварительно отберем только те типы документов, которые реально делают движения по регистру в нашей бизнес-логике (например, «ВыпускПродукции» или «КомплектацияИзделий»).
ВЫБРАТЬ
ДвиженияРегистра.Изделие КАК Изделие,
ДвиженияРегистра.Размер КАК Размер,
ДвиженияРегистра.ВидТкани КАК ВидТкани,
ДвиженияРегистра.КоличествоРасход / ДвиженияРегистра.Изделие.КоличествоВидовТканей КАК Укомплектовано,
ДвиженияРегистра.Регистратор.Пользователь.Сотрудник КАК Сотрудник
ПОМЕСТИТЬ ВТ_ОборотыИСотрудники
ИЗ
РегистрНакопления.РасчетУкомплектованныхИзделий КАК ДвиженияРегистра
ГДЕ
ДвиженияРегистра.Период МЕЖДУ &НачПериода И &КонПериода
И ДвиженияРегистра.Изделие = &Изделие
И ДвиженияРегистра.Регистратор ССЫЛКА Документ.ВыпускПродукции // Ограничиваем составной тип регистратора
И ДвиженияРегистра.Регистратор.Пользователь.Сотрудник = &ОтборСотрудник // Накладываем отбор на ранней стадии
ИНДЕКСИРОВАТЬ ПО
Изделие,
ВидТкани,
Размер
;
Важное примечание: Если на предприятии один и тот же размер изделия за один период могут комплектовать разные сотрудники, данные необходимо дополнительно сгруппировать по комбинации «Изделие, Размер, ВидТкани, Сотрудник».
Теперь получим остатки по регистру укомплектованных изделий. Чтобы остатки соответствовали выбранному временному интервалу, мы передаем параметр &КонПериода в первый параметр виртуальной таблицы. Для обеспечения максимальной производительности отбор по изделию передается во второй параметр виртуальной таблицы, а не в секцию ГДЕ.
ВЫБРАТЬ
РасчетОстатки.Изделие КАК Изделие,
РасчетОстатки.ВидТкани КАК ВидТкани,
РасчетОстатки.Размер КАК Размер,
РасчетОстатки.КоличествоОстаток КАК КоличествоОстаток
ПОМЕСТИТЬ ВТ_ОстаткиНаКонецПериода
ИЗ
РегистрНакопления.РасчетУкомплектованныхИзделий.Остатки(&КонПериода, Изделие = &Изделие) КАК РасчетОстатки
ИНДЕКСИРОВАТЬ ПО
Изделие,
ВидТкани,
Размер
;
Аналогичным образом мы можем получить остатки по партиям в производстве на ту же самую конечную дату периода:
ВЫБРАТЬ
ПартииОстатки.Изделие КАК Изделие,
ПартииОстатки.Размер КАК Размер,
ПартииОстатки.КоличествоОстаток КАК КоличествоОстаток
ПОМЕСТИТЬ ВТ_ПартииВПроизводстве
ИЗ
РегистрНакопления.ПартииВПроизводство.Остатки(&КонПериода, Изделие = &Изделие) КАК ПартииОстатки
ИНДЕКСИРОВАТЬ ПО
Изделие,
Размер
;
Имея подготовленные, легкие и проиндексированные временные таблицы, мы можем легко связать их между собой в основном запросе пакета. Все соединения выполняются по ключевым полям связи (Изделие, Размер, ВидТкани), что гарантирует моментальное выполнение запроса СУБД.
ВЫБРАТЬ
ИсходныеДанные.Изделие КАК Изделие,
ИсходныеДанные.ВидТкани КАК ВидТкани,
ИсходныеДанные.Размер КАК Размер,
// Расчетные показатели
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) + ЕСТЬNULL(Обороты.Укомплектовано, 0) КАК Раскроено,
ЕСТЬNULL(Обороты.Укомплектовано, 0) КАК Укомплектовано,
ЕСТЬNULL(Партии.КоличествоОстаток, 0) КАК Запланировано,
ИсходныеДанные.Изделие.Артикул КАК ИзделиеАртикул,
Обороты.Сотрудник КАК Сотрудник
ИЗ
ВТ_ИсходныеДанные КАК ИсходныеДанные
// Соединяем с остатками на конец периода
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиНаКонецПериода КАК Остатки
ПО ИсходныеДанные.Изделие = Остатки.Изделие
И ИсходныеДанные.ВидТкани = Остатки.ВидТкани
И ИсходныеДанные.Размер = Остатки.Размер
// Соединяем с оборотами за период (где применен отбор по сотруднику)
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОборотыИСотрудники КАК Обороты
ПО ИсходныеДанные.Изделие = Обороты.Изделие
И ИсходныеДанные.ВидТкани = Обороты.ВидТкани
И ИсходныеДанные.Размер = Обороты.Размер
// Соединяем с остатками партий в производстве
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПартииВПроизводстве КАК Партии
ПО ИсходныеДанные.Изделие = Партии.Изделие
И ИсходныеДанные.Размер = Партии.Размер
;
---
При передаче конечной даты периода (&КонПериода) в виртуальные таблицы 1С важно учитывать стандартное поведение платформы. Если в параметр передать просто значение типа Дата (например, 31.12.2023 23:59:59), то движения документов, записанные ровно в эту секунду, могут не попасть в расчет остатков.
Чтобы избежать этой проблемы, в коде инициализации запроса на встроенном языке всегда передавайте параметр конца периода, используя специальный объект Граница с видом границы Включая. Рассмотрим пример кода:
ГраницаКонцаПериода = Новый Граница(КонецДня(КонецПериода), ВидГраницы.Включая);
Запрос = Новый Запрос;
Запрос.Текст = "..."; // Текст нашего оптимизированного запроса
Запрос.УстановитьПараметр("НачПериода", НачалоДня(НачалоПериода));
Запрос.УстановитьПараметр("КонПериода", ГраницаКонцаПериода);
Запрос.УстановитьПараметр("Изделие", ВыбранноеИзделие);
Запрос.УстановитьПараметр("ОтборСотрудник", ВыбранныйСотрудник);
РезультатЗапроса = Запрос.Выполнить();
Отказ от использования временных интервалов и периодов из-за технических сложностей составления запроса — это тупиковый путь, который приводит к накоплению ошибок в логике приложения. Переход от тяжелых вложенных запросов к структурированным временным таблицам позволяет решить сразу несколько задач: