Database
 sql >> Baza danych >  >> RDS >> Database

Jak CTE może pomóc w pisaniu złożonych, potężnych zapytań:perspektywa wydajności

Często widzimy źle napisane złożone zapytania SQL działające na tabeli lub tabelach w bazach danych. Zapytania te powodują, że czas wykonania jest bardzo długi i pochłaniają ogromne ilości procesora i innych zasobów. Mimo to złożone zapytania w wielu przypadkach dostarczają cennych informacji aplikacji/osobie, która je uruchamia. Dlatego są użytecznymi zasobami we wszystkich zastosowaniach.

Złożone zapytania są trudne do debugowania

Jeśli przyjrzymy się problematycznym zapytaniom, wiele z nich jest złożonych, zwłaszcza te specyficzne, używane w raportach.

Złożone zapytania często składają się z pięciu lub więcej dużych tabel i są połączone ze sobą wieloma podzapytaniami. Każde podzapytanie zawiera klauzulę WHERE, która wykonuje proste lub złożone obliczenia i/lub transformacje danych, łącząc ze sobą odpowiednie tabele kolumn.

Takie zapytania mogą być trudne do debugowania bez zużywania dużej ilości zasobów. Powodem jest to, że trudno jest określić, czy każde podzapytanie i/lub połączone podzapytania dają poprawne wyniki.

Typowy scenariusz jest taki:dzwonią do Ciebie późno w nocy, aby rozwiązać problem na zajętym serwerze bazy danych ze złożonym zapytaniem, a Ty musisz to szybko naprawić. Jako programista lub administrator baz danych możesz mieć bardzo ograniczony czas i zasoby systemowe dostępne o późnej porze. Dlatego pierwszą rzeczą, jakiej potrzebujesz, jest plan debugowania problematycznego zapytania.

Czasami procedura debugowania przebiega dobrze. Czasami osiągnięcie celu i rozwiązanie problemu zajmuje dużo czasu i wysiłku.

Pisanie zapytań w strukturze CTE

Ale co by było, gdyby istniał sposób na pisanie złożonych zapytań, aby można było je szybko debugować, kawałek po kawałku?

Jest taki sposób. Nazywa się to Common Table Expression lub CTE.

Common Table Expression to standardowa funkcja w większości nowoczesnych baz danych, takich jak SQLServer, MySQL (od wersji 8.0), MariaDB (wersja 10.2.1), Db2 i Oracle. Ma prostą strukturę, która hermetyzuje jedno lub wiele podzapytań w tymczasowy nazwany zestaw wyników. Możesz użyć tego zestawu wyników dalej w innych nazwanych CTE lub podzapytaniach.

Wspólne wyrażenie tabelowe to do pewnego stopnia WIDOK, który istnieje tylko i do którego odwołuje się zapytanie w czasie wykonywania.

Przekształcenie złożonego zapytania w zapytanie w stylu CTE wymaga uporządkowanego myślenia. To samo dotyczy OOP z enkapsulacją podczas przepisywania złożonego zapytania do struktury CTE.

Musisz pomyśleć o:

  1. Każdy zestaw danych, które pobierasz z każdej tabeli.
  2. Jak są one połączone, aby zamknąć najbliższe podzapytania w jeden tymczasowy nazwany zestaw wyników.

Powtórz to dla każdego podzapytania i zestawu pozostałych danych, aż osiągniesz końcowy wynik zapytania. Zauważ, że każdy tymczasowy nazwany zestaw wyników jest również podzapytaniem.

Końcowa część zapytania powinna być bardzo „prostym” wyborem, zwracającym końcowy wynik do aplikacji. Po dotarciu do tej końcowej części możesz wymienić ją na zapytanie, które wybiera dane z indywidualnie nazwanego tymczasowego zestawu wyników.

W ten sposób debugowanie każdego tymczasowego zestawu wyników staje się łatwe.

