Разработчики в «1С:Предприятии 8» регулярно сталкиваются с необходимостью соединения таблиц по временным интервалам. Типичный сценарий: есть внешняя таблица значений (например, переданная в качестве параметра) и регистр сведений. Нам нужно объединить их по ключевым полям и по периоду. Однако, если в правой таблице на указанную дату отсутствует запись или дата не заполнена, разработчик получает пустое значение. Попытки подставить дату из левой таблицы часто приводят к непредсказуемым результатам, «левым» датам или дублированию строк.
В этой статье мы подробно разберем, почему возникают такие проблемы, проанализируем разницу между отсутствующим значением и пустой датой, а также рассмотрим два эффективных решения: через правильное использование условий в запросе и через высокопроизводительный паттерн ОБЪЕДИНИТЬ ВСЕ + СГРУППИРОВАТЬ.
Давайте проанализируем ситуацию, описанную в первом сообщении. Разработчик соединяет левую таблицу (внешнюю ТЗ) с правой (регистр сведений) с помощью левого соединения по нескольким полям, включая период. Если в правой таблице нет записи на эту дату, результатом соединения полей правой таблицы логично становится значение NULL.
При попытке подменить это пустое значение датой из левой таблицы разработчики часто совершают две критические ошибки:
NULL) и наличием строки с незаполненным реквизитом типа «Дата» (01.01.0001).Выясним причины каждой ошибки и разберем, как их исправить.
Рассмотрим пример ошибочной конструкции, которая часто встречается в коде начинающих разработчиков (и была предложена в Сообщении 10):
ВЫБОР
КОГДА ПраваяТаблица.Период <> NULL
ТОГДА ПраваяТаблица.Период
ИНАЧЕ ЛеваяТаблица.Дата
КОНЕЦ
Эта конструкция никогда не будет работать корректно. В языке SQL (и, соответственно, в запросах 1С) значение NULL означает неизвестность. С точки зрения логики СУБД, любое сравнение с неизвестностью (равно, не равно, больше, меньше) всегда возвращает результат «Неизвестно» (который трактуется как Ложь). Таким образом, условие ПраваяТаблица.Период <> NULL всегда будет вычисляться как Ложь, и запрос вернет не то, что ожидает разработчик.
Правило: Проверять поле на наличие значения NULL можно только с помощью специального оператора ЕСТЬ NULL или встроенной функции ЕСТЬNULL().
// Правильный вариант с оператором ЕСТЬ NULL
ВЫБОР
КОГДА ПраваяТаблица.Период ЕСТЬ NULL
ТОГДА ЛеваяТаблица.Дата
ИНАЧЕ ПраваяТаблица.Период
КОНЕЦ
В платформе «1С:Предприятие 8» тип данных «Дата» всегда содержит значение. Если поле даты в базе данных не заполнено, система записывает туда так называемую пустую дату — 01.01.0001 00:00:00 (в запросах она представляется как ДАТАВРЕМЯ(1, 1, 1)).
При левом соединении возможны два принципиально разных сценария:
NULL. Проверка РС.Период ЕСТЬ NULL сработает.01.01.0001. Проверка на ЕСТЬ NULL вернет Ложь, так как значение физически существует. В результате на экран выведется пустая дата начала нашей эры, которую пользователь воспримет как ошибку.Для надежного решения необходимо проверять оба этих состояния в одном выражении:
ВЫБОР
КОГДА РС.Период ЕСТЬ NULL ИЛИ РС.Период = ДАТАВРЕМЯ(1, 1, 1)
ТОГДА ВТ_ТЗ.Дата
ИНАЧЕ РС.Период
КОНЕЦ КАК ИтоговыйПериод
Если в условии связи написать ПО ВТ_ТЗ.Дата = РС.Период, то СУБД будет искать абсолютно точное совпадение вплоть до секунды. Если во внешней таблице дата равна 15.10.2025 14:35:12, а в регистре запись сделана на начало дня 15.10.2025 00:00:00, то условие связи не выполнится. База данных вернет NULL для правой таблицы, и вы увидите дату из левой таблицы.
Чтобы избежать этой проблемы, приводите даты обеих таблиц к единому началу периода (например, к началу дня) непосредственно в условии связи:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ИмяРегистра КАК РС
ПО ВТ_ТЗ.Номенклатура = РС.Номенклатура
И НАЧАЛОПЕРИОДА(ВТ_ТЗ.Дата, ДЕНЬ) = НАЧАЛОПЕРИОДА(РС.Период, ДЕНЬ)
Функция ЕСТЬNULL() работает на уровне базы данных быстрее, чем конструкция ВЫБОР КОГДА. Она принимает два параметра: проверяемое поле и значение, которое нужно подставить, если поле равно NULL.
Рассмотрим подробный пример запроса, где мы соединяем временную таблицу документов (или внешних данных) с периодическим регистром сведений:
ВЫБРАТЬ
ВТ_Данные.Номенклатура КАК Номенклатура,
ВТ_Данные.Дата КАК ДатаДокумента,
ЕСТЬNULL(Цены.Период, ВТ_Данные.Дата) КАК ИтоговыйПериод,
ЕСТЬNULL(Цены.Цена, 0) КАК Цена
ПОМЕСТИТЬ ВТ_Результат
ИЗ
ВТ_Данные КАК ВТ_Данные
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО ВТ_Данные.Номенклатура = Цены.Номенклатура
И НАЧАЛОПЕРИОДА(ВТ_Данные.Дата, ДЕНЬ) = НАЧАЛОПЕРИОДА(Цены.Период, ДЕНЬ)
;
В данном случае, если в регистре цен нет записи на дату документа, поле Цены.Период вернет NULL, и функция ЕСТЬNULL автоматически подставит значение ВТ_Данные.Дата.
Как совершенно справедливо заметили участники обсуждения в сообщениях 8, 9 и 10, левые и полные соединения на больших объемах данных могут сильно снижать производительность СУБД, особенно если соединение идет по неиндексированным полям или выражениям вроде НАЧАЛОПЕРИОДА.
Когда нам нужно сопоставить данные из внешнего источника и периодического регистра, избежав при этом проблем с NULL и медленной работы соединений, лучшим решением является использование конструкции ОБЪЕДИНИТЬ ВСЕ с последующей группировкой.
Разберем этот паттерн по шагам:
МАКСИМУМ или СУММА.Посмотрим, как выглядит такой запрос на практике:
// Шаг 1: Подготавливаем временную таблицу с индексами
ВЫБРАТЬ
ТЗ.Номенклатура КАК Номенклатура,
НАЧАЛОПЕРИОДА(ТЗ.Дата, ДЕНЬ) КАК Период,
ТЗ.Количество КАК Количество
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
&ПараметрТЗ КАК ТЗ
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
Период
;
// Шаг 2 и 3: Объединяем источники данных и группируем их
ВЫБРАТЬ
ОбъединенныеДанные.Номенклатура КАК Номенклатура,
ОбъединенныеДанные.Период КАК Период,
МАКСИМУМ(ОбъединенныеДанные.Количество) КАК Количество,
МАКСИМУМ(ОбъединенныеДанные.Цена) КАК Цена
ИЗ
(
// Выборка из исходной ТЗ
ВЫБРАТЬ
Данные.Номенклатура КАК Номенклатура,
Данные.Период КАК Период,
Данные.Количество КАК Количество,
0 КАК Цена
ИЗ
ВТ_ИсходныеДанные КАК Данные
ОБЪЕДИНИТЬ ВСЕ
// Выборка из регистра цен
ВЫБРАТЬ
Цены.Номенклатура КАК Номенклатура,
НАЧАЛОПЕРИОДА(Цены.Период, ДЕНЬ) КАК Период,
0 КАК Количество,
Цены.Цена КАК Цена
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК Цены
ГДЕ
Цены.Номенклатура В (ВЫБРАТЬ РАЗЛИЧНЫЕ Исходные.Номенклатура ИЗ ВТ_ИсходныеДанные КАК Исходные)
) КАК ОбъединенныеДанные
СГРУППИРОВАТЬ ПО
ОбъединенныеДанные.Номенклатура,
ОбъединенныеДанные.Период
Этот подход гарантирует: