C, PHP, VB, .NET

Дневникът на Филип Петров


* Рекурсивно извикване на подзаявки – още примери

Публикувано на 05 юни 2020 в раздел Бази от Данни.

В предишната статия показах един прост пример за използване на оператор WITH. Тук ще дам още няколко.

Задача 1. Да се генерира таблица с 100 произволни числа (разбира се може да промените това число на колкото искате)

Решение: В рекурсивната заявка дефинираме брояч, с който лимитираме броя на рекурсивните извиквания до 100:

WITH RECURSIVE random_nums AS (
     SELECT 1 AS counter, RAND() AS rand_num
     UNION ALL
     SELECT counter+1, RAND()
     FROM random_nums
     WHERE counter < 100
)
SELECT rand_num FROM random_nums;

Задача 2. Дадена е таблица със служители, в която е описано кой на кого е пряк ръководител:

CREATE TABLE employees(
  id INT PRIMARY KEY,
  name VARCHAR(255) NOT NULL UNIQUE,
  managerID INT NULL,
  FOREIGN KEY (managerID) REFERENCES employees(id)
              ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO employees(id, name, managerID)
VALUES (1, "Ivan", NULL),
       (2, "Petar", 1),
       (3, "Alexandra", 1),
       (4, "Maria", 2),
       (5, "Marian", 2),
       (6, "Margarita", 3),
       (7, "Mihail", 3),
       (8, "Eva", 6);

Трябва да изкараме списък с имената на всички служители и до тях да добавим имената на техните мениджъри.

Решение: Класическото решение на тази задача е със SELF JOIN:

SELECT slujitel.name AS slujitel, manager.name AS reportsTo
FROM employees AS slujitel LEFT JOIN employees AS manager
     ON slujitel.managerID = manager.id;

Ето как може същото да се осъществи с рекурсивна заявка:

WITH RECURSIVE empl_managers(id, slujitel, reportsTo) AS(
	SELECT id, name, CAST(NULL AS VARCHAR(255))
	FROM employees
	WHERE managerID IS NULL
	UNION ALL
	SELECT employees.id, employees.name, empl_managers.slujitel
	FROM employees
	JOIN empl_managers
	     ON empl_managers.id = employees.managerID
)
SELECT * FROM empl_managers;

В тази заявка показвам и как може да именовате колоните на резултатната таблица без да използвате "AS" в инициализиращата SELECT заявка (подават се в скоби непосредствено след името на таблицата). Използваме оператор CAST, за да дефинираме типа данни на колоната - ако не го направим, MariaDB ще го вземе автоматично от заявката с началното условие, а после ще прави автоматичен CAST на данните при рекурсивната заявка.

В тази задача ползата не е съвсем очевидна. Можем обаче да усложняваме условието. Например:

Задача 3. Вземете таблицата от задача 2 и изведете списък с преките и непреките подчинени на Alexandra. Преки подчинени на Alexandra са Margarita и Mihail, а непряк подчинен е Eva (тя е подчинена на Margarita, която от своя страна на Alexandra).

Решение: с минимални усилия променяме началното условие (вместо да започне от топ-мениджърите, да се започне конкретно от Alexandra), а единствено в изходната таблица премахваме самата нея (да са спазим условието на задачата):

WITH RECURSIVE empl_managers(id, slujitel, reportsTo) AS(
	SELECT id, name, CAST(NULL AS VARCHAR(255))
	FROM employees
	WHERE name = "Alexandra"
	UNION ALL
	SELECT employees.id, employees.name, empl_managers.slujitel
	FROM employees
	JOIN empl_managers
	     ON empl_managers.id = employees.managerID
)
SELECT * FROM empl_managers
WHERE reportsTo IS NOT NULL;

Друг поглед над това решение е, че по този начин имитирахме, че все едно Alexandra е мениджър от най-високо ниво (дефинирахме нейният мениджър като NULL в началното условие и от там насетне пуснахме предишната рекурсия).

Задача 4. Нека усложним задача 3, като добавим още един непряк подчинен на Alexandra:

INSERT INTO employees(id, name, managerID)
VALUES (9, "Tony", 8);

Виждаме, че вече имаме по-дълбока йерархия. Нека ги дефинираме като "нива". Преките ѝ подчинени Margarita и Mihail ще са от първо ниво, Eva ще е от второ ниво (подчинена на подчинена), а Tony ще е от трето ниво (подчинен на подчинена на подчинена). Новата задача е да се изкарат подчинените на Alexandra до второ ниво.

Решение: Ще дефинираме променлива lvl, с която ще мерим нивото, на което се намираме. С всяко рекурсивно извикване ще я увеличаваме с единица, а в WHERE условие ще спираме рекурсията когато това ниво е прескочило 1:

WITH RECURSIVE empl_managers(id, slujitel, reportsTo, lvl) AS(
	SELECT id, name, CAST(NULL AS VARCHAR(255)), 0
	FROM employees
	WHERE name = "Alexandra"
	UNION ALL
	SELECT employees.id, employees.name, empl_managers.slujitel, lvl+1
	FROM employees
	JOIN empl_managers
	     ON empl_managers.id = employees.managerID
	WHERE lvl<2
)
SELECT * FROM empl_managers
WHERE reportsTo IS NOT NULL;

Задача 5. От таблица employees да се изведе списък с номерата на служителите и конкатенирана поредица от техните мениджъри. Например за Eva трябва да се изведе следния резултат: 8, Eva->Margarita->Alexandra->Ivan

Решение: Можем да се възползваме от функцията CONCAT по следния начин:

WITH RECURSIVE empl_managers(id, slujitel) AS(
	SELECT id, name
	FROM employees
	WHERE managerID IS NULL
	UNION ALL
	SELECT employees.id, CONCAT(employees.name, "->", empl_managers.slujitel)
	FROM employees
	JOIN empl_managers
	     ON empl_managers.id = employees.managerID
)
SELECT * FROM empl_managers;

Виждате, че при изваждане на резултатите вече пропускаме колона reportsTo, защото тя е ненужна - всичко натрупваме в slujitel.

Задача 6. Дадена са таблици с градове и възможни автобусни маршрути между тях:

CREATE TABLE cities(
	id INT UNSIGNED PRIMARY KEY,
	city VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE bus_routes(
	source INT UNSIGNED,
	destination INT UNSIGNED,
	FOREIGN KEY(source) REFERENCES cities(id),
	FOREIGN KEY(destination) REFERENCES cities(id),
	PRIMARY KEY(source, destination)
);

INSERT INTO cities(id, city) VALUES
(1, "Sofia"), (2, "Plovdiv"), 
(3, "Asenovgrad"), (4, "Stara Zagora");

INSERT INTO bus_routes(source, destination)
VALUES (1,2), (2,1), (1,4), (4,1), 
       (2,3), (3,2), (2,4), (4,2);

Изведете списък с всички възможни маршрути.

Решение: Класическото решение чрез SELF JOIN ще бъде следното:

SELECT sources.id, sources.city, destinations.id, destinations.city
FROM bus_routes
JOIN cities AS sources
     ON sources.id = bus_routes.source
JOIN cities AS destinations
     ON destinations.id = bus_routes.destination
ORDER BY sources.id;

Използвайки рекурсивна подзаявка можем да осъществим същото чрез:

WITH RECURSIVE sofia_routes(source_id, source, destination_id, destination) AS(
	SELECT id, city, CAST(NULL AS INT), CAST(NULL AS VARCHAR(255))
	FROM cities
	UNION ALL
	SELECT sofia_routes.source_id, sofia_routes.source, cities.id, cities.city
	FROM bus_routes
	JOIN sofia_routes
	     ON sofia_routes.source_id = bus_routes.source
	JOIN cities
	     ON cities.id = bus_routes.destination
	WHERE sofia_routes.destination_id IS NULL
)
SELECT * FROM sofia_routes WHERE sofia_routes.destination_id IS NOT NULL
ORDER BY source_id;

Забележете, че спираме рекурсията чрез условието "WHERE sofia_routes.destination_id IS NULL". Ако не го направим, ще достигнем до безкрайно зацикляне (обяснете защо). Алтернативно можем да постигнем същото чрез оператор UNION вместо UNION ALL:

WITH RECURSIVE sofia_routes(source_id, source, destination_id, destination) AS(
	SELECT id, city, CAST(NULL AS INT), CAST(NULL AS VARCHAR(255))
	FROM cities
	UNION
	SELECT sofia_routes.source_id, sofia_routes.source, cities.id, cities.city
	FROM bus_routes
	JOIN sofia_routes
	     ON sofia_routes.source_id = bus_routes.source
	JOIN cities
	     ON cities.id = bus_routes.destination
)
SELECT * FROM sofia_routes WHERE sofia_routes.destination_id IS NOT NULL
ORDER BY source_id;

Обяснете защо и как UNION решава проблема със зациклянето.

Допълнителна задача 1: Напишете заявка, с която изведете всички градове, от които може да се достигне пряко или непряко (с прекачване през друг град) тръгвайки от Sofia. Например между Sofia и Asenovgrad в примерните данни няма пряка автобусна линия, но има връзка с прекачване през Plovdiv.

Допълнителна задача 2: Решете допълнителта задача 1, като опишете маршрутите чрез конкатенация. Например: Sofia->Plovdiv->Asenovgrad

 



Добави коментар

Адресът на електронната поща няма да се публикува


*