При разработке отчетов и обработок в среде 1С программисты часто сталкиваются с задачей пересчета сумм из одной валюты в другую. Типичный сценарий: есть таблица с транзакциями (например, затраты на рекламу или продажи) и периодический регистр сведений КурсыВалют. Мы пишем простой запрос, связываем таблицы по дате, но на выходе получаем пустые значения (NULL) в колонке курса для большинства строк.
В этой статье мы подробно разберем, почему возникает эта проблема, изучим классический паттерн проектирования запросов «Срез последних на каждую дату», рассмотрим альтернативные варианты решения через Систему компоновки данных (СКД) и механизмы Библиотеки стандартных подсистем (БСП), а также затронем нюансы производительности СУБД.
Регистр сведений КурсыВалют является периодическим. Это означает, что новая запись в физическую таблицу базы данных добавляется только тогда, когда курс валюты реально изменился. Если курс доллара (USD) был установлен 1 января, а следующее изменение произошло только 15 января, то в интервале с 2 по 14 января в базе данных просто физически нет записей.
Когда мы пытаемся связать таблицу документов или оборотов с регистром курсов напрямую по равенству периодов:
ПО Взаиморасчеты.ПериодМесяц = КурсыВалют.Период
СУБД вернет значение курса только для тех строк, даты которых точно совпадают с моментами изменения курса в регистре (например, ровно на 1 января). Для всех остальных дат, где точного совпадения нет, левое соединение вернет NULL.
Важное примечание: Помимо периодичности регистра, всегда стоит проверять корректность наименования валюты. Как справедливо отмечено на форуме, банальная опечатка (использование латинских букв вместо кириллицы, разный регистр символов «USD»/«usd» или пробелы) может полностью заблокировать соединение таблиц. Поэтому связывать таблицы всегда рекомендуется по ссылке (поле Валюта), а не по строковому представлению наименования.
---
Чтобы получить актуальный курс на любую произвольную дату, нам необходимо реализовать алгоритм, который для каждой даты из нашей основной таблицы найдет ближайшую предыдущую (или равную ей) дату в регистре курсов, на которую была сделана запись. Этот алгоритм называется «срез последних на каждую дату» и реализуется с помощью временных таблиц в три шага.
Рассмотрим этот алгоритм по шагам на примере оптимизированного запроса.
Сначала нам нужно собрать все уникальные даты (или периоды), для которых мы хотим получить курс. Вместо генерации искусственного календаря (как предлагалось в сообщении №2 на форуме), гораздо эффективнее и быстрее выбрать реальные даты непосредственно из нашей таблицы движений.
ВЫБРАТЬ РАЗЛИЧНЫЕ
НАЧАЛОПЕРИОДА(Взаиморасчеты.Период, МЕСЯЦ) КАК МесяцПериода
ПОМЕСТИТЬ ВТ_Периоды
ИЗ
РегистрНакопления.ВзаиморасчетыСКонтрагентами КАК Взаиморасчеты
ГДЕ
Взаиморасчеты.Период МЕЖДУ &НачалоПериода И &КонецПериода
ИНДЕКСИРОВАТЬ ПО
МесяцПериода
;
Теперь мы сопоставляем каждую нашу дату с таблицей курсов валют. Мы ищем все записи курсов, у которых дата меньше или равна нашей целевой дате, и с помощью агрегатной функции МАКСИМУМ находим самую последнюю из них.
ВЫБРАТЬ
ВТ_Периоды.МесяцПериода КАК МесяцПериода,
МАКСИМУМ(КурсыВалют.Период) КАК ПериодКурса
ПОМЕСТИТЬ ВТ_ДатыКурсов
ИЗ
ВТ_Периоды КАК ВТ_Периоды
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
ПО КурсыВалют.Валюта = &ВалютаUSD
И КурсыВалют.Период <= ВТ_Периоды.МесяцПериода
СГРУППИРОВАТЬ ПО
ВТ_Периоды.МесяцПериода
;
Обратите внимание на условие соединения: Мы соединяем таблицы по условию КурсыВалют.Период <= ВТ_Периоды.МесяцПериода. Именно это позволяет нам заглянуть в прошлое и найти последнюю установленную точку курса. Ошибка в использовании ключевого слова ГДЕ вместо ПО при левом соединении (как было в исходном сообщении на форуме) приводит к синтаксической ошибке СУБД или превращению соединения во внутреннее, что отсекает данные.
На последнем этапе мы соединяем наши основные данные с таблицей дат курсов, а затем вытаскиваем само значение курса из регистра по найденной максимальной дате.
ВЫБРАТЬ
Взаиморасчеты.Контрагент КАК Контрагент,
Взаиморасчеты.СуммаВзаиморасчетовПриход КАК ЗатратыНаРекламу,
НАЧАЛОПЕРИОДА(Взаиморасчеты.Период, МЕСЯЦ) КАК ПериодМесяц,
ВТ_ДатыКурсов.ПериодКурса КАК ДатаКурса,
РегистрКурсов.Курс КАК Курс
ИЗ
РегистрНакопления.ВзаиморасчетыСКонтрагентами КАК Взаиморасчеты
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДатыКурсов КАК ВТ_ДатыКурсов
ПО НАЧАЛОПЕРИОДА(Взаиморасчеты.Период, МЕСЯЦ) = ВТ_ДатыКурсов.МесяцПериода
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК РегистрКурсов
ПО РегистрКурсов.Валюта = &ВалютаUSD
И РегистрКурсов.Период = ВТ_ДатыКурсов.PeriodКурса
ГДЕ
Взаиморасчеты.Контрагент.Родитель.Код = "УТ0002342"
;
---
Если ваша задача — построить отчет с помощью Системы компоновки данных (СКД), то писать громоздкие запросы с временными таблицами вовсе не обязательно. Платформа 1С умеет выполнять срез последних на каждую дату в автоматическом режиме с помощью механизма Связи наборов данных.
Разберем, как это настроить:
НаборДанныхДокументы), в котором выберите ваши основные данные. В запросе обязательно должны присутствовать поля даты (например, ПериодМесяц) и поле валюты (Валюта).НаборДанныхКурсы). Текст этого запроса должен быть максимально простым и обращаться напрямую к виртуальной таблице среза последних:
ВЫБРАТЬ
КурсыВалютСрезПоследних.Валюта КАК Валюта,
КурсыВалютСрезПоследних.Курс КАК Курс
ИЗ
РегистрСведений.КурсыВалют.СрезПоследних(&ПериодКурса, Валюта = &ВалютаФильтр) КАК КурсыВалютСрезПоследних
НаборДанныхДокументыНаборДанныхКурсыВалюта = ВалютаПериодКурса -> Выражение источника: ПериодМесяц; Имя параметра: ВалютаФильтр -> Выражение источника: Валюта.При выполнении отчета СКД сама в цикле передаст дату и валюту из первой таблицы в параметры второй и вернет абсолютно точные, актуальные курсы без написания сложного SQL-кода.
---
При написании «сырых» запросов (вне СКД) у начинающих разработчиков часто возникает соблазн сделать ЛЕВОЕ СОЕДИНЕНИЕ напрямую с виртуальной таблицей РегистрСведений.КурсыВалют.СрезПоследних(&Период).
Делать так категорически не рекомендуется по следующим причинам:
СрезПоследних применяется ко всему запросу в целом. Если вы передадите туда одну дату, то получите срез только на эту дату, что сделает невозможным получение курсов для документов с разными датами в рамках одного запроса.Использование предложенного метода с временными таблицами и поиском МАКСИМУМ(Период) гарантирует предсказуемое и быстрое выполнение запроса на любых объемах данных, так как СУБД оперирует только проиндексированными физическими таблицами.
---
Если получение курса требуется не в сложном отчете, а во встроенном коде 1С (например, при проведении документа, заполнении табличной части или в подписке на события), то писать запросы вручную не нужно. В составе БСП есть готовый модуль РаботаСКурсамиВалют, предоставляющий оптимизированные методы.
Для получения курса одной валюты на определенную дату используйте функцию:
КурсВалюты = РаботаСКурсамиВалют.ПолучитьКурсВалюты(СсылкаНаВалюту, ДатаДокумента);
Если же вам необходимо получить курсы для целого списка валют или на разные даты, БСП предоставляет программный интерфейс для пакетного получения курсов, что работает значительно быстрее, чем отправка запросов к базе данных в цикле.