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