Разработка эффективных и отказоустойчивых запросов в системе «1С:Предприятие» — одна из ключевых задач программиста. Часто при связывании документов продажи и заказов возникают неочевидные ошибки: некорректно обрабатываются пустые значения (NULL), удваиваются или утраиваются суммы из-за связи «один ко многим», а в механизме проведения документы начинают вести себя странно, возвращая суммы еще не проведенных транзакций. В этой статье мы подробно разберем, почему возникают эти проблемы и как их правильно решать на практических примерах.
Начнем с классической ошибки, с которой часто сталкиваются начинающие разработчики. При левом или полном соединении таблиц в полях, где нет соответствия, СУБД возвращает специальное значение — NULL. Это не ноль, не пустая строка и не неопределенное значение. Это обозначение отсутствия данных.
Проанализируем типичный ошибочный код, в котором разработчик пытается сравнить суммы заказа и реализации:
ВЫБРАТЬ
СУММА(ВЫБОР
КОГДА ЗаказыРеализации.СуммаЗаказов = ЗаказыРеализации.СуммаРеализации
ТОГДА ЗаказыРеализации.СуммаРеализации
КОГДА ЗаказыРеализации.СуммаЗаказов > ЗаказыРеализации.СуммаРеализации
ТОГДА ЗаказыРеализации.СуммаЗаказов
КОГДА ЗаказыРеализации.СуммаЗаказов < ЗаказыРеализации.СуммаРеализации
ТОГДА ЗаказыРеализации.СуммаРеализации
КОНЕЦ) КАК СуммаЗаПериод
Если хотя бы одно из полей (СуммаЗаказов или СуммаРеализации) содержит NULL, то любые математические операции сравнения (равно, больше, меньше) вернут ложь или неопределенность. В результате ни одно условие КОГДА не выполнится. Поскольку ветка ИНАЧЕ в конструкции ВЫБОР отсутствует, вся конструкция вернет NULL. Функция СУММА(NULL) в итоге также вернет NULL.
Для решения этой проблемы мы должны использовать функцию ЕСТЬNULL. Она заменяет NULL на указанное дефолтное значение (например, 0). Разберем по шагам, как оптимизировать этот расчет:
ВЫБРАТЬ
СУММА(ВЫБОР
КОГДА ЕСТЬNULL(ЗаказыРеализации.СуммаЗаказов, 0) >= ЕСТЬNULL(ЗаказыРеализации.СуммаРеализации, 0)
ТОГДА ЕСТЬNULL(ЗаказыРеализации.СуммаЗаказов, 0)
Иначе
ЕСТЬNULL(ЗаказыРеализации.СуммаРеализации, 0)
КОНЕЦ) КАК СуммаЗаПериод
Важное примечание по производительности: Функция ЕСТЬNULL(Поле, 0) на уровне СУБД транслируется в эффективные конструкции (например, ISNULL или COALESCE). Использование условного оператора ВЫБОР КОГДА Поле ЕСТЬ NULL ТОГДА 0 ИНАЧЕ Поле КОНЕЦ работает медленнее, так как СУБД выполняет дополнительные логические ветвления для каждой строки.
Рассмотрим ситуацию, когда одному заказу покупателя соответствует несколько документов реализации. Например, у нас есть заказ на сумму 150 000 рублей. По нему было сделано две отгрузки: на 100 000 рублей и на 50 000 рублей.
Если мы выполним простое левое соединение таблицы документов ЗаказПокупателя с таблицей РеализацияТоваровУслуг по ссылке заказа, база данных вернет две строки:
Если в итоговой выборке мы попытаемся просуммировать поле ЗаказПокупателя.СуммаДокумента, то СУБД сложит 150 000 + 150 000 и выдаст ошибочный результат — 300 000 рублей! Произошло классическое задвоение данных.
Чтобы избежать этой критической ошибки, мы должны использовать метод предварительной группировки данных во временных таблицах. Сначала мы сворачиваем подчиненную таблицу (реализации) по заказам, а уже затем соединяем уникальные заказы со сгруппированными реализациями.
Рассмотрим пример правильной структуры пакета запросов:
// Шаг 1: Собираем и группируем подчиненные документы реализации по сделкам
ВЫБРАТЬ
Реализация.Сделка КАК Заказ,
СУММА(Реализация.СуммаДокумента) КАК СуммаРеализации
ПОМЕСТИТЬ ВТ_РеализацииПоЗаказам
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Проведен = ИСТИНА
СГРУППИРОВАТЬ ПО
Реализация.Сделка
ИНДЕКСИРОВАТЬ ПО
Заказ
;
// Шаг 2: Левым соединением связываем уникальные заказы со сгруппированными суммами реализаций
ВЫБРАТЬ
Заказ.Ссылка КАК Заказ,
Заказ.СуммаДокумента КАК СуммаЗаказа,
ЕСТЬNULL(ВТ_РеализацииПоЗаказам.СуммаРеализации, 0) КАК СуммаРеализации
ИЗ
Документ.ЗаказПокупателя КАК Заказ
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_РеализацииПоЗаказам КАК ВТ_РеализацииПоЗаказам
ПО Заказ.Ссылка = ВТ_РеализацииПоЗаказам.Заказ
При таком подходе на один заказ в выборке всегда будет приходиться ровно одна запись из временной таблицы свернутых реализаций, что полностью исключает дублирование или искажение сумм.
Очень часто разработчики сталкиваются с загадочным поведением платформы 1С: при выполнении запроса в обработчике проведения нового (еще не проведенного!) документа, запрос каким-то образом умудряется получить сумму и данные этого документа, хотя транзакция формально не завершена.
Выясним причину этого поведения. Проведение документа в 1С всегда происходит внутри транзакции базы данных. Последовательность событий выглядит следующим образом:
ПередЗаписью в модуле объекта.Проведен уже может иметь значение ИСТИНА, если инициировано проведение.ОбработкаПроведения, внутри которого вы запускаете свой запрос.Поскольку ваш запрос в процедуре ОбработкаПроведения выполняется на шаге №3, он работает в той же самой транзакции. Согласно принципам изоляции транзакций, СУБД видит свои собственные («грязные») записи, которые транзакция уже успела внести в базу. Именно поэтому запрос к таблице документов возвращает данные текущего проводимого объекта.
Если вам необходимо рассчитать обороты или лимиты без учета текущего документа, его нужно принудительно исключить из условий запроса по ссылке. Посмотрим на пример реализации:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СУММА(ЗаказПокупателя.СуммаДокумента) КАК СуммаЗаПериод
|ИЗ
| Документ.ЗаказПокупателя КАК ЗаказПокупателя
|ГДЕ
| ЗаказПокупателя.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
| И ЗаказПокупателя.Контрагент = &Контрагент
| И ЗаказПокупателя.Проведен = ИСТИНА
| И ЗаказПокупателя.Ссылка <> &ТекущийДокумент"; // Исключаем сам проводимый документ
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(Ссылка.Дата));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(Ссылка.Дата));
Запрос.УстановитьПараметр("Контрагент", Ссылка.Контрагент);
Запрос.УстановитьПараметр("ТекущийДокумент", Ссылка); // Передаем ссылку на проводимый документ
Результат = Запрос.Выполнить();
Обращение к физическим таблицам документов (Документ.ЗаказПокупателя, Документ.РеализацияТоваровУслуг) в транзакциях проведения — это грубая архитектурная ошибка. Рассмотрим, к каким последствиям она приводит:
Правильное архитектурное решение: Для получения сумм по заказам и продажам необходимо использовать виртуальные таблицы оборотов регистров накопления. В типовых конфигурациях 1С для этих целей предназначены регистры ЗаказыПокупателей (для остатков по заказам) и Продажи (для анализа оборотов реализации). Запросы к регистрам накопления оптимизированы платформой, используют индексы и работают на порядок быстрее и безопаснее для блокировок.
Если перед вами все же стоит задача написать сквозной аналитический отчет или обработку, которая должна связать заказы с реализациями напрямую по документам (включая заказы без реализаций и реализации без заказов), оптимальным решением будет использование конструкции ПОЛНОЕ СОЕДИНЕНИЕ. Это позволит собрать все данные в один проход без громоздких объединений.
Рассмотрим пример такого запроса:
ВЫБРАТЬ
ЕСТЬNULL(ЗаказПокупателя.Контрагент, РеализацияТоваровУслуг.Контрагент) КАК Контрагент,
ЕСТЬNULL(ЗаказПокупателя.Ссылка, "Нет заказа (Реализация без заказа)") КАК Заказ,
ЕСТЬNULL(ЗаказПокупателя.СуммаДокумента, 0) КАК СуммаЗаказа,
ЕСТЬNULL(РеализацияТоваровУслуг.Ссылка, "Нет реализации (Неотгруженный заказ)") КАК Реализация,
ЕСТЬNULL(РеализацияТоваровУслуг.СуммаДокумента, 0) КАК СуммаРеализации,
// Вычисляем максимальную сумму для аналитики
ВЫБОР
КОГДА ЕСТЬNULL(ЗаказПокупателя.СуммаДокумента, 0) >= ЕСТЬNULL(РеализацияТоваровУслуг.СуммаДокумента, 0)
ТОГДА ЕСТЬNULL(ЗаказПокупателя.СуммаДокумента, 0)
ИНАЧЕ ЕСТЬNULL(РеализацияТоваровУслуг.СуммаДокумента, 0)
КОНЕЦ КАК МаксимальнаяСумма
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ПОЛНОЕ СОЕДИНЕНИЕ Документ.ЗаказПокупателя КАК ЗаказПокупателя
ПО РеализацияТоваровУслуг.Сделка = ЗаказПокупателя.Ссылка
И РеализацияТоваровУслуг.Проведен = ИСТИНА
ГДЕ
// Фильтруем по контрагенту, учитывая возможность NULL с любой стороны соединения
ЕСТЬNULL(ЗаказПокупателя.Контрагент, РеализацияТоваровУслуг.Contragent) = &Контрагент
И (ЗаказПокупателя.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
ИЛИ РеализацияТоваровУслуг.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания)
Этот запрос изящно и надежно решает все поставленные задачи:
NULL значения через ЕСТЬNULL.Применяйте описанные правила структурирования запросов, всегда используйте временные таблицы с индексами для предварительной группировки и не забывайте об особенностях изоляции транзакций при проведении документов!