Заметки по SQL: Создание ранжирующей функции ROW_NUMBER и функций смещения LAG и LEAD простым запросом

04.01.25

Разработка - Универсальные функции

В статье рассматривается создание ранжирующей функции ROW_NUMBER с использованием простого SQL запроса, создание на его основе функций смещения LAG и LEAD и варианты применения этих функций.

Введение.

Стандарт SQL поддерживает четыре оконные функции, которые служат для ранжирования. Это ROW_NUMBER, NTILE, RANK и DENSE_RANK. Функции ранжирования появились в MS SQL начиная с Server 2005.

ROW_NUMBER – функция вычисляет последовательные номера строк, начиная  с 1, в соответствии с заданным упорядочением окна.

Под окном (секцией) понимается заданная группировка строк.

Синтаксис

ROW_NUMBER () OVER ([ PARTITION BY value_expression , ... [ n ] ] ORDER BY [value_expression] )

value_expression определяет столбец, по которому секционируется результирующий набор. Если параметр PARTITION BY не указан, функция обрабатывает все строки результирующего набора запроса как одну группу. 

ORDER BY определяет последовательность, в которой строкам назначаются уникальные номера с помощью функции ROW_NUMBER в пределах указанной секции (окна). Оно должно указываться обязательно.

До Server 2005 в качестве упрощённого аналога функции ROW_NUMBER () использовался запрос

SELECT orderid, val,
  (SELECT COUNT(*) FROM Sales.OrderValues) AS O2
   WHERE O2.orderid <= O1.orderid) AS rownum
FROM Sales.OrderValues AS O1;

В этом решении используется агрегат COUNT во вложенном запросе для определения, у скольких строк значение упорядочения (в нашем случае orderid) меньше или равно текущему. 

Недостатком запроса является низкая скорость выполнения, поскольку в каждой строке запроса выполняется дополнительный запрос.

1с не поддерживает подзапросы в разделе SELECT.

В СКД 1с реализован аналог функции ROW_NUMBER ().

ВЫЧИСЛИТЬВЫРАЖЕНИЕ(НомерПоПорядкуВГруппировке(),  [Поле группировки], , , , [Поле сортировки])

 

Создание ранжирующего запроса.

Поскольку 1с не поддерживает подзапросы в разделе SELECT, перенесем подзапрос в строке запроса

