Ca. ĂŒbers. : In diesem Artikel gibt der Teamleiter des britischen Unternehmens Ticketsolve eine Lösung fĂŒr sein sehr spezifisches Problem und demonstriert allgemeine AnsĂ€tze zur Erstellung sogenannter Akkumulationsfunktionen mit modernen MySQL 8.0-Funktionen. Die Auflistungen sind klar und mit detaillierten ErklĂ€rungen versehen, die helfen, das Wesentliche des Problems zu verstehen, selbst fĂŒr diejenigen, die sich nicht so tief damit befasst haben.
Eine gĂ€ngige Strategie fĂŒr die DurchfĂŒhrung von Aktualisierungen mithilfe kumulativer Funktionen in MySQL ist die Verwendung benutzerdefinierter Variablen und Muster
UPDATE [...] SET mycol = (@myvar := EXPRESSION(@myvar, mycol))
.
Dieses Muster funktioniert nicht gut mit dem Optimierer (was zu nicht deterministischem Verhalten fĂŒhrt), daher haben sie beschlossen, es aufzugeben. Das Ergebnis ist eine Art Leere, weil (relativ) komplexe Logik jetzt schwieriger zu implementieren ist (zumindest mit der gleichen Einfachheit).
In diesem Artikel werden zwei Möglichkeiten zur Implementierung erlĂ€utert: Verwendung von Fensterfunktionen (kanonischer Ansatz) und Verwendung rekursiver CTEs (allgemeine TabellenausdrĂŒcke).
Anforderungen und Hintergrund
Obwohl CTEs ziemlich intuitiv sind, empfehle ich fĂŒr diejenigen, die mit ihnen nicht sehr vertraut sind, auf meinen vorherigen Beitrag zu diesem Thema zu verweisen .
Gleiches gilt fĂŒr Fensterfunktionen: Ich werde Abfragen / Konzepte im Detail kommentieren, aber eine allgemeine Idee tut immer noch nicht weh. Eine groĂe Anzahl von BĂŒchern und Veröffentlichungen widmet sich Fensterfunktionen (weshalb ich immer noch nicht darĂŒber geschrieben habe); In den meisten Beispielen werden Berechnungen jedoch entweder anhand von Finanzergebnissen oder anhand demografischer Indikatoren durchgefĂŒhrt. In diesem Artikel werde ich jedoch einen realen Fall verwenden.
Auf der Softwareseite empfehle ich die Verwendung von MySQL 8.0.19 (aber nicht erforderlich). Alle AusdrĂŒcke mĂŒssen in derselben Konsole ausgefĂŒhrt werden, um wiederverwendet zu werden
@venue_id
.
In der Softwarewelt gibt es ein bekanntes Architekturdilemma: Sollte Logik auf Anwendungsebene oder auf Datenbankebene implementiert werden? WĂ€hrend dies eine legitime Frage ist, gehe ich in unserem Fall davon aus, dass die Logik auf der Basisebene bleiben sollte ; Der Grund hierfĂŒr könnten beispielsweise Geschwindigkeitsanforderungen sein (wie dies in unserem Fall der Fall war).
Aufgabe
Bei dieser Aufgabe vergeben wir SitzplÀtze in einem bestimmten Saal (Theater).
FĂŒr geschĂ€ftliche Zwecke muss jedem Standort eine sogenannte "Gruppierung" zugewiesen werden - eine zusĂ€tzliche Nummer, die ihn darstellt.
Hier ist der Algorithmus zum Bestimmen des Gruppierungswerts:
- beginne bei 0 und oben links;
- Wenn zwischen dem aktuellen und dem vorherigen Leerzeichen ein Leerzeichen steht oder dies eine neue Zeile ist, addieren wir 2 zum vorherigen Wert (wenn dies nicht der absolute erste Platz ist), andernfalls erhöhen wir den Wert um 1;
- einem Ort eine Gruppierung zuweisen;
- Gehen Sie zu einer neuen Stelle in derselben Reihe oder zur nÀchsten Reihe (wenn die vorherige vorbei ist) und wiederholen Sie ab Punkt 2; Wir machen alles weiter, bis die PlÀtze ausgehen.
Algorithmus im Pseudocode:
current_grouping = 0
for each row:
for each number:
if (is_there_a_space_after_last_seat or is_a_new_row) and is_not_the_first_seat:
current_grouping += 2
else
current_grouping += 1
seat.grouping = current_grouping
Im wirklichen Leben möchten wir, dass die Konfiguration links die rechts angezeigten Werte angibt:
xâ 0 1 2 0 1 2
y âââââŹââââŹââââź âââââŹââââŹââââź
â 0 â x â x â â â 1 â 2 â â
âââââŒââââŒâââ†âââââŒââââŒââââ€
1 â x â â x â â 4 â â 6 â
âââââŒââââŒâââ†âââââŒââââŒââââ€
2 â x â â â â 8 â â â
â°ââââŽââââŽâââ⯠â°ââââŽââââŽââââŻ
Ausbildung
Lassen Sie die Basistabelle die folgende minimalistische Struktur haben:
CREATE TABLE seats (
id INT AUTO_INCREMENT PRIMARY KEY,
venue_id INT,
y INT,
x INT,
`row` VARCHAR(16),
number INT,
`grouping` INT,
UNIQUE venue_id_y_x (venue_id, y, x)
);
Wir brauchen die
row
und Spalten nicht wirklich number
. Auf der anderen Seite möchten wir keine Tabelle verwenden, deren DatensÀtze vollstÀndig im Index enthalten sind (nur um den tatsÀchlichen Problemen nÀher zu sein).
Basierend auf dem obigen Diagramm sind die Koordinaten jedes Ortes (y, x):
- (0, 0), (0, 1)
- (1, 0), (1, 2)
- (20)
Beachten Sie, dass wir y als erste Koordinate verwenden, da dies die Verfolgung der Zeilen erleichtert.
Sie mĂŒssen eine ausreichend groĂe Anzahl von DatensĂ€tzen laden, um zu verhindern, dass der Optimierer unerwartete kurze Pfade findet. NatĂŒrlich verwenden wir rekursive CTEs:
INSERT INTO seats(venue_id, y, x, `row`, number)
WITH RECURSIVE venue_ids (id) AS
(
SELECT 0
UNION ALL
SELECT id + 1 FROM venue_ids WHERE id + 1 < 100000
)
SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */
v.id,
c.y, c.x,
CHAR(ORD('A') + FLOOR(RAND() * 3) USING ASCII) `row`,
FLOOR(RAND() * 3) `number`
FROM venue_ids v
JOIN (
VALUES
ROW(0, 0),
ROW(0, 1),
ROW(1, 0),
ROW(1, 2),
ROW(2, 0)
) c (y, x)
;
ANALYZE TABLE seats;
Ein paar Anmerkungen:
- Hier wird CTE auf interessante (hoffentlich!) Weise verwendet: Jede Schleife stellt eine Veranstaltungsort-ID dar. Da jedoch fĂŒr jeden Veranstaltungsort mehrere Standorte generiert werden sollen, fĂŒhren wir eine KreuzverknĂŒpfung mit der Tabelle durch, die die Standorte enthĂ€lt.
- Verwendete den Zeilenkonstruktor aus Version 8.0.19 (
VALUES ROW()...
), um eine ( verbindbare ) Tabelle darzustellen, ohne sie tatsĂ€chlich zu erstellen. - Erzeugt zufĂ€llige Werte fĂŒr Zeile und Zahl als Platzhalter.
- Der Einfachheit halber haben wir keine Optimierungen vorgenommen (z. B. sind Datentypen breiter als erforderlich; Indizes werden vor dem EinfĂŒgen von DatensĂ€tzen usw. hinzugefĂŒgt).
Alter Ansatz
Der gute alte Ansatz ist ganz einfach und unkompliziert:
SET @venue_id = 5000; -- venue id; () id
SET @grouping = -1;
SET @y = -1;
SET @x = -1;
WITH seat_groupings (id, y, x, `grouping`, tmp_y, tmp_x) AS
(
SELECT
id, y, x,
@grouping := @grouping + 1 + (seats.x > @x + 1 OR seats.y != @y),
@y := seats.y,
@x := seats.x
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
)
UPDATE
seats s
JOIN seat_groupings sg USING (id)
SET s.grouping = sg.grouping
;
-- Query OK, 5 rows affected, 3 warnings (0,00 sec)
Nun, das war einfach (aber vergessen Sie nicht die Warnungen)!
Ein kleiner Exkurs: In diesem Fall verwende ich die Eigenschaften der Booleschen Arithmetik. Die folgenden AusdrĂŒcke sind Ă€quivalent:
SELECT seats.x > @x + 1 OR seats.y != @y `increment`;
SELECT IF (
seats.x > @x + 1 OR seats.y != @y,
1,
0
) `increment`;
Einige finden das intuitiv, andere nicht; es ist Geschmackssache. Von nun an werde ich einen kompakteren Ausdruck verwenden.
Mal sehen, das Ergebnis:
SELECT id, y, x, `grouping` FROM seats WHERE venue_id = @venue_id ORDER BY y, x;
-- +-------+------+------+----------+
-- | id | y | x | grouping |
-- +-------+------+------+----------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 0 | 8 |
-- +-------+------+------+----------+
Toller Ansatz!
Leider hat es einen "kleinen" Nachteil: Es funktioniert hervorragend, auĂer wenn es nicht funktioniert ...
Tatsache ist, dass das Abfrageoptimierungsprogramm nicht unbedingt Berechnungen von links nach rechts durchfĂŒhrt, sodass Zuweisungen (: =) in der falschen Reihenfolge ausgefĂŒhrt werden können was zum falschen Ergebnis fĂŒhrt. Nach dem Update von MySQL treten hĂ€ufig Probleme auf.
In MySQL 8.0 ist diese FunktionalitÀt in der Tat veraltet:
-- UPDATE.
--
SHOW WARNINGS\G
-- *************************** 1. row ***************************
-- Level: Warning
-- Code: 1287
-- Message: Setting user variables within expressions is deprecated and will be removed in a future release. Consider alternatives: 'SET variable=expression, ...', or 'SELECT expression(s) INTO variables(s)'.
-- [...]
Nun, lassen Sie uns die Situation beheben!
Moderner Ansatz Nr. 1: Fensterfunktionen
Die EinfĂŒhrung von Fensterfunktionen war in der MySQL-Welt ein mit Spannung erwartetes Ereignis.
Im Allgemeinen funktioniert die "gleitende" Natur von Fensterfunktionen gut mit kumulativen Funktionen. Einige komplexe kumulative Funktionen erfordern jedoch die Ergebnisse des letzten Ausdrucks - Funktionen, die Fensterfunktionen nicht unterstĂŒtzen, da sie mit Spalten arbeiten.
Dies bedeutet nicht, dass das Problem nicht gelöst werden kann: Es muss nur ĂŒberdacht werden.
In unserem Fall kann die Aufgabe in zwei Teile unterteilt werden. Die Gruppierung fĂŒr jeden Standort kann als Summe von zwei Werten betrachtet werden:
- die Seriennummer jedes Ortes,
- der kumulative Wert der Inkremente aller Stellen vor dieser
Wer mit Fensterfunktionen vertraut ist, erkennt hier die typischen Muster.
Die Sequenznummer jedes Sitzes ist eine eingebaute Funktion:
ROW_NUMBER() OVER <window>
Aber mit dem kumulativen Wert ist alles viel interessanter ... Um ihn zu berechnen, fĂŒhren wir zwei Aktionen aus:
- ZĂ€hlen Sie das Inkrement fĂŒr jeden Ort und schreiben Sie es in die Tabelle (oder den CTE).
- Dann addieren wir fĂŒr jeden Standort die Inkremente fĂŒr diesen Standort mithilfe der Fensterfunktion.
Werfen wir einen Blick auf SQL:
WITH
increments (id, increment) AS
(
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1 OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
)
SELECT
s.id, y, x,
ROW_NUMBER() OVER tzw + SUM(increment) OVER tzw `grouping`
FROM seats s
JOIN increments i USING (id)
WINDOW tzw AS (ORDER BY y, x)
;
-- +-------+---+---+----------+
-- | id | y | x | grouping |
-- +-------+---+---+----------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 1 | 8 |
-- +-------+---+---+----------+
Toll!
(Beachten Sie, dass ich das UPDATE der Einfachheit halber von nun an weglasse.)
Lassen Sie uns die Anfrage analysieren.
Logik auf hoher Ebene
Der folgende CTE (bearbeitet) :
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1 OR y != LAG(y, 1, y) OVER tzw `increment`
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
;
-- +-------+-----------+
-- | id | increment |
-- +-------+-----------+
-- | 24887 | 0 |
-- | 27186 | 0 |
-- | 29485 | 1 |
-- | 31784 | 1 |
-- | 34083 | 1 |
-- +-------+-----------+
⊠Berechnet die Inkremente fĂŒr jeden Standort gegenĂŒber dem vorherigen (dazu
LAG()
spÀter mehr). Es funktioniert auf jedem Datensatz und dem vorhergehenden und ist nicht kumulativ.
Um die kumulativen Inkremente zu berechnen, verwenden wir einfach eine Fensterfunktion, um die Summe bis einschlieĂlich jeder Position zu berechnen:
-- (CTE here...)
SELECT
s.id, y, x,
ROW_NUMBER() OVER tzw `pos.`,
SUM(increment) OVER tzw `cum.incr.`
FROM seats s
JOIN increments i USING (id)
WINDOW tzw AS (ORDER BY y, x);
-- +-------+---+---+------+-----------+
-- | id | y | x | pos. | cum.incr. | (grouping)
-- +-------+---+---+------+-----------+
-- | 24887 | 0 | 0 | 1 | 0 | = 1 + 0 (curr.)
-- | 27186 | 0 | 1 | 2 | 0 | = 2 + 0 (#24887) + 0 (curr.)
-- | 29485 | 1 | 0 | 3 | 1 | = 3 + 0 (#24887) + 0 (#27186) + 1 (curr.)
-- | 31784 | 1 | 2 | 4 | 2 | = 4 + 0 (#24887) + 0 (#27186) + 1 (#29485) + 1 (curr.)
-- | 34083 | 2 | 1 | 5 | 3 | = 5 + 0 (#24887) + 0 (#27186) + 1 (#29485) + 1 (#31784)â”
-- +-------+---+---+------+-----------+ + 1 (curr.)
LAG () Fensterfunktion
Die LAG-Funktion gibt in ihrer einfachsten Form (
LAG(x)
) den vorherigen Wert einer bestimmten Spalte zurĂŒck. Die klassische Unannehmlichkeit mit solchen Funktionen besteht darin, die ersten EintrĂ€ge im Fenster zu behandeln. Da es keinen vorherigen Datensatz gibt, geben sie NULL zurĂŒck. Bei LAG können Sie den gewĂŒnschten Wert als dritten Parameter angeben:
LAG(x, 1, x - 1) -- `x -1`
LAG(y, 1, y) -- `y`
Durch die Angabe der Standardwerte stellen wir sicher, dass die allererste Stelle in den Fenstergrenzen dieselbe Logik hat wie die Stelle nach der anderen (x-1) und ohne die Zeile (y) zu Àndern.
Eine alternative Lösung ist die Verwendung
IFNULL
, die AusdrĂŒcke sind jedoch ziemlich umstĂ€ndlich:
-- , !
--
IFNULL(x > LAG(x) OVER tzw + 1 OR y != LAG(y) OVER tzw, 0)
IFNULL(x > LAG(x) OVER tzw + 1, FALSE) OR IFNULL(y != LAG(y) OVER tzw, FALSE)
Der zweite Parameter
LAG()
ist die Anzahl der Positionen, die innerhalb des Fensters zurĂŒckbewegt werden mĂŒssen. 1 ist der vorherige Wert (dies ist auch der Standardwert).
Technische Aspekte
Benannte Fenster
Unsere Abfrage verwendet oft dasselbe Fenster. Die folgenden zwei Abfragen sind formal gleichwertig:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x);
SELECT
id,
x > LAG(x, 1, x - 1) OVER (ORDER BY y, x) + 1
OR y != LAG(y, 1, y) OVER (ORDER BY y, x)
FROM seats
WHERE venue_id = @venue_id;
Das zweite kann jedoch zu einem nicht optimalen Verhalten fĂŒhren (auf das ich - zumindest in der Vergangenheit - gestoĂen bin): Der Optimierer kann die Fenster als unabhĂ€ngig betrachten und jedes einzeln berechnen. Aus diesem Grund empfehle ich Ihnen, immer benannte Fenster zu verwenden (zumindest wenn sie wiederholt werden).
PARTITION BY-Anweisung
Normalerweise werden Fensterfunktionen fĂŒr eine Partition ausgefĂŒhrt. In unserem Fall sieht es so aus:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (PARTITION BY venue_id ORDER BY y, x); -- !
Da das Fenster mit dem vollstĂ€ndigen Satz von DatensĂ€tzen ĂŒbereinstimmt (der nach der Bedingung gefiltert wird
WHERE
), mĂŒssen wir ihn nicht angeben (die Partition).
Wenn diese Abfrage jedoch fĂŒr die gesamte Tabelle ausgefĂŒhrt
seats
werden mĂŒsste, mĂŒsste sie durchgefĂŒhrt werden, damit das Fenster fĂŒr alle zurĂŒckgesetzt wird venue_id
.
Sortierung
Die Anforderung
ORDER BY
wird auf Fensterebene festgelegt:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
In diesem Fall ist die Fenstersortierung von SELECT getrennt. Es ist sehr wichtig! Das Verhalten dieser Anfrage:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS ()
ORDER BY y, x
⊠nicht definiert. Wenden wir uns dem Handbuch zu :
Die Abfrageergebniszeichenfolgen werden aus der FROM-Klausel bestimmt, nachdem die WHERE-, GROUP BY- und HAVING-Klauseln ausgefĂŒhrt wurden, und die AusfĂŒhrung innerhalb des Fensters erfolgt vor ORDER BY, LIMIT und SELECT DISTINCT.
Einige Ăberlegungen
Im Allgemeinen ist es fĂŒr diese Art von Problem sinnvoll, die StatusĂ€nderung fĂŒr jeden Datensatz zu berechnen und dann zu summieren - anstatt jeden Datensatz als Funktion des vorherigen darzustellen.
Diese Lösung ist komplexer als die FunktionalitÀt, die sie ersetzt, aber gleichzeitig zuverlÀssig. Leider ist dieser Ansatz nicht immer möglich oder einfach zu implementieren. Hier kommen rekursive CTEs ins Spiel.
Moderner Ansatz Nr. 2: Rekursive CTEs
Dieser Ansatz erfordert aufgrund der begrenzten Funktionen des CTE in MySQL einige Tricks. Auf der anderen Seite handelt es sich um eine einheitliche, direkte Lösung, bei der kein Ăberdenken eines globalen Ansatzes erforderlich ist.
Beginnen wir mit einer vereinfachten Version der endgĂŒltigen Anfrage:
-- `p_` `Previous`
--
WITH RECURSIVE groupings (p_id, p_venue_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s
WHERE s.venue_id = p_venue_id AND (s.y, s.x) > (p_y, p_x)
ORDER BY s.venue_id, s.y, s.x
LIMIT 1
)
SELECT * FROM groupings;
Bingo! Diese Abfrage ist (relativ) einfach, aber was noch wichtiger ist, sie drĂŒckt die kumulative Gruppierungsfunktion auf einfachste Weise aus:
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
-- :
@grouping := @grouping + 1 + (seats.x > @x + 1 OR seats.y != @y),
@y := seats.y,
@x := seats.x
Die Logik ist auch fĂŒr diejenigen klar, die mit CTE nicht allzu vertraut sind. Die erste Reihe ist der erste Platz in der Halle, in der Reihenfolge:
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
Im rekursiven Teil iterieren wir:
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s
WHERE s.venue_id = p_venue_id AND (s.y, s.x) > (p_y, p_x)
ORDER BY s.venue_id, s.y, s.x
LIMIT 1
Konditionieren Sie
WHERE
zusammen mit den Operatoren ORDER BY
und LIMIT
suchen Sie einfach den nÀchsten Ort, einen Ort mit demselben venue_id
, der jedoch fĂŒr lshimi-Koordinaten (x, y) in der Sequenz (Veranstaltungsort_ID, x, y) verwendet wird.
Der Teil
s.venue_id
im Sortierausdruck ist sehr wichtig! Es erlaubt uns, einen Index zu verwenden.
Betreiber
SELECT
:
- fĂŒhrt Akkumulation durch (berechnet
(p_)grouping
), - stellt Werte fĂŒr die aktuelle Position (
s.id
,s.venue_id
,s.y
,s.x
) in dem nÀchsten Zyklus.
Wir entscheiden uns,
FROM groupings
die Anforderungen fĂŒr die CTE-RekursivitĂ€t zu erfĂŒllen.
Interessant ist hier, dass wir einen rekursiven CTE als Iterator verwenden, aus einer Tabelle
groupings
in einer rekursiven Unterabfrage abrufen und diese zusammenfĂŒgen seats
, um die Daten fĂŒr die weitere Verarbeitung zu finden.
JOIN
ist formal cross, aber aufgrund des Operators wird LIMIT
nur ein Datensatz zurĂŒckgegeben.
Arbeitsversion
Leider funktioniert die obige Abfrage nicht, da sie
ORDER BY
derzeit in rekursiven Unterabfragen nicht unterstĂŒtzt wird. DarĂŒber hinaus unterscheidet sich die hier verwendete Semantik LIMIT
von der typischen Semantik , die fĂŒr eine externe Abfrage gilt :
LIMIT wird jetzt unterstĂŒtzt [..] Die Auswirkung auf das resultierende Dataset entspricht der Verwendung von LIMIT mit einem externen SELECT
Dies ist jedoch kein so ernstes Problem. Schauen wir uns eine funktionierende Version an:
WITH RECURSIVE groupings (p_id, p_venue_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s WHERE s.id = (
SELECT si.id
FROM seats si
WHERE si.venue_id = p_venue_id AND (si.y, si.x) > (p_y, p_x)
ORDER BY si.venue_id, si.y, si.x
LIMIT 1
)
)
SELECT * FROM groupings;
-- +-------+------+------+------------+
-- | p_id | p_y | p_x | p_grouping |
-- +-------+------+------+------------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 0 | 8 |
-- +-------+------+------+------------+
Es ist etwas unangenehm, eine Unterabfrage zu verwenden, aber dieser Ansatz funktioniert und die Boilerplate ist hier minimal, da ohnehin mehrere AusdrĂŒcke erforderlich sind.
Anstatt die mit der Vereinigung
groupings
und verknĂŒpfte Reihenfolge und Begrenzung seats
durchzufĂŒhren, fĂŒhren wir dies hier innerhalb der Unterabfrage durch und ĂŒbergeben es an die Ă€uĂere Abfrage, die dann nur den Zieldatensatz auswĂ€hlt.
Ăberlegungen zur Leistung
Lassen Sie uns den AbfrageausfĂŒhrungsplan mit EXPLAIN ANALYZE untersuchen:
mysql> EXPLAIN ANALYZE WITH RECURSIVE groupings [...]
-> Table scan on groupings (actual time=0.000..0.001 rows=5 loops=1)
-> Materialize recursive CTE groupings (actual time=0.140..0.141 rows=5 loops=1)
-> Limit: 1 row(s) (actual time=0.019..0.019 rows=1 loops=1)
-> Index lookup on seats using venue_id_y_x (venue_id=(@venue_id)) (cost=0.75 rows=5) (actual time=0.018..0.018 rows=1 loops=1)
-> Repeat until convergence
-> Nested loop inner join (cost=3.43 rows=2) (actual time=0.017..0.053 rows=2 loops=2)
-> Scan new records on groupings (cost=2.73 rows=2) (actual time=0.001..0.001 rows=2 loops=2)
-> Filter: (s.id = (select #5)) (cost=0.30 rows=1) (actual time=0.020..0.020 rows=1 loops=5)
-> Single-row index lookup on s using PRIMARY (id=(select #5)) (cost=0.30 rows=1) (actual time=0.014..0.014 rows=1 loops=5)
-> Select #5 (subquery in condition; dependent)
-> Limit: 1 row(s) (actual time=0.007..0.008 rows=1 loops=9)
-> Filter: ((si.y,si.x) > (groupings.p_y,groupings.p_x)) (cost=0.75 rows=5) (actual time=0.007..0.007 rows=1 loops=9)
-> Index lookup on si using venue_id_y_x (venue_id=groupings.p_venue_id) (cost=0.75 rows=5) (actual time=0.006..0.006 rows=4 loops=9)
Der Plan entspricht den Erwartungen. In diesem Fall liegt die Basis des optimalen Plans in der Indexsuche:
-> Nested loop inner join (cost=3.43 rows=2) (actual time=0.017..0.053 rows=2 loops=2)
-> Single-row index lookup on s using PRIMARY (id=(select #5)) (cost=0.30 rows=1) (actual time=0.014..0.014 rows=1 loops=5)
-> Index lookup on si using venue_id_y_x (venue_id=groupings.p_venue_id) (cost=0.75 rows=5) (actual time=0.006..0.006 rows=4 loops=9)
... von gröĂter Bedeutung. Die Leistung nimmt erheblich ab, wenn Sie die Indizes scannen (dh die IndexdatensĂ€tze linear scannen, anstatt nach den richtigen zu suchen).
Damit diese Strategie funktioniert, mĂŒssen die verknĂŒpften Indizes vorhanden sein und vom Optimierer so effizient wie möglich verwendet werden.
Wenn die EinschrĂ€nkungen in Zukunft aufgehoben werden, muss keine Unterabfrage verwendet werden, was die Aufgabe fĂŒr den Optimierer erheblich vereinfacht.
Alternative fĂŒr suboptimale PlĂ€ne
Wenn der optimale Plan nicht ermittelt werden kann, verwenden Sie eine temporÀre Tabelle:
CREATE TEMPORARY TABLE selected_seats (
id INT NOT NULL PRIMARY KEY,
y INT,
x INT,
UNIQUE (y, x)
)
SELECT id, y, x
FROM seats WHERE venue_id = @venue_id;
WITH RECURSIVE
groupings (p_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s WHERE s.id = (
SELECT ss.id
FROM selected_seats ss
WHERE (ss.y, ss.x) > (p_y, p_x)
ORDER BY ss.y, ss.x
LIMIT 1
)
)
SELECT * FROM groupings;
Selbst wenn Index-Scans in dieser Abfrage ĂŒbergeben werden, kosten sie
selected_seats
viel Geld , da die Tabelle sehr klein ist.
Fazit
Ich freue mich sehr, dass der effiziente, aber fehlerhafte Workflow jetzt durch die in MySQL 8.0 eingefĂŒhrte relativ einfache FunktionalitĂ€t ersetzt werden kann.
In der Zwischenzeit wird die Entwicklung neuer Funktionen fĂŒr 8.0 fortgesetzt, was eine bereits erfolgreiche Veröffentlichung noch besser macht.
Erfolgreiche Rekursion!
PS vom Ăbersetzer
Lesen Sie auch in unserem Blog: