Почему не подтягивается курс валюты в запросе 1С и как это исправить

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

При разработке отчетов и обработок в среде 1С программисты часто сталкиваются с задачей пересчета сумм из одной валюты в другую. Типичный сценарий: есть таблица с транзакциями (например, затраты на рекламу или продажи) и периодический регистр сведений КурсыВалют. Мы пишем простой запрос, связываем таблицы по дате, но на выходе получаем пустые значения (NULL) в колонке курса для большинства строк.

В этой статье мы подробно разберем, почему возникает эта проблема, изучим классический паттерн проектирования запросов «Срез последних на каждую дату», рассмотрим альтернативные варианты решения через Систему компоновки данных (СКД) и механизмы Библиотеки стандартных подсистем (БСП), а также затронем нюансы производительности СУБД.

В чем кроется суть проблемы?

Регистр сведений КурсыВалют является периодическим. Это означает, что новая запись в физическую таблицу базы данных добавляется только тогда, когда курс валюты реально изменился. Если курс доллара (USD) был установлен 1 января, а следующее изменение произошло только 15 января, то в интервале с 2 по 14 января в базе данных просто физически нет записей.

Когда мы пытаемся связать таблицу документов или оборотов с регистром курсов напрямую по равенству периодов:

ПО Взаиморасчеты.ПериодМесяц = КурсыВалют.Период

СУБД вернет значение курса только для тех строк, даты которых точно совпадают с моментами изменения курса в регистре (например, ровно на 1 января). Для всех остальных дат, где точного совпадения нет, левое соединение вернет NULL.

Важное примечание: Помимо периодичности регистра, всегда стоит проверять корректность наименования валюты. Как справедливо отмечено на форуме, банальная опечатка (использование латинских букв вместо кириллицы, разный регистр символов «USD»/«usd» или пробелы) может полностью заблокировать соединение таблиц. Поэтому связывать таблицы всегда рекомендуется по ссылке (поле Валюта), а не по строковому представлению наименования.

---

Классическое решение: Срез последних на каждую дату в запросе

Чтобы получить актуальный курс на любую произвольную дату, нам необходимо реализовать алгоритм, который для каждой даты из нашей основной таблицы найдет ближайшую предыдущую (или равную ей) дату в регистре курсов, на которую была сделана запись. Этот алгоритм называется «срез последних на каждую дату» и реализуется с помощью временных таблиц в три шага.

Рассмотрим этот алгоритм по шагам на примере оптимизированного запроса.

Шаг 1: Получение списка уникальных дат

Сначала нам нужно собрать все уникальные даты (или периоды), для которых мы хотим получить курс. Вместо генерации искусственного календаря (как предлагалось в сообщении №2 на форуме), гораздо эффективнее и быстрее выбрать реальные даты непосредственно из нашей таблицы движений.


ВЫБРАТЬ РАЗЛИЧНЫЕ
    НАЧАЛОПЕРИОДА(Взаиморасчеты.Период, МЕСЯЦ) КАК МесяцПериода
ПОМЕСТИТЬ ВТ_Периоды
ИЗ
    РегистрНакопления.ВзаиморасчетыСКонтрагентами КАК Взаиморасчеты
ГДЕ
    Взаиморасчеты.Период МЕЖДУ &НачалоПериода И &КонецПериода
ИНДЕКСИРОВАТЬ ПО
    МесяцПериода
;

Шаг 2: Поиск максимальной даты курса для каждого периода

Теперь мы сопоставляем каждую нашу дату с таблицей курсов валют. Мы ищем все записи курсов, у которых дата меньше или равна нашей целевой дате, и с помощью агрегатной функции МАКСИМУМ находим самую последнюю из них.


ВЫБРАТЬ
    ВТ_Периоды.МесяцПериода КАК МесяцПериода,
    МАКСИМУМ(КурсыВалют.Период) КАК ПериодКурса
ПОМЕСТИТЬ ВТ_ДатыКурсов
ИЗ
    ВТ_Периоды КАК ВТ_Периоды
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
        ПО КурсыВалют.Валюта = &ВалютаUSD
        И КурсыВалют.Период <= ВТ_Периоды.МесяцПериода
СГРУППИРОВАТЬ ПО
    ВТ_Периоды.МесяцПериода
;

Обратите внимание на условие соединения: Мы соединяем таблицы по условию КурсыВалют.Период <= ВТ_Периоды.МесяцПериода. Именно это позволяет нам заглянуть в прошлое и найти последнюю установленную точку курса. Ошибка в использовании ключевого слова ГДЕ вместо ПО при левом соединении (как было в исходном сообщении на форуме) приводит к синтаксической ошибке СУБД или превращению соединения во внутреннее, что отсекает данные.