(SELECT COUNT(* FROM Sales.OrderValues) AS O2

в раздел объединения таблиц. Для этого создадим тестовый запрос с набором данных:

ВЫБРАТЬ
	148 КАК id,
	410 КАК val,
	"Dgon" КАК name
ПОМЕСТИТЬ ВременнаяТаблица

ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
	130,
	370,
	"Smit"

ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
	150,
	170,
	"Dgon"

ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
	151,
	405,
	"Dgon"

ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
	153,
	770,
	"Smit"
;

//Ранжирующий запрос//
ВЫБРАТЬ
          ВременнаяТаблица.id КАК id,
          ВременнаяТаблица.val КАК val,
          КОЛИЧЕСТВО(ВременнаяТаблица.id) КАК rownum,
          СУММА(1) КАК rownum1
ИЗ
     ВременнаяТаблица КАК ВременнаяТаблица
     ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВременнаяТаблица КАК ВременнаяТаблица1
         ПО ВременнаяТаблица.id >= ВременнаяТаблица1.id
СГРУППИРОВАТЬ ПО
          ВременнаяТаблица.id,
          ВременнаяТаблица.val

Результат:

Таким образом последний запрос нашего теста, фактически является ранжирующей функцией ROW_NUMBER.

Отметим, что колонки с выражениями

КОЛИЧЕСТВО(ВременнаяТаблица.id) КАК rownum,

СУММА(1) КАК rownum1,

являются эквивалентными.

Проведем ранжирвание по полю name, в порядке следвания id для этого измеим последний запрос 

ВЫБРАТЬ
	ВременнаяТаблица.id КАК id,
	ВременнаяТаблица.val КАК val,
	ВременнаяТаблица.name КАК name,
	КОЛИЧЕСТВО(ВременнаяТаблица.val) КАК rownum,
	СУММА(1) КАК rownum1
ИЗ
	ВременнаяТаблица КАК ВременнаяТаблица
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВременнаяТаблица КАК ВременнаяТаблица1
		ПО ВременнаяТаблица.id >= ВременнаяТаблица1.id
			И ВременнаяТаблица.name = ВременнаяТаблица1.name
СГРУППИРОВАТЬ ПО
	ВременнаяТаблица.id,
	ВременнаяТаблица.name,
	ВременнаяТаблица.val

УПОРЯДОЧИТЬ ПО
	name,
	rownum1

Результат 

Ранжирвание по полю name в порядке слдования val

ВЫБРАТЬ
	ВременнаяТаблица.id КАК id,
	ВременнаяТаблица.val КАК val,
	ВременнаяТаблица.name КАК name,
	КОЛИЧЕСТВО(ВременнаяТаблица.val) КАК rownum,
	СУММА(1) КАК rownum1
ИЗ
	ВременнаяТаблица КАК ВременнаяТаблица
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВременнаяТаблица КАК ВременнаяТаблица1
		ПО ВременнаяТаблица.val >= ВременнаяТаблица1.val
			И ВременнаяТаблица.name = ВременнаяТаблица1.name
СГРУППИРОВАТЬ ПО
	ВременнаяТаблица.id,
	ВременнаяТаблица.name,
	ВременнаяТаблица.val

УПОРЯДОЧИТЬ ПО
	name,
	rownum1

Результат

Итоги

В первом запросе окно не задано и нумеруется весь набор записей, что стветствует функции с параметрами

ROW_NUMBER () OVER (ORDER BY id)

Параметр функции ORDER BY id сответствует соединеию 

ВременнаяТаблица.id >= ВременнаяТаблица1.id

Во вотром и третьем запросе окно задано это поле name, отличаются запросы только параметром порядка:

ROW_NUMBER (ВременнаяТаблица.name = ВременнаяТаблица1.name) OVER ( PARTITION BY  name ORDER BY id)

ВременнаяТаблица.id >= ВременнаяТаблица1.id и

ROW_NUMBER (ВременнаяТаблица.name = ВременнаяТаблица1.name) OVER ( PARTITION BY  name ORDER BY val)

ВременнаяТаблица.val>= ВременнаяТаблица1.val

Соответственно, за параметр окна PARTITION BY в запросах отвечает сединение 

ВременнаяТаблица.name = ВременнаяТаблица1.name

 

Создание функций смещения LAG и LEAD.

Начиная с Server 2012 поддерживаются функции смещения LAG, LEAD, FIRST_VALUE и LAST_VALUE.

Функции LAG и LEAD позволяют возвращать выражение значения из строки в секции окна, которая находится на заданном смещении перед (LAG) или после (LEAD) текущей строки. Смещение по умолчанию — «1», оно применяется, если смещение не указать.

Например строка в запросе -

LAG(val)  OVER(PARTITION BY id ORDER BY id) AS prevvalр,

возвращает значение val из предыдущей строки.

LAG(val, [prev]) – возвращает значение val перед строкой  со  смещением = prev.

В СКД 1с есть аналог функций LAG и LEAD:

ВычислитьВыражение("val", , ,"Предыдущая","Предыдущая" )

ВычислитьВыражение(“val”, , ,“Следующая”,“Следующая”)

Для создания функций смещения, изменим наш тест следующим образом. Поместим последний запрос во временную таблицу – «ВременнаяТаблица1» и создадим еще один запрос:

ВЫБРАТЬ
         ВременнаяТаблица1.id КАК id,
         ВременнаяТаблица1.rownum КАК rownum,
         ВременнаяТаблица1.val КАК val,
         ВременнаяТаблица11.val КАК prevvalр
ИЗ
     ВременнаяТаблица1 КАК ВременнаяТаблица1
ЛЕВОЕ СОЕДИНЕНИЕ ВременнаяТаблица1 КАК ВременнаяТаблица11
     ПО ВременнаяТаблица1.rownum = ВременнаяТаблица11.rownum + 1
УПОРЯДОЧИТЬ ПО
     Rownum

Результат запроса:

 

Таким образом создан аналог функции LAG, которая возвращает значения из предыдущей строки.

Изменяя в выражении соединения переменную [смещение], можно менять строки из которых берутся значения.

ПО ВременнаяТаблица1.rownum = ВременнаяТаблица11.rownum  + [смещение],

Если изменить условие соединения на

ПО ВременнаяТаблица1.rownum = ВременнаяТаблица11.rownum - 1,

то получим аналог функции LEAD..

 

                                               Примеры применения.

  1. Отчет (ссылка) «Продажи в динамике изменения цен номенклатуры за период».

Отчет показывает:

  • Среднее количество проданного товара в день за интервалы, действия цен номенклатуры по заданному виду цен, за выбранный период дат;
  • Изменение цены номенклатуры (в %), по интервалам действия цен;
  • Отклонение расчетной цены продажи в документах реализации (действие скидок и наценок) от цены установленной в РегистрСведений.ЦеныНоменклатуры;
  • Ошибки установки цены в документах реализации.

Отчет позволяет анализировать средние продажи за период действия цен в зависимости от их изменения, в заданном интервале дат. Тестировался для конфигурации "Управление торговлей 11" (релизы 11.2-11.5), Комплексная автоматизация 2.5.16.101.

 
 Фрагмент кода запроса:

 

  1. Отчет (ссылка) «Реестр показаний по приборам учета за период с расчетом среднего потребления ресурса».

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

Конфигурация "Расчеты с населением за газ" (Релиз 1.1.2.2).

 
 Фрагмент кода запроса:
  1. Анализ дебиторской задолженности по дням – вычисляем тренд и абсолютное увеличение или уменьшение задолженности относительно прошлого периода
  2. Анализ курсов валют и оценка прибыли или убытков от курсовой разницы – нужно получить дельту между текущим и предыдущим курсом и умножить на остаток денежных средств в валюте.
  3. Получение скорости и ускорения передвижения объекта.

Имеем данные изменения положения объекта во времени.

Вычисляем скорость объекта как,

Скорость =

КоординатаВточке i - КоординатаВточке i-1 / времяВточке i - времяВточке i-1

Вычисляем ускорение.

Ускорение =

Скорость в точке i - Скорость в точке i-1 / время в точке i - время в точке i-1

Фактически реализуются формулы:

v = dS/dt  

a = dV/dt

 

Вам может быть интересно. 

Загрузка данных контрагентов из России, Беларуси и Казахстана из веб-сервисов.

Мониторинг потребления ресурсов базами кластеров сервера

Заметки по SQL: Срез последних - аналог запроса

Поиск ссылок в метаданных конфигурации

Загрузка и обновление данных номенклатуры, дополнительных реквизитов и сведений, характеристик и наборов упаковок номенклатуры из файлов формата MS Excel, OpenOfficeCalc (ODS), CSV, TXT, DBF, MXL для 1С: УТ, КА, ЕРП, Розница, БП

Запросы SQL

См. также

Инструментарий разработчика Роли и права Запросы СКД Программист Руководитель проекта Платформа 1С v8.3 Управляемые формы Запросы Система компоновки данных Платные (руб)

Инструменты для разработчиков 1С 8.3: Infostart Toolkit. Автоматизация и ускорение разработки на управляемых формах. Легкость работы с 1С.

12000 руб.

02.09.2020    169246    937    403    

905

Запросы Программист Платформа 1С v8.3 Запросы Конфигурации 1cv8 Бесплатно (free)

Столкнулся с интересной ситуацией, которую хотел бы разобрать, ввиду её неочевидности. Речь пойдёт про использование функции запроса АВТОНОМЕРЗАПИСИ() и проблемы, которые могут возникнуть.

11.10.2024    6336    XilDen    36    

83

Универсальные функции Программист Платформа 1С v8.3 Конфигурации 1cv8 Бесплатно (free)

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

21.05.2024    23932    dimanich70    81    

147

HighLoad оптимизация Запросы

Очень немногие из тех, кто занимается поддержкой MS SQL, работают с хранилищем запросов. А ведь хранилище запросов – это очень удобный, мощный и, главное, бесплатный инструмент, позволяющий быстро найти и локализовать проблему производительности и потребления ресурсов запросами. В статье расскажем о том, как использовать хранилище запросов в MS SQL и какие плюсы и минусы у него есть.

11.10.2023    19939    skovpin_sa    15    

106

WEB-интеграция Универсальные функции Механизмы платформы 1С Программист Платформа 1С v8.3 Конфигурации 1cv8 Бесплатно (free)

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

28.08.2023    16136    YA_418728146    8    

170

Пакетная печать Печатные формы Адаптация типовых решений Универсальные функции Платформа 1С v8.3 1С:ERP Управление предприятием 2 1С:Управление торговлей 11 1С:Комплексная автоматизация 2.х Россия Абонемент ($m)

Расширение для программ 1С:Управление торговлей, 1С:Комплексная автоматизация, 1С:ERP, которое позволяет распечатывать печатные формы для непроведенных документов. Можно настроить, каким пользователям, какие конкретные формы документов разрешено печатать без проведения документа.

2 стартмани

22.08.2023    4047    66    progmaster    9    

4

Запросы HighLoad оптимизация Программист Запросы Бесплатно (free)

Многие знают, что для ускорения работы запроса нужно «изучить план». При этом сам план обычно обескураживает: куча разноцветных иконок и стрелочек; ничего не понятно, но очень интересно! Аналитик производительности Александр Денисов на конференции Infostart Event 2021 Moscow Premiere рассказал, как выполняется план запроса и что нужно сделать, чтобы с его помощью находить проблемы производительности.

20.06.2023    30781    Филин    37    

119
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. PerlAmutor 155 17.11.24 19:20 Сейчас в теме
1с не поддерживает подзапросы в разделе SELECT.

На самом деле поддерживает, но лишь для проверки результата подзапроса на ЛОЖЬ или ИСТИНА.
2. starik-2005 3096 18.11.24 15:07 Сейчас в теме
Прикольно. Не совсем ясно, зачем в запросах пустые строки между строками - выглядит ушибом глаза.
paulwist; triviumfan; +2 Ответить
3. paulwist 18.11.24 15:18 Сейчас в теме
Таким образом последний запрос нашего теста, фактически является ранжирующей функцией ROW_NUMBER.


Нуу, предложенное решение сильно ограничено в функционале


with cte as
(
sel ect  148  id, 440  val
uni on all
select  130, 770
-- Просто добавим одну строку с id = 131, val = 770 из "первой строки"
uni on all
select  131, 770
--
uni on all
sel ect  150, 170
union all
sel ect  151, 270
)
sel ect
                cte.id  id,
                cte.val val,
                count(cte.id) rownum,
                sum(1) rownum1,
				row_number() over (partition by cte.val order by cte.id, cte.val) [Настоящий Row_Number]
fr om
                cte cte
inner join      cte cte1
                on cte.id >= cte1.id
group by
                cte.id,
                cte.val
order by
                cte.id,
                cte.val
Показать


Итог, "сильно" кривой по сравнению с row_number от вендора :)
Прикрепленные файлы:
4. triviumfan 97 21.11.24 14:58 Сейчас в теме
(3)
Итог, "сильно" кривой по сравнению с row_number от вендора :)

