Database
 sql >> Datenbank >  >> RDS >> Database

Wie CTE beim Schreiben komplexer, leistungsstarker Abfragen helfen kann:Eine Leistungsperspektive

Wir sehen oft schlecht geschriebene komplexe SQL-Abfragen, die für eine Tabelle oder Tabellen in Datenbanken ausgeführt werden. Diese Abfragen machen die Ausführungszeit sehr lang und verursachen einen enormen Verbrauch von CPU und anderen Ressourcen. Dennoch liefern komplexe Abfragen in vielen Fällen wertvolle Informationen für die Anwendung/Person, die sie ausführt. Daher sind sie nützliche Hilfsmittel in allen Arten von Anwendungen.

Komplexe Abfragen sind schwer zu debuggen

Wenn wir uns die problematischen Abfragen genauer ansehen, sind viele von ihnen komplex, insbesondere die spezifischen Abfragen, die in Berichten verwendet werden.

Komplexe Abfragen bestehen oft aus fünf oder mehr großen Tabellen und werden durch viele Unterabfragen miteinander verbunden. Jede Unterabfrage hat eine WHERE-Klausel, die einfache bis komplexe Berechnungen und/oder Datentransformationen durchführt, während die relevanten Tabellen der Spalten miteinander verbunden werden.

Das Debuggen solcher Abfragen kann schwierig werden, ohne viele Ressourcen zu verbrauchen. Der Grund dafür ist, dass es schwierig ist festzustellen, ob jede Unterabfrage und/oder verbundene Unterabfragen korrekte Ergebnisse liefern.

Ein typisches Szenario ist:Sie rufen Sie spät in der Nacht an, um ein Problem auf einem ausgelasteten Datenbankserver mit einer komplexen Abfrage zu lösen, und Sie müssen es schnell beheben. Als Entwickler oder DBA haben Sie möglicherweise zu später Stunde nur sehr begrenzt Zeit und Systemressourcen zur Verfügung. Das erste, was Sie also brauchen, ist ein Plan, wie Sie die problematische Abfrage debuggen können.

Manchmal verläuft das Debugging-Verfahren gut. Manchmal braucht es viel Zeit und Mühe, bis Sie das Ziel erreichen und das Problem lösen.

Schreiben von Abfragen in CTE-Struktur

Aber was wäre, wenn es eine Möglichkeit gäbe, komplexe Abfragen so zu schreiben, dass man sie schnell Stück für Stück debuggen könnte?

Es gibt einen solchen Weg. Es heißt Common Table Expression oder CTE.

Common Table Expression ist eine Standardfunktion in den meisten modernen Datenbanken wie SQLServer, MySQL (ab Version 8.0), MariaDB (Version 10.2.1), Db2 und Oracle. Es hat eine einfache Struktur, die eine oder mehrere Unterabfragen in eine temporäre benannte Ergebnismenge kapselt. Sie können diese Ergebnismenge in anderen benannten CTEs oder Unterabfragen weiterverwenden.

Ein Common Table Expression ist gewissermaßen eine VIEW, die nur zum Zeitpunkt der Ausführung existiert und von der Abfrage referenziert wird.

Die Umwandlung einer komplexen Abfrage in eine Abfrage im CTE-Stil erfordert etwas strukturiertes Denken. Dasselbe gilt für OOP mit Kapselung beim Umschreiben einer komplexen Abfrage in eine CTE-Struktur.

Sie müssen Folgendes bedenken:

  1. Jeder Datensatz, den Sie aus jeder Tabelle ziehen.
  2. Wie sie zusammengefügt werden, um die engsten Unterabfragen in einer temporären benannten Ergebnismenge zu kapseln.

Wiederholen Sie dies für jede Unterabfrage und jeden verbleibenden Datensatz, bis Sie das endgültige Ergebnis der Abfrage erreichen. Beachten Sie, dass jede temporär benannte Ergebnismenge auch eine Unterabfrage ist.

Der letzte Teil der Abfrage sollte eine sehr „einfache“ Auswahl sein, die das Endergebnis an die Anwendung zurückgibt. Wenn Sie diesen letzten Teil erreicht haben, können Sie ihn mit einer Abfrage austauschen, die die Daten aus einer individuell benannten temporären Ergebnismenge auswählt.

