Корелирани подзаявки
Накратко: Корелираната подзаявка ползва колона от външната заявка и затова се смята наново за всеки ред, а подзаявката във FROM прави от резултата нова таблица.
Проблем
Скъпо за кого?
От предишния урок знаеш кои продукти са над средната цена в магазина:
Всичко от кухнята е „скъпо“, а нито един хляб не е. Само че хлябът не се сравнява с тиган. Управителят иска да знае кои продукти са скъпи за своята категория: сиренето срещу млечните, кафето срещу напитките.
Идеята
Свързана подзаявка
Подзаявката може да ползва колона от реда, който външната заявка разглежда в момента:
p е продуктът отвън, p2 са продуктите в подзаявката. Условието p2.category_id = p.category_id казва „продуктите от неговата категория“. За белия хляб подзаявката смята средната на хляба (1,60 €), за сиренето средната на млечните (4,66 €).
Такава подзаявка се казва свързана (correlated subquery): тя зависи от реда отвън и се пресмята наново за всеки ред. Обикновената подзаявка от предишния урок дава един и същ отговор за всички редове.
Без кратките имена p и p2 не би станало: и двете са таблицата products, а базата трябва да знае коя category_id е отвън и коя отвътре.
Идеята
Във WHERE
Сега въпросът на управителя: кои продукти са над средната за категорията си?
Девет продукта, поне по един от всяка категория: пълнозърнестият хляб е над средното за хляба, а прахът за пране е над средното за почистването.
Подаръчната карта я няма, макар че е за 20 €. Категорията ѝ е NULL, p2.category_id = NULL не е вярно за никой ред, средната е NULL и сравнението е неизвестно. Същият NULL от урока за NULL, само по-скрит.
Пробвай
Последната поръчка
За всеки клиент покажи името му и датата на последната му поръчка (колона last_order). Клиентите без поръчки също трябва да са в списъка.
Решение
SELECT name,
(SELECT MAX(o.ordered_at) FROM orders o WHERE o.customer_id = c.id) AS last_order
FROM customers c;(SELECT MAX(o.ordered_at) FROM orders o WHERE o.customer_id = c.id) като колона. 20 реда. Теодора и Явор нямат поръчки, затова при тях е NULL. С JOIN и GROUP BY те щяха да изчезнат, освен ако не е LEFT JOIN.
По-точно, за любопитните
По замисъл свързаната подзаявка се изпълнява веднъж за всеки ред отвън: 20 клиенти, 20 пъти. Базите често я пренаписват на нещо по-бързо, но при големи таблици си струва да сравниш с JOIN и GROUP BY. Как се гледа какво прави базата, ще видим в модула за скоростта.
Свързаната подзаявка дава и номера на последната поръчка, не само датата ѝ. В модула за обобщаване видя, че SQLite го позволява с MAX и свободна колона, а другите бази не. Ето начинът, който върви навсякъде:
18 реда, по един за всеки клиент с поръчки. Ако клиент има две поръчки в последния си ден, ще излязат и двете.
Виж го
Средната стойност на поръчка
Счетоводството пита колко е средната стойност на една поръчка. Първият опит:
Трябват две стъпки: първо стойността на всяка поръчка, после средната на тези стойности. Първата стъпка вече я можеш:
Идеята
Подзаявка във FROM
Резултатът на всяка заявка е таблица. Затова може да стои във FROM, там където пишеш име на таблица:
Вътрешната заявка прави таблица с 60 реда, по един на поръчка. Външната я чете като всяка друга таблица и смята средното: 32,73 €. Това се казва подзаявка във FROM (derived table). Името след AS е как наричаш новата таблица.
По-точно, за любопитните
SQLite не изисква име на подзаявката във FROM, но MySQL и PostgreSQL до версия 16 го искат задължително. Давай ѝ име винаги и заявката ще върви навсякъде.
Пробвай
Колко продукта има в поръчка
Складът планира кутиите и пита: средно колко различни продукта има в една поръчка? Закръгли до два знака след запетаята (колона avg_lines). Полето вече брои продуктите във всяка поръчка.
Решение
SELECT ROUND(AVG(lines), 2) AS avg_lines
FROM (
SELECT order_id, COUNT(*) AS lines
FROM order_items
GROUP BY order_id
) AS t;Броят на всяка поръчка става таблица във FROM, а отвън е ROUND(AVG(lines), 2): 2,53 продукта. 152 реда в 60 поръчки.
Упражнение
Две задачи
1. Маркетингът иска да си върне клиентите, които не са поръчвали от лятото.
Покажи имената на клиентите, чиято последна поръчка е преди 1 юли 2026 г.
Решение
SELECT name FROM customers c
WHERE (SELECT MAX(o.ordered_at) FROM orders o WHERE o.customer_id = c.id) < '2026-07-01';WHERE (SELECT MAX(o.ordered_at) FROM orders o WHERE o.customer_id = c.id) < '2026-07-01'. Осем клиенти. Теодора и Явор ги няма: без поръчки последната дата е NULL, а сравнението с NULL е неизвестно.
2. Колко харчи средно един клиент в магазина?
Сметни средната сума, която клиент е похарчил за поръчките си без отказаните, закръглена до два знака (колона avg_spent). Броят се само клиентите с поне една поръчка.
Решение
SELECT ROUND(AVG(spent), 2) AS avg_spent
FROM (
SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS spent
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY o.customer_id
) AS t;Във FROM е сумата на всеки клиент (JOIN на orders и order_items, WHERE o.status <> 'cancelled', GROUP BY o.customer_id), а отвън е ROUND(AVG(spent), 2): 94,28 €. Средното е върху 18 клиенти. Двамата без поръчки не са в подзаявката, затова не дърпат средното надолу.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Агрегатни функции: COUNT, SUM, AVG · GROUP BY: отчет по групи · JOIN: две таблици в една заявка · Подзаявки
- Продължава в: Отчет на стъпки: WITH · Прозоречни функции · Естествен ключ и денормализация · Колонни и времеви бази
- Задачи по темата: Подзаявки и прозорци