Често виждаме лошо написани сложни SQL заявки, работещи срещу таблица или таблици в бази данни. Тези заявки правят времето за изпълнение много дълго и причиняват консумация на огромен процесор и други ресурси. Все пак сложните заявки предоставят ценна информация на приложението/лицето, което ги изпълнява в много случаи. Следователно те са полезни активи във всички видове приложения.
Сложните заявки са трудни за отстраняване на грешки
Ако разгледаме отблизо проблемните заявки, много от тях са сложни, особено тези, които се използват в отчетите.
Сложните заявки често се състоят от пет или повече големи таблици и са обединени от много подзаявки. Всяка подзаявка има клауза WHERE, която извършва прости до сложни изчисления и/или трансформации на данни, като съединява съответните колони на съответните таблици заедно.
Такива заявки могат да станат трудни за отстраняване на грешки, без да се консумират много ресурси. Причината е, че е трудно да се определи дали всяка подзаявка и/или обединени подзаявки дават правилни резултати.
Типичен сценарий е:те ви се обаждат късно през нощта, за да решат проблем на натоварен сървър на база данни с включена сложна заявка и трябва бързо да го поправите. Като разработчик или администратор на база данни може да разполагате с много ограничено време и системни ресурси в късен час. Следователно, първото нещо, от което се нуждаете, е план как да отстраните грешките в проблемната заявка.
Понякога процедурата за отстраняване на грешки върви добре. Понякога отнема много време и усилия, преди да постигнете целта и да разрешите проблема.
Писане на заявки в CTE структура
Но какво ще стане, ако имаше начин да се напишат сложни заявки, така че човек да може бързо да ги отстрани, парче по парче?
Има такъв начин. Нарича се Common Table Expression или CTE.
Common Table Expression е стандартна функция в повечето съвременни бази данни като SQLServer, MySQL (от версия 8.0), MariaDB (версия 10.2.1), Db2 и Oracle. Той има проста структура, която капсулира една или много подзаявки във временен набор от резултати. Можете да използвате този набор от резултати допълнително в други именувани CTE или подзаявки.
Общият табличен израз е до известна степен ИЗГЛЕД, който съществува само и е препратен от заявката в момента на изпълнение.
Преобразуването на сложна заявка в заявка в стил CTE изисква известно структурирано мислене. Същото важи и за ООП с капсулиране при пренаписване на сложна заявка в CTE структура.
Трябва да помислите за:
- Всеки набор от данни, който извличате от всяка таблица.
- Как се обединяват, за да се капсулират най-близките подзаявки в един временен набор от резултати с име.
Повторете го за всяка подзаявка и набор от данни, които остават, докато стигнете до крайния резултат от заявката. Имайте предвид, че всеки временен именуван резултатен набор също е подзаявка.
Последната част от заявката трябва да бъде много „прост“ избор, връщащ крайния резултат на приложението. След като стигнете до тази последна част, можете да я обмените със заявка, която избира данните от индивидуално наречен временен набор от резултати.
По този начин отстраняването на грешки на всеки временен набор от резултати става лесна работа.
За да разберем как можем да изградим нашите заявки от прости към сложни, нека разгледаме структурата на CTE. Най-простата форма е следната:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Тук CTE_1 е уникално име, което давате на временния именуван набор от резултати. Може да има толкова набори от резултати, колкото е необходимо. По този начин формата се простира до, както е показано по-долу:
WITH CTE_1 as (
select .... from some_table where ...
), CTE_2 as (
select .... from some_other_table where ...
)
select ... from CTE_1 c1,CTE_2 c2
where c1.col1 = c2.col1
....
Първо, всяка CTE част се създава отделно. След това напредва, тъй като CTE се свързват заедно, за да се изгради крайния набор от резултати на заявката.
Сега, нека разгледаме друг случай, като запитваме измислена база данни за продажби. Искаме да знаем какви продукти, включително количеството и общите продажби, са били продадени във всяка категория през предходния месец и кои от тях са получили повече общи продажби от месеца преди това.
Ние изграждаме нашата заявка в няколко CTE части, като всяка част препраща към предишната. Първо, ние изграждаме набор от резултати, за да изброим подробните данни, от които се нуждаем от нашите таблици, за да формираме останалата част от заявката:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
)
select dt.*
from detailed_data dt.
order by dt.order_date desc, dt.category_name, dt.product_name
Следващата стъпка е да се обобщят данните за количеството и общите продажби по всяка категория и имена на продукти:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
)
select ps.*
from product_sales ps
order by ps.year desc, ps.month desc, ps.category_name,ps.product_name
Последната стъпка е да създадете два временни набора от резултати, представящи данните за последния и предходния месец. След това филтрирайте данните, които да бъдат върнати като краен набор от резултати:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
), last_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -1
and ps.month = month(CURRENT_DATE) -1
), prev_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -2
and ps.month = month(CURRENT_DATE) -2
)
select lmd.*
from last_month_data lmd, prev_month_data pmd
where lmd.category_name = pmd.category_name
and lmd.product_name = pmd.product_name
and ( lmd.total_quantity > pmd.total_quantity
or lmd.total_product_sales > pmd.total_product_sales )
order by lmd.year desc, lmd.month desc, lmd.category_name,lmd.product_name, lmd.total_product_sales desc, lmd.total_quantity desc
Имайте предвид, че в SQLServer задавате getdate() вместо CURRENT_DATE.
По този начин можем да обменим последната част с избрана, която прави заявки за отделни CTE части, за да видим резултата от избрана част. В резултат на това можем бързо да отстраним проблема.
Освен това, като изпълняваме обяснение за всяка CTE част (и цялата заявка), ние оценяваме колко добре ще се представят всяка част и/или цялата заявка върху таблиците и данните.
Съответно, можете да оптимизирате всяка част чрез пренаписване и/или добавяне на подходящи индекси към участващите таблици. След това обяснявате цялата заявка, за да видите окончателния план на заявката и да продължите с оптимизация, ако е необходимо.
Рекурсивни заявки, използващи CTE структура
Друга полезна функция на CTE е създаването на рекурсивни заявки.
Рекурсивните SQL заявки ви позволяват да постигнете неща, които не бихте си представили възможни с този тип SQL и неговата скорост. Можете да разрешите много бизнес проблеми и дори да пренапишете сложна логика на SQL/приложение до просто рекурсивно SQL извикване към базата данни.
Има леки вариации в създаването на рекурсивни заявки между системите за бази данни. Целта обаче е същата.
Няколко примера за полезността на рекурсивния CTE:
- Можете да го използвате, за да намерите пропуски в данните.
- Можете да създавате организационни диаграми.
- Можете да създадете предварително изчислени данни, които да използвате допълнително в друга CTE част
- Накрая можете да създадете тестови данни.
Думатарекурсивна казва всичко. Имате заявка, която многократно се извиква с някаква начална точка и, ИЗКЛЮЧИТЕЛНО ВАЖНО, крайна точка (безопасен изход както аз го наричам).
Ако нямате безопасен изход или вашата рекурсивна формула надхвърля това, вие сте в голяма беда. Заявката ще премине в безкрана линията което води до много високо CPU и много високо използване на LOG. Това ще доведе до изчерпване на паметта и/или паметта.
Ако заявката ви се обърка, трябва да помислите много бързо, за да я деактивирате. Ако не можете да го направите, незабавно предупредете своя DBA, така че те да предотвратят задушаването на системата на базата данни, убивайки избягалата нишка.
Вижте примера:
with RECURSIVE mydates (level,nextdate) as (
select 1 level, FROM_UNIXTIME(RAND()*2147483647) nextdate from DUAL
union all
select level+1, FROM_UNIXTIME(RAND()*2147483647) nextdate
from mydates
where level < 1000
)
SELECT nextdate from mydates
);
Този пример е MySQL/MariaDB рекурсивен CTE синтаксис. С него произвеждаме хиляда произволни дати. Нивото е нашият брояч и безопасен изход за безопасен изход от рекурсивната заявка.
Както е показано, ред 2 е нашата начална точка, докато редове 4-5 са рекурсивното извикване с крайната точка в клаузата WHERE (ред 6). Редове 8 и 9 са извикванията при изпълнение на рекурсивната заявка и извличане на данните.
Друг пример:
DECLARE @today as date;
DECLARE @1stjanprevyear as date;
select @today = DATEADD(DAY, 0, DATEDIFF(DAY, 0, getdate())),
@1stjanprevyear = DATEFROMPARTS(YEAR(GETDATE())-1, 1, 1) ;
WITH DatesCTE as (
SELECT @1stjanprevyear as CalendarDate
UNION ALL
SELECT dateadd(day , 1, CalendarDate) AS CalendarDate FROM DatesCTE
WHERE dateadd (day, 1, CalendarDate) < @today
), MaxMinDates as (
SELECT Max(CalendarDate) MaxDate,Min(CalendarDate) MinDate
FROM DatesCTE
)
SELECT i.*
FROM InvoiceTable i, MaxMinDates t
where i.INVOICE_DATE between t.MinDate and t.MaxDate
OPTION (MAXRECURSION 1000);
Този пример е синтаксис на SQLServer. Тук оставяме частта DatesCTE да произвежда всички дати между днес и 1 януари на предходната година. Използваме го, за да върнем всички фактури, принадлежащи към тези дати.
Отправната точка е @1stjanprevyear променлива и безопасния изход @today . Възможни са максимум 730 дни. По този начин максималната опция за рекурсия е настроена на 1000, за да се гарантира, че спира.
Бихме могли дори да пропуснем MaxMinDates част и напишете последната част, както е показано по-долу. Може да бъде по-бърз подход, тъй като имаме съвпадаща клауза WHERE.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Заключение
Като цяло ние накратко обсъдихме и показахме как да трансформирате сложна заявка в CTE структурирана заявка. Когато една заявка е разбита на различни CTE части, можете да ги използвате в други части и да се обаждате независимо в крайната SQL заявка за целите на отстраняване на грешки.
Друг ключов момент е, че използването на CTE улеснява отстраняването на грешки в сложна заявка, когато е разбита на управляеми части, за връщане на правилния и очакван набор от резултати. Важно е да осъзнаете, че изпълнението на обяснение за всяка част от заявката и за цялата заявка е от решаващо значение, за да се гарантира, че заявката и СУБД работят възможно най-оптимално.
Освен това илюстрирах писането на мощна рекурсивна CTE заявка/част при генериране на данни в движение, които да се използват допълнително в заявка.
По-специално, когато пишете рекурсивна заявка, Бъдете МНОГО внимателни да НЕ забравите безопасния изход . Уверете се, че сте проверили отново изчисленията, използвани в безопасния изход, за да произведете сигнал за спиране и/или използвайте maxrecursion опция, която SQLServer предоставя.
По същия начин други СУБД могат да използват или cte_max_recursion_depth (MySQL 8.0) или max_recursive_iterations (MariaDB 10.3) като допълнителни безопасни изходи.
Прочетете също
Всичко, което трябва да знаете за SQL CTE на едно място