Aby zrozumieć, w jaki sposób możemy budować nasze zapytania od prostych do złożonych, spójrzmy na strukturę CTE. Najprostsza forma jest następująca:

WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...

Tutaj CTE_1 to unikalna nazwa, którą nadajesz tymczasowemu nazwanemu zestawowi wyników. Może być tyle zestawów wyników, ile potrzeba. W ten sposób formularz rozciąga się do, jak pokazano poniżej:

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
....

Na początku każda część CTE jest tworzona osobno. Następnie postępuje, ponieważ CTE są ze sobą połączone, aby zbudować ostateczny zestaw wyników zapytania.

Przeanalizujmy teraz inny przypadek, przeszukując fikcyjną bazę danych Sales. Chcemy wiedzieć, jakie produkty, w tym ilość i łączną sprzedaż, zostały sprzedane w każdej kategorii w poprzednim miesiącu i który z nich osiągnął większą łączną sprzedaż niż miesiąc wcześniej.

Konstruujemy nasze zapytanie na kilka części CTE, gdzie każda część odwołuje się do poprzedniej. Najpierw konstruujemy zestaw wyników, aby wyświetlić szczegółowe dane z naszych tabel, których potrzebujemy do utworzenia reszty zapytania:

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

Następnym krokiem jest podsumowanie ilości i łącznych danych sprzedaży według każdej kategorii i nazw produktów:

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

Ostatnim krokiem jest utworzenie dwóch tymczasowych zestawów wyników reprezentujących dane z ostatniego miesiąca i poprzedniego miesiąca. Następnie odfiltruj dane, które mają zostać zwrócone jako końcowy zestaw wyników:

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

Zauważ, że w SQLServer ustawiasz getdate() zamiast CURRENT_DATE.

W ten sposób możemy wymienić ostatnią część z selekcją, która odpytuje poszczególne części CTE, aby zobaczyć wynik wybranej części. W rezultacie możemy szybko usunąć problem.

Ponadto, wykonując wyjaśnienie dla każdej części CTE (i całego zapytania), szacujemy, jak dobrze każda część i/lub całe zapytanie będzie działać w tabelach i danych.

Odpowiednio, możesz zoptymalizować każdą część, przepisując i/lub dodając odpowiednie indeksy do odpowiednich tabel. Następnie wyjaśnij całe zapytanie, aby zobaczyć ostateczny plan zapytania i w razie potrzeby kontynuuj optymalizację.

Zapytania rekurencyjne wykorzystujące strukturę CTE

Inną przydatną funkcją CTE jest tworzenie zapytań rekurencyjnych.

Rekurencyjne zapytania SQL pozwalają osiągnąć rzeczy, których nie wyobrażasz sobie za pomocą tego typu SQL i jego szybkości. Możesz rozwiązać wiele problemów biznesowych, a nawet przepisać złożoną logikę SQL/aplikacji do prostego rekursywnego wywołania SQL do bazy danych.

Istnieją niewielkie różnice w tworzeniu zapytań rekurencyjnych między systemami baz danych. Jednak cel jest ten sam.

Kilka przykładów użyteczności rekurencyjnego CTE:

  1. Możesz go użyć, aby znaleźć luki w danych.
  2. Możesz tworzyć schematy organizacyjne.
  3. Możesz utworzyć wstępnie obliczone dane do dalszego wykorzystania w innej części CTE
  4. Na koniec możesz utworzyć dane testowe.

Słowo rekurencyjne mówi wszystko. Masz zapytanie, które wielokrotnie wywołuje samo siebie z pewnym punktem początkowym i BARDZO WAŻNE punkt końcowy (bezpieczne wyjście jak to nazywam).

Jeśli nie masz bezpiecznego wyjścia lub Twoja formuła rekurencyjna wykracza poza to, jesteś w poważnych tarapatach. Zapytanie przejdzie w nieskończoną pętlę co skutkuje bardzo wysokim procesorem i bardzo wysokim wykorzystaniem LOGów. Doprowadzi to do wyczerpania pamięci i/lub przechowywania.