У него ведь секции по id, а не val.
5. paulwist 21.11.24 15:59 Сейчас в теме
(4)
У него ведь секции по id, а не val.


У ТСа нет секций, вернее она одна на все данные, если

row_number() over (partition by cte.ID order by cte.id, cte.val)


тогда row_number для всех уникальных ID будет равным 1 (единице)

with cte as
(
sel ect  148  id, 440  val
uni on all
select  130, 770
/*-- Просто добавим одну строку с id = 131, val = 770 из "первой строки"
uni on all
select  130, 771
--*/
uni on all
sel ect  150, 170
union all
sel ect  151, 270
)
sel ect
                cte.id  id,
                cte.val val,
                count(cte.id) rownum,
                sum(1) rownum1,
				row_number() over (partition by cte.id order by cte.id, cte.val) [Настоящий Row_Number]
fr om
                cte cte
inner join      cte cte1
                on cte.id >= cte1.id
group by
                cte.id,
                cte.val
order by
                cte.id,
                cte.val
Показать
triviumfan; +1 Ответить
6. triviumfan 97 25.11.24 23:41 Сейчас в теме
(5) Только добрался до ssms)
Получается, что у него 1 секция типа:
row_number() over (order by cte.id, cte.val) [Настоящий Row_Number]
7. paulwist 26.11.24 08:32 Сейчас в теме
(6)
Получается, что у него 1 секция типа:


Ну да, одно "окно".

Предложенный ТСом метод хорош если надо пронумеровать результат, хотя как правило в БД делают табличку-счетчик c одним Identity полем и заполняют его скажем до 1 млн (кому сколько надо), но это не row_number() однозначно. :)
8. IVC_goal 227 04.12.24 10:52 Сейчас в теме
(6) Для вас Добавил раздел Выводы

(5)
triviumfan; +1 Ответить
Оставьте свое сообщение