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