Jeśli Twoje zapytanie się zepsuje, musisz bardzo szybko pomyśleć, aby je wyłączyć. Jeśli nie możesz tego zrobić, natychmiast powiadom swojego administratora, aby zapobiec dławieniu się systemu bazy danych i zabiciu niekontrolowanego wątku.

Zobacz przykład:

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
);

Ten przykład to rekurencyjna składnia CTE MySQL/MariaDB. Dzięki niemu produkujemy tysiąc losowych dat. Poziom jest naszym licznikiem i bezpiecznym wyjściem, aby bezpiecznie wyjść z zapytania rekurencyjnego.

Jak pokazano, wiersz 2 jest naszym punktem początkowym, podczas gdy wiersze 4-5 to wywołanie rekurencyjne z punktem końcowym w klauzuli WHERE (wiersz 6). Linie 8 i 9 to wywołania w wykonaniu zapytania rekurencyjnego i pobraniu danych.

Inny przykład:

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);

Ten przykład to składnia SQLServer. Tutaj pozwalamy, aby część DatesCTE generowała wszystkie daty między dniem dzisiejszym a 1 stycznia poprzedniego roku. Używamy go do zwrotu wszystkich faktur należących do tych dat.

Punktem wyjścia jest @1stjanprevyear zmienna i bezpieczne wyjście @today . Możliwe jest maksymalnie 730 dni. W związku z tym maksymalna opcja rekurencji jest ustawiona na 1000, aby upewnić się, że się zatrzyma.

Moglibyśmy nawet pominąć MaxMinDates część i napisz ostatnią część, jak pokazano poniżej. Może to być szybsze podejście, ponieważ mamy pasującą klauzulę WHERE.

....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);

Wniosek

Podsumowując, krótko omówiliśmy i pokazaliśmy, jak przekształcić złożone zapytanie w zapytanie strukturalne CTE. Gdy zapytanie jest podzielone na różne części CTE, możesz użyć ich w innych częściach i wywołać niezależnie w końcowym zapytaniu SQL w celu debugowania.

Inną kluczową kwestią jest to, że użycie CTE ułatwia debugowanie złożonego zapytania, gdy jest ono podzielone na łatwe do zarządzania części, w celu zwrócenia prawidłowego i oczekiwanego zestawu wyników. Ważne jest, aby zdać sobie sprawę, że uruchomienie wyjaśnienia dla każdej części zapytania i całego zapytania ma kluczowe znaczenie dla zapewnienia, że ​​zapytanie i DBMS działają tak optymalnie, jak to możliwe.

Zilustrowałem również pisanie potężnego rekurencyjnego zapytania/części CTE w generowaniu danych w locie do dalszego wykorzystania w zapytaniu.

Warto zauważyć, że podczas pisania zapytania rekurencyjnego BARDZO uważaj, aby NIE zapomnieć o bezpiecznym wyjściu . Upewnij się, że dokładnie sprawdziłeś obliczenia użyte w bezpiecznym wyjściu, aby wygenerować sygnał zatrzymania i/lub użyj maksymalnej rekurencji opcja, którą zapewnia SQLServer.

Podobnie inne DBMS mogą używać cte_max_recursion_depth (MySQL 8.0) lub max_recursive_iteration (MariaDB 10.3) jako dodatkowe bezpieczne wyjścia.

Czytaj także

Wszystko, co musisz wiedzieć o SQL CTE w jednym miejscu


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Zapytania do bazy danych:jak znaleźć igłę w stogu siana?

  2. Samouczek SQL JOIN z przykładami

  3. W poszukiwaniu szybkiej pamięci lokalnej

  4. Statystyka szarpnięcia kolanem:PAGEIOLATCH_SH

  5. Wielowyrazowe TVF w Dynamics CRM