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