Функции FIRST_VALUE и LAST_VALUE

Для каждой компании выводить один рейс, выбираемый случайным образом
(база данных аэропорт).

Использование коррелирующего подзапроса

В подзапросе для каждой компании данные сортируются случайным образом при использовании функции newid(), после чего выбирается одна (первая строка) этого отсортированного набора:

SELECT id_comp,
  (SELECT TOP 1 trip_no FROM trip t WHERE c.id_comp = t.id_comp ORDER BY NEWID()) trip_no
FROM company c
ORDER BY id_comp;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
id_comptrip_no
11188
21146
31124
41101
57771

Разумеется, вы скорее всего получите другой результат, но, поскольку данных в таблице немного, рано или поздно вы сможете получить и такой. :-)

Использование функции FIRST_VALUE

Эта оконная функция возвращает первое из упорядоченного набора значений. Теперь мы можем сделать все без подзапросов, выделив в окне набор рейсов для компании из текущей строки запроса с помощью предложения PARTITION BY и упорядочив его, как и в предыдущем примере, случайным образом в предложении ORDER BY:

SELECT DISTINCT id_comp,
FIRST_VALUE(trip_no) OVER(PARTITION BY id_comp ORDER BY NEWID()) trip_no
FROM trip
ORDER BY id_comp;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
id_comptrip_no
11195
21145
31124
41100
58882

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

Засада с LAST_VALUE

Казалось бы, какая разница брать первое или последнее значение из случайным образом упорядоченного набора? Но давайте посмотрим, что мы получим, если в предыдущем запросе заменить FIRST_VALUE на LAST_VALUE:

SELECT DISTINCT id_comp,
LAST_VALUE(trip_no) OVER(PARTITION BY id_comp ORDER BY NEWID()) trip_no
FROM trip
ORDER BY id_comp;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Я приведу результаты только для id_comp = 1. Вы можете сами выполнить запрос, чтобы убедиться, что будут выводиться абсолютно все рейсы из таблицы Trip.

id_comptrip_no
11181
11182
11187
11188
11195
11196

Что мы делаем в подобных случаях? Конечно, обращаемся к документации, а там мы читаем… Нет, постойте, сначала полный синтаксис:

LAST_VALUE | FIRST_VALUE ( [ скалярное_выражение ] )  [ IGNORE NULLS | RESPECT NULLS ]
    OVER ( [ предложение_partition_by ] предложение_order_by [ предложение_rows_range ] )

Здесь
IGNORE NULLS или RESPECT NULLS определяют, будут ли учитываться NULL-значения;
предложение_rows_range задает параметры окна.

А теперь читаем:

Важно

Диапазоном по умолчанию является RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

Т.е. окном является диапазон от текущей строки и неограниченно выше. Поскольку мы выбираем последнюю строку диапазона, то всегда будет выводиться текущая строка, как бы не сортировались строки. Т.е. сколько бы строк выше не оказалось при случайной сортировке.

Потому и DISTINCT не помогает, т.к. все выводимые строки оказываются уникальными.

Значит нам просто нужно явно (и правильно!) задать параметры окна, а именно, от текущей строки и неограниченно ниже, поскольку мы выбираем последнее значение:

SELECT DISTINCT id_comp,
LAST_VALUE(trip_no) OVER(PARTITION BY id_comp ORDER BY NEWID()
RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) trip_no
FROM trip
ORDER BY id_comp;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
id_comptrip_no
11188
21145
31123
41101
57773

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

Ответ лежит на поверхности - потому что здесь значение по умолчанию нам подошло, хотя я и не нашел в документации, каким оно должно быть для FIRST_VALUE .

Могу предположить, что тем же, что и для LAST_VALUE.