Прозоречни функции
Накратко: Прозоречните функции с OVER и PARTITION BY смятат върху група редове, без да ги свиват в един: място в класация, текуща сума или предишният ред с LAG.
Проблем
Средната, но и редовете
Средната цена за всяка категория вече я можеш с GROUP BY:
Само че управителят иска таблица с всички продукти и до всеки средната на неговата категория, за да види кой колко се отклонява. GROUP BY е свил продуктите: от 25 реда са останали 7. В урока за свързаните подзаявки го направи с подзаявка за всеки ред. Има и по-кратък начин.
Идеята
OVER и PARTITION BY
AVG(price) OVER (PARTITION BY category_id) смята средната върху групата на реда, но не свива редовете: всички 25 продукта са тук, всеки със средната на своята категория.
OVER (…) казва върху кои редове да се смята. Тази група редове се казва прозорец, а функцията с OVER се казва прозоречна функция (window function). PARTITION BY разделя редовете на групи, като GROUP BY, но без да ги слива.
Едно отличие от свързаната подзаявка: при подаръчната карта средната е 20 €, а не NULL. PARTITION BY слага всички NULL в една група, както GROUP BY, а p2.category_id = p.category_id не беше вярно за NULL.
Идеята
Място в класацията
ROW_NUMBER() дава поредния номер на реда в прозореца. С ORDER BY в OVER казваш по какво се подреждат:
Във всяка категория номерата започват отначало, от най-скъпия продукт: най-скъпото в „Кухня“ (категория 8) е комплектът ножове с номер 1.
Естественото продължение е WHERE place = 1, за да останат само първите. Но:
Прозоречните функции се смятат след WHERE: първо се избират редовете, после се подреждат в прозорци. Затова класацията се прави в стъпка, а филтърът е отвън.
Пробвай
Най-скъпото във всяка категория
Покажи за всяка категория името ѝ (колона category), най-скъпия ѝ продукт и цената му (колони name и price). Полето вече номерира продуктите. Сложи го в WITH и вземи само първите.
Решение
WITH ranked AS (
SELECT name, category_id, price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS place
FROM products
)
SELECT c.name AS category, r.name, r.price
FROM ranked r
JOIN categories c ON c.id = r.category_id
WHERE r.place = 1;WITH ranked AS (…), отвън WHERE r.place = 1 и JOIN categories за името. Шест реда, по един на подкатегория. Подаръчната карта няма категория и JOIN я изпуска. Това е образецът за „първите N във всяка група“: номерирай в прозорец, филтрирай отвън.
Виж го
Равни места
Сега класация по поръчани бройки в „Хляб и тестени“ (категория 3), с трите функции една до друга:
ROW_NUMBERдава различни номера дори при равенство. Кой от двамата е първи, решава второто подреждане (тук името). Без него не е обещано.RANKдава еднакво място на равните и после прескача: 1, 1, 3.DENSE_RANKдава еднакво място и не прескача: 1, 1, 2.
За „топ 3 продукта“ избирай съзнателно: с ROW_NUMBER излизат точно трима, с RANK излизат всички, които делят място.
Идеята
Текуща сума
С ORDER BY в OVER прозорецът на всеки ред е „от началото до тук“. Колко сме продали от 1 септември до всеки ден (без отказаните поръчки):
running_total расте ред по ред: на 3 октомври става 415,30 €, а на 19 октомври е 480,30 €. Това се казва текуща сума (running total).
По-точно, за любопитните
Ако два реда имат една и съща стойност в ORDER BY, по подразбиране и двата получават сумата заедно с другия. Тук всеки ден е един ред, затова няма как да се случи. Ако подреждаш поръчки по дата, а в един ден има две поръчки, добави второ подреждане, например ORDER BY ordered_at, id, или изрично ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
Пробвай
Колко клиенти сме имали
За всеки клиент покажи името, датата на регистрация и колко клиенти е имал магазинът към тази дата, заедно с него (колона customers_so_far).
Решение
SELECT name, joined_at,
COUNT(*) OVER (ORDER BY joined_at) AS customers_so_far
FROM customers;COUNT(*) OVER (ORDER BY joined_at): от 1 при Мария до 20 при Явор. Двама клиенти с еднаква дата биха получили еднакво число, но тук датите са различни.
Идеята
LAG: предишният ред
LAG(колона) дава стойността от предишния ред в прозореца. Колко дни минават между поръчките на Мария?
previous е датата на предишната ѝ поръчка. julianday превръща датата в номер на ден, затова разликата е броят дни между двете поръчки. Първата поръчка няма предишна и получава NULL. Обратната функция е LEAD: следващият ред.
Упражнение
Две задачи
1. Витрината на сайта показва по два продукта от всяка категория, най-скъпите.
Покажи за всяка категория името ѝ (колона category) и двата ѝ най-скъпи продукта с цените им (колони name и price).
Решение
WITH ranked AS (
SELECT name, category_id, price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS place
FROM products
)
SELECT c.name AS category, r.name, r.price
FROM ranked r
JOIN categories c ON c.id = r.category_id
WHERE r.place <= 2;Същото като при най-скъпия, но WHERE r.place <= 2. 12 реда, по два от шестте подкатегории.
2. Маркетингът иска да знае колко най-дълго е чакал всеки клиент между две свои поръчки.
За всеки клиент с поне две поръчки покажи името и най-дългата пауза в дни между две поредни негови поръчки (колона longest_pause).
Решение
WITH gaps AS (
SELECT customer_id,
julianday(ordered_at) - julianday(LAG(ordered_at) OVER (PARTITION BY customer_id ORDER BY ordered_at)) AS days
FROM orders
)
SELECT c.name, MAX(g.days) AS longest_pause
FROM gaps g
JOIN customers c ON c.id = g.customer_id
WHERE g.days IS NOT NULL
GROUP BY c.id, c.name;В стъпка: LAG(ordered_at) OVER (PARTITION BY customer_id ORDER BY ordered_at) и разликата с julianday. Отвън MAX(g.days) по клиент, без NULL-овете на първите поръчки. 17 клиенти. Калин има само една поръчка и затова го няма. Най-дълго е чакал Николай: 314 дни.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: JOIN: две таблици в една заявка · Подзаявки · Корелирани подзаявки · Отчет на стъпки: WITH
- Продължава в: Потоци от събития · Колонни и времеви бази · Пекарната, етап 6: отчетът, копието и DROP
- Задачи по темата: Подзаявки и прозорци