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