При работе с регистрами сведений в платформе «1С:Предприятие 8» разработчики часто сталкиваются с классической задачей: необходимо получить срез максимальных (или минимальных) цен для целого списка товаров, причем вместе с этой ценой обязательно нужно вытащить документ, который эту цену установил (поле Регистратор).
На первый взгляд задача кажется тривиальной. Однако стандартная группировка по максимальной цене требует включения поля Регистратор в секцию СГРУППИРОВАТЬ ПО, что мгновенно разрушает логику свертки. В итоге мы получаем не одну строку с максимальной ценой, а столько строк, сколько раз менялась цена у этой номенклатуры разными документами. В этой статье мы подробно разберем, почему простые решения здесь не работают, проанализируем неочевидные логические ошибки популярных методов и построим эталонный, максимально производительный запрос на временных таблицах.
Давайте проанализируем типичные ошибки, которые совершают начинающие разработчики при решении этой задачи.
Попытка 1: Обычная группировка с добавлением регистратора
Если мы попытаемся написать запрос напрямую, как в примере ниже, то СУБД выдаст ошибку синтаксиса или вернет избыточные строки:
ВЫБРАТЬ
Цены.Номенклатура КАК Номенклатура,
МАКСИМУМ(Цены.Цена) КАК ЦенаМакс,
Цены.Регистратор КАК Регистратор
ИЗ
РегистрСведений.ЦеныНоменклатурыКонтрагентов КАК Цены
ГДЕ
Цены.Номенклатура В (&Номенклатура)
СГРУППИРОВАТЬ ПО
Цены.Номенклатура,
Цены.Регистратор
СУБД обязана группировать записи по всем полям, к которым не применяются агрегатные функции (такие как МАКСИМУМ, СУММА и т. д.). Добавив Регистратор в группировку, мы заставляем систему выводить уникальные комбинации номенклатуры и каждого отдельного документа. В итоге цель — получить только одну запись с максимальной ценой — не достигается.
Попытка 2: Использование конструкции ПЕРВЫЕ 1
Иногда разработчики пытаются решить проблему с помощью ограничения выборки и сортировки:
ВЫБРАТЬ ПЕРВЫЕ 1
Цены.Номенклатура КАК Номенклатура,
Цены.Цена КАК Цена,
Цены.Регистратор КАК Регистратор
ИЗ
РегистрСведений.ЦеныНоменклатурыКонтрагентов КАК Цены
ГДЕ
Цены.Номенклатура = &Номенклатура
УПОРЯДОЧИТЬ ПО
Цена УБЫВ
Этот метод отлично работает, если нам нужно получить данные по одной конкретной позиции номенклатуры, переданной в одиночный параметр. Но как только перед нами встает задача получить аналогичный отчет для списка из сотен или тысяч товаров (например, при заполнении табличной части или выводе прайс-листа), этот подход становится бесполезным. Конструкция ПЕРВЫЕ 1 вернет ровно одну строку из всей результирующей выборки, а не по одной строке для каждого товара.
В ходе обсуждения на форумах часто предлагают рабочий, но крайне опасный с точки зрения производительности вариант. Давайте разберем его подробно, чтобы понять, почему его нельзя использовать в реальных высоконагруженных конфигурациях.
ВЫБРАТЬ
врЦены.Номенклатура,
врЦены.Цена,
МАКСИМУМ(врРегистраторы.Регистратор) КАК Регистратор
ИЗ
(ВЫБРАТЬ
ЦеныНоменклатуры.Номенклатура КАК Номенклатура,
МАКСИМУМ(ЦеныНоменклатуры.Цена) КАК Цена
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
ГДЕ
ЦеныНоменклатуры.Номенклатура В (&Список)
СГРУППИРОВАТЬ ПО
ЦеныНоменклатуры.Номенклатура) КАК врЦены
ЛЕВОЕ СОЕДИНЕНИЕ
(ВЫБРАТЬ
ЦеныНоменклатуры.Регистратор КАК Регистратор,
ЦеныНоменклатуры.Номенклатура КАК Номенклатура,
ЦеныНоменклатуры.Цена КАК Цена
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
ГДЕ
ЦеныНоменклатуры.Номенклатура В (&Список)) КАК врРегистраторы
ПО врЦены.Номенклатура = врРегистраторы.Номенклатура
И врЦены.Цена = врРегистраторы.Цена
СГРУППИРОВАТЬ ПО
врЦены.Номенклатура,
врЦены.Цена
Этот код действительно решает задачу «в лоб» и возвращает нужные данные. Тем не менее, здесь кроются две серьезные проблемы — архитектурная и логическая.
1. Проблема производительности СУБД (Вложенные запросы в соединениях)
Использование конструкции ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ...) КАК врРегистраторы является одной из ключевых причин деградации производительности в 1С. Когда СУБД (будь то MS SQL Server, PostgreSQL или Oracle) строит план выполнения для такого запроса, она не может заранее знать распределение данных во вложенной виртуальной таблице. Она не может построить по ней индексы «на лету» в момент соединения. На больших базах данных это приводит к операциям полного сканирования таблиц (Table Scan), высокой нагрузке на процессор сервера и длительным блокировкам таблиц.
2. Логическая ошибка: МАКСИМУМ(Регистратор)
В данном решении для исключения дублей (если одинаковая максимальная цена была установлена несколькими документами в разное время) применяется функция МАКСИМУМ(Регистратор).
Важно понимать, что тип данных УникальныйИдентификатор (UUID), на основе которого строятся ссылки в 1С, не имеет никакого отношения к хронологии. Сравнение ссылок на уровне СУБД происходит по их двоичному представлению. Таким образом, функция МАКСИМУМ(Регистратор) вернет не самый последний по дате документ, а просто тот документ, у которого GUID чисто математически оказался больше при сравнении строк. Это критическая ошибка для бизнес-логики, где важна именно хронологическая последовательность!
Чтобы решить задачу быстро, надежно и с гарантией правильного хронологического результата, мы применим пакетный запрос с использованием Временных таблиц и индексацией промежуточных результатов. Этот подход позволяет СУБД оптимально использовать индексы базы данных.
Выполним задачу в три шага.
Сначала мы выберем список уникальных товаров и определим для каждого из них максимальную цену. Результат поместим во временную таблицу и обязательно проиндексируем поле связи.
ВЫБРАТЬ
Цены.Номенклатура КАК Номенклатура,
МАКСИМУМ(Цены.Цена) КАК МаксЦена
ПОМЕСТИТЬ ВТ_МаксЦены
ИЗ
РегистрСведений.ЦеныНоменклатурыКонтрагентов КАК Цены
ГДЕ
Цены.Номенклатура В (&СписокНоменклатуры)
СГРУППИРОВАТЬ ПО
Цены.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
Вполне возможна ситуация, когда одна и та же максимальная цена устанавливалась разным временем или датами. Чтобы однозначно определить «последний» документ, нам нужно найти максимальное значение поля Период для связки товара и его максимальной цены.
ВЫБРАТЬ
ВТ_МаксЦены.Номенклатура КАК Номенклатура,
ВТ_МаксЦены.МаксЦена КАК МаксЦена,
МАКСИМУМ(Цены.Период) КАК МаксПериод
ПОМЕСТИТЬ ВТ_МаксПериоды
ИЗ
ВТ_МаксЦены КАК ВТ_МаксЦены
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатурыКонтрагентов КАК Цены
ПО ВТ_МаксЦены.Номенклатура = Цены.Номенклатура
И ВТ_МаксЦены.МаксЦена = Цены.Цена
СГРУППИРОВАТЬ ПО
ВТ_МаксЦены.Номенклатура,
ВТ_МаксЦены.МаксЦена
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
МаксПериод
;
Обратите внимание: мы проиндексировали временную таблицу по полям соединения, чтобы СУБД мгновенно выполнила операцию на следующем шаге.
Теперь, когда у нас есть точная номенклатура, ее максимальная цена и точный момент времени (период), когда эта цена была актуальна, мы можем простым внутренним соединением получить искомый документ-регистратор.
ВЫБРАТЬ
ВТ_МаксПериоды.Номенклатура КАК Номенклатура,
ВТ_МаксПериоды.МаксЦена КАК Цена,
Цены.Регистратор КАК Регистратор
ИЗ
ВТ_МаксПериоды КАК ВТ_МаксПериоды
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатурыКонтрагентов КАК Цены
ПО ВТ_МаксПериоды.Номенклатура = Цены.Номенклатура
И ВТ_МаксПериоды.МаксЦена = Цены.Цена
И ВТ_МаксПериоды.МаксПериод = Цены.Период
;
// Не забываем освобождать ресурсы tempdb
УНИЧТОЖИТЬ ВТ_МаксЦены;
УНИЧТОЖИТЬ ВТ_МаксПериоды
В реальных крупных базах данных регистры цен могут содержать миллионы строк. Обратите внимание, что в наших запросах мы соединяем таблицы по полю Цена.
В платформе 1С поле Цена является ресурсом регистра. По умолчанию СУБД не строит индексы по ресурсам (индексы создаются только по измерениям и периоду). Чтобы этот запрос на огромных базах данных выполнялся за доли секунды, рекомендуется зайти в конфигуратор и у ресурса Цена регистра сведений установить свойство «Индексировать» в значение Индексировать.
Если вы решаете эту задачу в рамках разработки отчета, то писать сложный трехэтапный пакетный запрос в коде нет необходимости. Платформа 1С предоставляет мощный инструмент в виде СКД.
В Системе компоновки данных вы можете вытащить детальные записи (включая регистратор) обычным простым запросом, а затем настроить правильный расчет ресурсов во вкладке «Ресурсы». Для этого для поля Регистратор можно написать выражение расчета с использованием функций СКД:
ВычислитьВыражение("Регистратор", , , "Первая", "Цена Убыв")
Данная функция вычислит значение регистратора для первой записи в упорядоченном по убыванию цены массиве данных прямо в рамках группировки по номенклатуре. СКД сама оптимизирует итоговый SQL-запрос к базе данных, избавив вас от необходимости ручного проектирования пакетов запросов.