При разработке механизмов заполнения документов в системе «1С:Предприятие» программисты часто сталкиваются с неприятным эффектом: строки в заполняемой табличной части начинают неконтролируемо двоиться или троиться. Классический сценарий возникновения этой проблемы — левое соединение (LEFT JOIN) исходной табличной части документа с виртуальной таблицей остатков регистра накопления (например, ПартииТоваров.Остатки).
В этой статье мы подробно разберем, почему возникает этот эффект (декартово произведение), проанализируем исходный проблемный запрос и рассмотрим пять различных архитектурных решений — от простых группировок до использования современных оконных функций и временных таблиц.
Давайте проанализируем классическую ситуацию, описанную автором темы. В документе КорректировкаРеализации есть табличная часть Товары. Программист хочет дополнить данные этой табличной части информацией о партии — получить поля ДокументОприходования и ДокументПеремещения из регистра остатков партий. Он пишет следующий запрос:
ВЫБРАТЬ
КорректировкаРеализацииТовары.Номенклатура,
КорректировкаРеализацииТовары.НомерСтроки,
КорректировкаРеализацииТовары.Цена,
КорректировкаРеализацииТовары.КоличествоДоИзменения - КорректировкаРеализацииТовары.Количество КАК Количество,
КорректировкаРеализацииТовары.СерияНоменклатуры,
ПартииТоваровОстатки.ДокументОприходования,
ПартииТоваровОстатки.ДокументПеремещения,
КорректировкаРеализацииТовары.СуммаДоИзменения - КорректировкаРеализацииТовары.Сумма КАК Сумма
ИЗ
Документ.КорректировкаРеализации.Товары КАК КорректировкаРеализацииТовары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПартииТоваров.Остатки КАК ПартииТоваровОстатки
ПО КорректировкаРеализацииТовары.Номенклатура = ПартииТоваровОстатки.Номенклатура
И КорректировкаРеализацииТовары.СерияНоменклатуры = ПартииТоваровОстатки.СерияНоменклатуры
И КорректировкаРеализацииТовары.Склад = ПартииТоваровОстатки.Склад
ГДЕ
КорректировкаРеализацииТовары.Ссылка В(&Список)
И КорректировкаРеализацииТовары.ОтражатьТоварныйУчет = ИСТИНА
На первый взгляд всё кажется логичным. Но как только этот запрос выполняется в реальной базе данных, количество строк в выборке внезапно превышает количество строк в самом документе. Почему это происходит?
Дело в природе левого соединения. Связь между табличной частью документа и виртуальной таблицей остатков работает по принципу «один-ко-многим». В документе строка с конкретным сочетанием Номенклатура, СерияНоменклатуры и Склад является уникальной (одна строка). Однако в регистре партий на остатках по этому же складу и этой номенклатуре может находиться несколько различных партий (с разными документами оприходования или перемещения). СУБД добросовестно сопоставляет строку документа с каждой подходящей строкой из регистра остатков. В результате одна исходная строка размножается в выборке столько раз, сколько партий найдено на остатках.
Это приводит к фатальной ошибке в расчетах: поле Количество, вычисляемое как разница исходных полей документа, дублируется во всех получившихся строках. При последующей записи этих данных в новый документ «Заказ» общее количество товара будет многократно завышено.
---
Самый простой и быстрый способ избавиться от дублей прямо в запросе — свернуть дублирующиеся записи с помощью группировки и агрегатных функций. Если для бизнес-логики не критично, какую именно партию подтянуть (например, достаточно взять любую актуальную или последнюю по алфавиту), мы можем использовать функцию МАКСИМУМ для полей документов партии.
Рассмотрим, как будет выглядеть такой запрос:
ВЫБРАТЬ
КорректировкаРеализацииТовары.Номенклатура КАК Номенклатура,
КорректировкаРеализацииТовары.НомерСтроки КАК НомерСтроки,
КорректировкаРеализацииТовары.Цена КАК Цена,
КорректировкаРеализацииТовары.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
КорректировкаРеализацииТовары.СтавкаНДС КАК СтавкаНДС,
КорректировкаРеализацииТовары.КоличествоДоИзменения - КорректировкаРеализацииТовары.Количество КАК Количество,
КорректировкаРеализацииТовары.СерияНоменклатуры КАК СерияНоменклатуры,
МАКСИМУМ(ПартииТоваровОстатки.ДокументОприходования) КАК ДокументОприходования,
МАКСИМУМ(ПартииТоваровОстатки.ДокументПеремещения) КАК ДокументПеремещения,
КорректировкаРеализацииТовары.СуммаДоИзменения - КорректировкаРеализацииТовары.Сумма КАК Сумма
ИЗ
Документ.КорректировкаРеализации.Товары КАК КорректировкаРеализацииТовары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПартииТоваров.Остатки КАК ПартииТоваровОстатки
ПО КорректировкаРеализацииТовары.Номенклатура = ПартииТоваровОстатки.Номенклатура
И КорректировкаРеализацииТовары.СерияНоменклатуры = ПартииТоваровОстатки.СерияНоменклатуры
И КорректировкаРеализацииТовары.Склад = ПартииТоваровОстатки.Склад
ГДЕ
КорректировкаРеализацииТовары.Ссылка В(&Список)
И КорректировкаРеализацииТовары.ОтражатьТоварныйУчет = ИСТИНА
ГРУППИРОВАТЬ ПО
КорректировкаРеализацииТовары.Номенклатура,
КорректировкаРеализацииТовары.НомерСтроки,
КорректировкаРеализацииТовары.Цена,
КорректировкаРеализацииТовары.ЕдиницаИзмерения,
КорректировкаРеализацииТовары.СтавкаНДС,
КорректировкаРеализацииТовары.КоличествоДоИзменения - КорректировкаРеализацииТовары.Количество,
КорректировкаРеализацииТовары.СерияНоменклатуры,
КорректировкаРеализацииТовары.СуммаДоИзменения - КорректировкаРеализацииТовары.Сумма
Преимущества метода: Простота реализации. Строки гарантированно перестанут двоиться.
Недостатки метода: Ссылка на документ оприходования выбирается формально (берется максимальная ссылка по ее внутреннему уникальному идентификатору UUID). Это может не соответствовать реальной хронологии или партионному учету ФИФО/ЛИФО.
---
С точки зрения стандартов разработки «1С», исходный запрос содержит грубейшую ошибку производительности: в нем соединяется физическая таблица документа с виртуальной таблицей остатков без параметров отбора. СУБД вынуждена рассчитывать остатки по всему регистру партий в базе данных, а уже затем соединять этот огромный массив с парой строк документа.
Давайте спроектировать правильный, быстрый и надежный алгоритм с использованием Временных Таблиц (ВТ):
Посмотрим на пример реализации такого пакета запросов:
// ПАКЕТ ЗАПРОСОВ
// Шаг 1: Выбираем уникальные товары из документа
ВЫБРАТЬ РАЗЛИЧНЫЕ
ТоварыДокумента.Номенклатура КАК Номенклатура,
ТоварыДокумента.СерияНоменклатуры КАК СерияНоменклатуры,
ТоварыДокумента.Склад КАК Склад
ПОМЕСТИТЬ ВТ_ТоварыДокумента
ИЗ
Документ.КорректировкаRealization.Товары КАК ТоварыДокумента
ГДЕ
ТоварыДокумента.Ссылка В(&Список)
И ТоварыДокумента.ОтражатьТоварныйУчет = ИСТИНА
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 2: Получаем остатки партий только по отфильтрованным товарам
ВЫБРАТЬ
ПартииОстатки.Номенклатура КАК Номенклатура,
ПартииОстатки.СерияНоменклатуры КАК СерияНоменклатуры,
ПартииОстатки.Склад КАК Склад,
МАКСИМУМ(ПартииОстатки.ДокументОприходования) КАК ДокументОприходования,
МАКСИМУМ(ПартииОстатки.ДокументПеремещения) КАК ДокументПеремещения
ПОМЕСТИТЬ ВТ_СхлопнутыеОстатки
ИЗ
РегистрНакопления.ПартииТоваров.Остатки(
&Период,
(Номенклатура, СерияНоменклатуры, Склад) В
(ВЫБРАТЬ
ВТ.Номенклатура,
ВТ.СерияНоменклатуры,
ВТ.Склад
ИЗ
ВТ_ТоварыДокумента КАК ВТ)) КАК ПартииОстатки
ГРУППИРОВАТЬ ПО
ПартииОстатки.Номенклатура,
ПартииОстатки.СерияНоменклатуры,
ПартииОстатки.Склад
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 3: Соединяем исходный документ с уникальными остатками
ВЫБРАТЬ
КорректировкаРеализацииТовары.Номенклатура,
КорректировкаРеализацииТовары.НомерСтроки,
КорректировкаРеализацииТовары.Цена,
КорректировкаРеализацииТовары.ЕдиницаИзмерения,
КорректировкаРеализацииТовары.СтавкаНДС,
КорректировкаРеализацииТовары.КоличествоДоИзменения - КорректировкаРеализацииТовары.Количество КАК Количество,
КорректировкаРеализацииТовары.СерияНоменклатуры,
ЕСТЬNULL(ВТ_Остатки.ДокументОприходования, ЗНАЧЕНИЕ(Документ.ПоступлениеТоваровУслуг.ПустаяСсылка)) КАК ДокументОприходования,
ЕСТЬNULL(ВТ_Остатки.ДокументПеремещения, ЗНАЧЕНИЕ(Документ.ПеремещениеТоваров.ПустаяСсылка)) КАК ДокументПеремещения,
КорректировкаРеализацииТовары.СуммаДоИзменения - КорректировкаРеализацииТовары.Сумма КАК Сумма
ИЗ
Документ.КорректировкаРеализации.Товары КАК КорректировкаРеализацииТовары
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_СхлопнутыеОстатки КАК ВТ_Остатки
ПО КорректировкаРеализацииТовары.Номенклатура = ВТ_Остатки.Номенклатура
И КорректировкаРеализацииТовары.СерияНоменклатуры = ВТ_Остатки.СерияНоменклатуры
И КорректировкаРеализацииТовары.Склад = ВТ_Остатки.Склад
ГДЕ
КорректировкаРеализацииТовары.Ссылка В(&Список)
И КорректировкаРеализацииТовары.ОтражатьТоварныйУчет = ИСТИНА
Этот способ является золотым стандартом разработки. Он производителен, надежен и гарантирует отсутствие дублирования, поскольку соединение происходит по уникальным ключам временной таблицы остатков.
---
Если вы работаете на современной версии платформы «1С:Предприятие» (начиная с релиза 8.3.20), вам доступен мощный инструмент — оконные функции в языке запросов. Это позволяет пронумеровать партии для каждой номенклатуры по определенному правилу (например, по дате партии в порядке убывания) и выбрать строго первую запись.
Рассмотрим, как красиво решить задачу с помощью функции ROW_NUMBER():
ВЫБРАТЬ
ПартииОстатки.Номенклатура КАК Номенклатура,
ПартииОстатки.СерияНоменклатуры КАК СерияНоменклатуры,
ПартииОстатки.Склад КАК Склад,
ПартииОстатки.ДокументОприходования КАК ДокументОприходования,
ПартииОстатки.ДокументПеремещения КАК ДокументПеремещения,
ROW_NUMBER() OVER (
PARTITION BY ПартииОстатки.Номенклатура, ПартииОстатки.СерияНоменклатуры, ПартииОстатки.Склад
ORDER BY ПартииОстатки.ДокументОприходования.МоментВремени УБЫВ
) КАК ИндексСтроки
ПОМЕСТИТЬ ВТ_ОстаткиСИндексом
ИЗ
РегистрНакопления.ПартииТоваров.Остатки КАК ПартииОстатки
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
КорректировкаРеализацииТовары.Номенклатура,
КорректировкаРеализацииТовары.НомерСтроки,
КорректировкаРеализацииТовары.Цена,
КорректировкаРеализацииТовары.КоличествоДоИзменения - КорректировкаРеализацииТовары.Количество КАК Количество,
ВТ.ДокументОприходования,
ВТ.ДокументПеремещения
ИЗ
Документ.КорректировкаРеализации.Товары КАК КорректировкаРеализацииТовары
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиСИндексом КАК ВТ
ПО КорректировкаРеализацииТовары.Номенклатура = ВТ.Номенклатура
И КорректировкаРеализацииТовары.СерияНоменклатуры = ВТ.СерияНоменклатуры
И КорректировкаРеализацииТовары.Склад = ВТ.Склад
И ВТ.ИндексСтроки = 1 // Выбираем только одну, самую свежую партию!
ГДЕ
КорректировкаРеализацииТовары.Ссылка В(&Список)
Использование оконных функций позволяет гибко управлять тем, какая именно партия будет выбрана (первая, последняя, с наибольшим остатком и т.д.), полностью исключая размножение строк.
---
В финале обсуждения автор темы пишет: «В итоге достаточно было взять обычный регистр и связать по регистратору». Разберем этот подход. Вместо обращения к таблице Остатки автор обратился к физической таблице движений регистра, наложив условие связи по регистратору (самому документу корректировки):
ВЫБРАТЬ
КорректировкаРеализацииТовары.Номенклатура,
ПартииТоваров.ДокументОприходования,
ПартииТоваров.ДокументПеремещения
ИЗ
Документ.КорректировкаРеализации.Товары КАК КорректировкаРеализацииТовары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПартииТоваров КАК ПартииТоваров
ПО КорректировкаРеализацииТовары.Ссылка = ПартииТоваров.Регистратор
И КорректировкаРеализацииТовары.Номенклатура = ПартииТоваров.Номенклатура
Важное предупреждение: Хотя для автора это решение оказалось рабочим, данный способ имеет критические недостатки, о которых необходимо знать:
КорректировкаРеализации уже проведен и успешно сделал движения по регистру. Если пользователь пытается заполнить табличную часть в новом, еще не записанном или не проведенном документе, запрос вернет пустое значение (так как движений в базе данных еще нет).Регистратор на больших базах данных может выполняться медленнее, чем обращение к оптимизированным индексам виртуальных таблиц.---
Если бизнес-требования все же обязывают вывести пользователю все имеющиеся партии (и дублирование на уровне СУБД неизбежно), но при этом нельзя ломать расчет количества, мы можем перенести логику схлопывания на уровень встроенного языка 1С.
Для этого в запросе используются ИТОГИ, а обход выборки выполняется в двойном цикле:
// В запросе добавляем секцию ИТОГИ
Запрос.Текст =
"ВЫБРАТЬ
| КорректировкаРеализацииТовары.Номенклатура КАК Номенклатура,
| КорректировкаРеализацииТовары.КоличествоДоИзменения - КорректировкаРеализацииТовары.Количество КАК Количество,
| ПартииТоваровОстатки.ДокументОприходования КАК ДокументОприходования
|ИЗ
| Документ.КорректировкаРеализации.Товары КАК КорректировкаРеализацииТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПартииТоваров.Остатки КАК ПартииТоваровОстатки
| ПО КорректировкаРеализацииТовары.Номенклатура = ПартииТоваровОстатки.Номенклатура
|ИТОГИ СУММА(Количество) ПО Номенклатура";
ВыборкаНоменклатура = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНоменклатура.Следующий() Цикл
// 1. Создаем строку в документе "Заказ" один раз для уникальной номенклатуры
НоваяСтрока = ОбъектЗаказ.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаНоменклатура); // Количество заполнится корректно (один раз)
// 2. Вложенным циклом собираем детальные записи о партиях, если они нужны
ВыборкаДетальные = ВыборкаНоменклатура.Выбрать();
СтрокаПартий = "";
Пока ВыборкаДетальные.Следующий() Цикл
Если ЗначениеЗаполнено(ВыборкаДетальные.ДокументОприходования) Тогда
СтрокаПартий = СтрокаПартий + Строка(ВыборкаДетальные.ДокументОприходования) + "; ";
КонецЕсли;
КонецЦикла;
НоваяСтрока.КомментарийКПарии = СтрокаПартий; // Записываем собранную информацию в строку
КонецЦикла;
Этот метод позволяет сохранить корректное суммарное количество товара и одновременно аккумулировать информацию обо всех связанных документах партии без заведения дублирующих строк в целевом документе.
Мы проанализировали проблему дублирования данных при левом соединении табличной части с регистрами. Для качественного решения этой задачи в реальных проектах рекомендуем использовать Решение №2 (Временные таблицы с параметризацией) как наиболее производительное и универсальное, либо Решение №3 (Оконные функции), если вы работаете на современных версиях платформы 1С.