Auf diese Weise wird das Debuggen jeder temporären Ergebnismenge zu einer einfachen Aufgabe.

Um zu verstehen, wie wir unsere Abfragen von einfach bis komplex aufbauen können, schauen wir uns die CTE-Struktur an. Die einfachste Form lautet wie folgt:

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

Hier CTE_1 ist ein eindeutiger Name, den Sie der temporären benannten Ergebnismenge geben. Es können beliebig viele Ergebnismengen vorhanden sein. Dadurch erstreckt sich das Formular wie unten gezeigt auf:

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

Zunächst wird jeder CTE-Teil separat erstellt. Dann geht es weiter, da CTEs miteinander verknüpft werden, um die endgültige Ergebnismenge der Abfrage aufzubauen.

Lassen Sie uns nun einen anderen Fall untersuchen und eine fiktive Sales-Datenbank abfragen. Wir möchten wissen, welche Produkte, einschließlich Menge und Gesamtumsatz, in jeder Kategorie im Vormonat verkauft wurden und welche davon mehr Gesamtumsatz als im Monat davor erzielt haben.

Wir bauen unsere Abfrage in mehrere CTE-Teile auf, wobei jeder Teil auf den vorherigen verweist. Zuerst konstruieren wir eine Ergebnismenge, um die detaillierten Daten aufzulisten, die wir aus unseren Tabellen benötigen, um den Rest der Abfrage zu bilden:

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

Der nächste Schritt besteht darin, die Mengen- und Gesamtverkaufsdaten nach Kategorien und Produktnamen zusammenzufassen:

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

Der letzte Schritt besteht darin, zwei temporäre Ergebnissätze zu erstellen, die die Daten des letzten Monats und des Vormonats darstellen. Filtern Sie danach die Daten heraus, die als endgültige Ergebnismenge zurückgegeben werden sollen:

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

Beachten Sie, dass Sie in SQLServer getdate() anstelle von CURRENT_DATE.

setzen

Auf diese Weise können wir den letzten Teil durch eine Auswahl ersetzen, die einzelne CTE-Teile abfragt, um das Ergebnis eines ausgewählten Teils zu sehen. Dadurch können wir das Problem schnell beheben.

Durch Ausführen einer Erklärung für jeden CTE-Teil (und die gesamte Abfrage) schätzen wir außerdem, wie gut jeder Teil und/oder die gesamte Abfrage bei den Tabellen und Daten abschneiden wird.

Dementsprechend können Sie jeden Teil optimieren, indem Sie die betreffenden Tabellen umschreiben und/oder geeignete Indizes hinzufügen. Dann erläutern Sie die gesamte Abfrage, um den endgültigen Abfrageplan zu sehen und bei Bedarf mit der Optimierung fortzufahren.

Rekursive Abfragen mit CTE-Struktur

Eine weitere nützliche Funktion von CTE ist das Erstellen rekursiver Abfragen.

Mit rekursiven SQL-Abfragen können Sie Dinge erreichen, die Sie sich mit dieser Art von SQL und seiner Geschwindigkeit nicht vorstellen würden. Sie können viele Geschäftsprobleme lösen und sogar einige komplexe SQL-/Anwendungslogiken in einen einfachen rekursiven SQL-Aufruf an die Datenbank umschreiben.

Es gibt geringfügige Abweichungen beim Erstellen rekursiver Abfragen zwischen Datenbanksystemen. Das Ziel ist jedoch dasselbe.

Einige Beispiele für die Nützlichkeit rekursiver CTE:

  1. Sie können es verwenden, um Datenlücken zu finden.
  2. Sie können Organigramme erstellen.
  3. Sie können vorberechnete Daten zur weiteren Verwendung in einem anderen CTE-Teil erstellen
  4. Schließlich können Sie Testdaten erstellen.

Das Wort rekursiv das sagt alles. Sie haben eine Abfrage, die sich selbst wiederholt mit einem Ausgangspunkt und EXTREM WICHTIG aufruft ein Endpunkt (ein ausfallsicherer Ausgang wie ich es nenne).