Шаг 3: Сборка итогового запроса

На последнем этапе мы соединяем наши основные данные с таблицей дат курсов, а затем вытаскиваем само значение курса из регистра по найденной максимальной дате.


ВЫБРАТЬ
    Взаиморасчеты.Контрагент КАК Контрагент,
    Взаиморасчеты.СуммаВзаиморасчетовПриход КАК ЗатратыНаРекламу,
    НАЧАЛОПЕРИОДА(Взаиморасчеты.Период, МЕСЯЦ) КАК ПериодМесяц,
    ВТ_ДатыКурсов.ПериодКурса КАК ДатаКурса,
    РегистрКурсов.Курс КАК Курс
ИЗ
    РегистрНакопления.ВзаиморасчетыСКонтрагентами КАК Взаиморасчеты
    
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДатыКурсов КАК ВТ_ДатыКурсов
        ПО НАЧАЛОПЕРИОДА(Взаиморасчеты.Период, МЕСЯЦ) = ВТ_ДатыКурсов.МесяцПериода
        
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК РегистрКурсов
        ПО РегистрКурсов.Валюта = &ВалютаUSD
        И РегистрКурсов.Период = ВТ_ДатыКурсов.PeriodКурса
ГДЕ
    Взаиморасчеты.Контрагент.Родитель.Код = "УТ0002342"
;

---

Альтернативный вариант: Связи наборов данных в СКД

Если ваша задача — построить отчет с помощью Системы компоновки данных (СКД), то писать громоздкие запросы с временными таблицами вовсе не обязательно. Платформа 1С умеет выполнять срез последних на каждую дату в автоматическом режиме с помощью механизма Связи наборов данных.

Разберем, как это настроить:

  1. Создайте Набор данных - запрос №1 (например, НаборДанныхДокументы), в котором выберите ваши основные данные. В запросе обязательно должны присутствовать поля даты (например, ПериодМесяц) и поле валюты (Валюта).
  2. Создайте Набор данных - запрос №2 (например, НаборДанныхКурсы). Текст этого запроса должен быть максимально простым и обращаться напрямую к виртуальной таблице среза последних:
    
    ВЫБРАТЬ
        КурсыВалютСрезПоследних.Валюта КАК Валюта,
        КурсыВалютСрезПоследних.Курс КАК Курс
    ИЗ
        РегистрСведений.КурсыВалют.СрезПоследних(&ПериодКурса, Валюта = &ВалютаФильтр) КАК КурсыВалютСрезПоследних
    
  3. Перейдите на вкладку Связи наборов данных и добавьте новую строку связи:
    • Источник связи: НаборДанныхДокументы
    • Приемник связи: НаборДанныхКурсы
    • Условие связи: Валюта = Валюта
    • Параметры: Имя параметра: ПериодКурса -> Выражение источника: ПериодМесяц; Имя параметра: ВалютаФильтр -> Выражение источника: Валюта.

При выполнении отчета СКД сама в цикле передаст дату и валюту из первой таблицы в параметры второй и вернет абсолютно точные, актуальные курсы без написания сложного SQL-кода.

---

Влияние на производительность СУБД (PostgreSQL vs MS SQL)

При написании «сырых» запросов (вне СКД) у начинающих разработчиков часто возникает соблазн сделать ЛЕВОЕ СОЕДИНЕНИЕ напрямую с виртуальной таблицей РегистрСведений.КурсыВалют.СрезПоследних(&Период).

Делать так категорически не рекомендуется по следующим причинам:

Использование предложенного метода с временными таблицами и поиском МАКСИМУМ(Период) гарантирует предсказуемое и быстрое выполнение запроса на любых объемах данных, так как СУБД оперирует только проиндексированными физическими таблицами.

---

Использование функций Библиотеки стандартных подсистем (БСП)

Если получение курса требуется не в сложном отчете, а во встроенном коде 1С (например, при проведении документа, заполнении табличной части или в подписке на события), то писать запросы вручную не нужно. В составе БСП есть готовый модуль РаботаСКурсамиВалют, предоставляющий оптимизированные методы.

Для получения курса одной валюты на определенную дату используйте функцию:


КурсВалюты = РаботаСКурсамиВалют.ПолучитьКурсВалюты(СсылкаНаВалюту, ДатаДокумента);

Если же вам необходимо получить курсы для целого списка валют или на разные даты, БСП предоставляет программный интерфейс для пакетного получения курсов, что работает значительно быстрее, чем отправка запросов к базе данных в цикле.

← На главную