Wenn Sie keinen ausfallsicheren Ausgang haben oder Ihre rekursive Formel darüber hinausgeht, stecken Sie in großen Schwierigkeiten. Die Abfrage geht in eine Endlosschleife was zu einer sehr hohen CPU- und sehr hohen LOG-Auslastung führt. Dies führt zu Speicher- und/oder Speichererschöpfung.

Wenn Ihre Abfrage drunter und drüber geht, müssen Sie sehr schnell nachdenken, um sie zu deaktivieren. Wenn Sie dies nicht tun können, benachrichtigen Sie sofort Ihren DBA, damit er verhindert, dass das Datenbanksystem abwürgt und den außer Kontrolle geratenen Thread beendet.

Siehe Beispiel:

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

Dieses Beispiel ist eine rekursive MySQL/MariaDB-CTE-Syntax. Damit produzieren wir tausend zufällige Daten. Das Level ist unser Zähler und ausfallsicherer Ausgang, um die rekursive Abfrage sicher zu beenden.

Wie gezeigt, ist Zeile 2 unser Ausgangspunkt, während die Zeilen 4-5 der rekursive Aufruf mit dem Endpunkt in der WHERE-Klausel (Zeile 6) sind. Die Zeilen 8 und 9 sind die Aufrufe zum Ausführen der rekursiven Abfrage und zum Abrufen der Daten.

Ein weiteres Beispiel:

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

Dieses Beispiel ist eine SQLServer-Syntax. Hier lassen wir den DatesCTE-Teil alle Daten zwischen heute und dem 1. Januar des Vorjahres produzieren. Wir verwenden es, um alle Rechnungen zurückzusenden, die zu diesen Daten gehören.

Ausgangspunkt ist das @1stjanprevyear -Variable und den ausfallsicheren Exit @today . Maximal 730 Tage sind möglich. Daher wird die maximale Rekursionsoption auf 1000 gesetzt, um sicherzustellen, dass sie stoppt.

Wir könnten sogar die MaxMinDates überspringen Teil und schreiben Sie den letzten Teil, wie unten gezeigt. Es kann ein schnellerer Ansatz sein, da wir eine passende WHERE-Klausel haben.

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

Schlussfolgerung

Insgesamt haben wir kurz besprochen und gezeigt, wie man eine komplexe Abfrage in eine strukturierte CTE-Abfrage umwandelt. Wenn eine Abfrage in verschiedene CTE-Teile unterteilt ist, können Sie sie in anderen Teilen verwenden und zu Debugging-Zwecken unabhängig voneinander in der endgültigen SQL-Abfrage aufrufen.

Ein weiterer wichtiger Punkt ist, dass die Verwendung von CTE es einfacher macht, eine komplexe Abfrage zu debuggen, wenn sie in überschaubare Teile zerlegt wird, um die richtige und erwartete Ergebnismenge zurückzugeben. Es ist wichtig, sich bewusst zu machen, dass das Ausführen eines EXPLAIN für jeden Abfrageteil und die gesamte Abfrage entscheidend ist, um sicherzustellen, dass die Abfrage und das DBMS so optimal wie möglich ausgeführt werden.

Ich habe auch das Schreiben einer leistungsstarken rekursiven CTE-Abfrage/eines Teils zum Generieren von Daten im laufenden Betrieb zur weiteren Verwendung in einer Abfrage veranschaulicht.

Insbesondere beim Schreiben einer rekursiven Abfrage achten Sie SEHR darauf, den ausfallsicheren Exit NICHT zu vergessen . Stellen Sie sicher, dass Sie die Berechnungen, die im ausfallsicheren Ausgang verwendet werden, um ein Stoppsignal zu erzeugen, noch einmal überprüfen und/oder die maxrecursion verwenden Option, die SQLServer bereitstellt.

In ähnlicher Weise können andere DBMS entweder cte_max_recursion_depth verwenden (MySQL 8.0) oder max_recursive_iterations (MariaDB 10.3) als zusätzliche ausfallsichere Exits.

Lesen Sie auch

Alles, was Sie über SQL CTE wissen müssen, an einem Ort