Environ. trad. : Dans cet article, le chef d'Ă©quipe de la sociĂ©tĂ© britannique Ticketsolve partage une solution Ă son problĂšme trĂšs spĂ©cifique, tout en prĂ©sentant des approches gĂ©nĂ©rales pour crĂ©er des fonctions dites d'accumulation Ă l'aide des fonctionnalitĂ©s modernes de MySQL 8.0. Ses listes sont claires et fournies avec des explications dĂ©taillĂ©es, ce qui aide Ă comprendre l'essence du problĂšme, mĂȘme pour ceux qui ne s'y sont pas plongĂ©s si profondĂ©ment.
Une stratégie courante pour effectuer des mises à jour à l'aide de fonctions cumulatives dans MySQL consiste à utiliser des variables et des modÚles personnalisés
UPDATE [...] SET mycol = (@myvar := EXPRESSION(@myvar, mycol))
.
Ce modĂšle ne fonctionne pas bien avec l'optimiseur (conduisant Ă un comportement non dĂ©terministe), ils ont donc dĂ©cidĂ© de l'abandonner. Le rĂ©sultat est une sorte de vide car une logique (relativement) complexe est dĂ©sormais plus difficile Ă mettre en Ćuvre (du moins avec la mĂȘme simplicitĂ©).
L'article discutera de deux façons de l'implĂ©menter: en utilisant des fonctions de fenĂȘtre (approche canonique) et en utilisant des CTE rĂ©cursifs (expressions de table gĂ©nĂ©rales).
Exigences et contexte
Bien que les CTE soient assez intuitifs, pour ceux qui ne les connaissent pas trÚs bien, je recommande de se référer à mon article précédent sur ce sujet .
La mĂȘme chose est vraie pour les fonctions de fenĂȘtre: je vais commenter les requĂȘtes / concepts en dĂ©tail, mais une idĂ©e gĂ©nĂ©rale ne fait toujours pas de mal. Un grand nombre de livres et de publications sont consacrĂ©s aux fonctions de fenĂȘtre (c'est pourquoi je n'ai toujours pas Ă©crit Ă leur sujet); cependant, dans la plupart des exemples, les calculs sont effectuĂ©s soit sur les rĂ©sultats financiers, soit sur des indicateurs dĂ©mographiques. Cependant, dans cet article, j'utiliserai un cas rĂ©el.
Pour les logiciels, je recommande d'utiliser MySQL 8.0.19 (mais pas obligatoire). Toutes les expressions doivent ĂȘtre exĂ©cutĂ©es dans la mĂȘme console pour ĂȘtre rĂ©utilisĂ©es
@venue_id
.
Dans le monde du logiciel, il existe un cĂ©lĂšbre dilemme architectural: faut-il implĂ©menter la logique au niveau de l'application ou au niveau de la base de donnĂ©es? Bien que ce soit une question parfaitement valable, dans notre cas, je suppose que la logique doit rester au niveau de base; la raison peut ĂȘtre, par exemple, les exigences de vitesse (comme ce fut le cas dans notre cas).
TĂąche
Dans cette tùche, nous attribuons des siÚges dans une certaine salle (théùtre).
à des fins commerciales, chaque emplacement doit se voir attribuer un soi-disant «regroupement» - un numéro supplémentaire le représentant.
Voici l'algorithme pour déterminer la valeur de regroupement:
- commencer Ă 0 et en haut Ă gauche;
- s'il y a un espace vide entre l'actuel et le précédent, ou s'il s'agit d'une nouvelle ligne, alors on ajoute 2 à la valeur précédente (si ce n'est pas la premiÚre place absolue), sinon, on augmente la valeur de 1;
- attribuer un groupement Ă un lieu;
- aller Ă un nouvel endroit dans la mĂȘme ligne ou Ă la ligne suivante (si la prĂ©cĂ©dente est terminĂ©e) et rĂ©pĂ©ter Ă partir du point 2; nous continuons tout jusqu'Ă ce que les places soient Ă©puisĂ©es.
Algorithme en 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
Dans la vraie vie, nous voulons que la configuration de gauche donne les valeurs affichées à droite:
xâ 0 1 2 0 1 2
y âââââŹââââŹââââź âââââŹââââŹââââź
â 0 â x â x â â â 1 â 2 â â
âââââŒââââŒâââ†âââââŒââââŒââââ€
1 â x â â x â â 4 â â 6 â
âââââŒââââŒâââ†âââââŒââââŒââââ€
2 â x â â â â 8 â â â
â°ââââŽââââŽâââ⯠â°ââââŽââââŽââââŻ
EntraĂźnement
Laissez la table de base avoir la structure minimaliste suivante:
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)
);
Nous n'avons pas vraiment besoin des colonnes
row
et number
. Par contre, nous ne voulons pas utiliser une table dont les enregistrements sont entiĂšrement contenus dans l'index (juste pour ĂȘtre au plus prĂšs de vrais problĂšmes).
Sur la base du diagramme ci-dessus, les coordonnées de chaque emplacement sont (y, x):
- (0, 0), (0, 1)
- (1, 0), (1, 2)
- (20)
Notez que nous utilisons y comme premiÚre coordonnée, car cela facilite le suivi des lignes.
Vous devez charger un nombre suffisant d'enregistrements pour empĂȘcher l'optimiseur de trouver des chemins courts inattendus. Bien sĂ»r, nous utilisons des CTE rĂ©cursifs:
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;
Quelques notes:
- Ici, CTE est utilisé d'une maniÚre intéressante (espérons-le!): Chaque boucle représente un venue_id, mais puisque nous voulons que plusieurs emplacements soient générés pour chaque lieu, nous faisons une jointure croisée avec la table contenant les emplacements.
- Le constructeur de lignes v8.0.19 (
VALUES ROW()...
) est utilisé pour représenter une table ( joignable ) sans la créer. - GénÚre des valeurs aléatoires pour la ligne et le nombre comme espaces réservés.
- Par souci de simplicité, nous n'avons fait aucune optimisation (par exemple, les types de données sont plus larges que nécessaire; les index sont ajoutés avant l'insertion d'enregistrements, etc.).
Ancienne approche
La bonne vieille approche est assez simple et directe:
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)
Et bien c'Ă©tait facile (mais n'oubliez pas les avertissements)!
Une petite digression: dans ce cas, j'utilise les propriétés de l'arithmétique booléenne. Les expressions suivantes sont équivalentes:
SELECT seats.x > @x + 1 OR seats.y != @y `increment`;
SELECT IF (
seats.x > @x + 1 OR seats.y != @y,
1,
0
) `increment`;
Certains trouvent cela intuitif, d'autres non; c'est une question de goût. à partir de maintenant, j'utiliserai une expression plus compacte.
Voyons le résultat:
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 |
-- +-------+------+------+----------+
Excellente approche!
Hélas, il a un défaut "mineur": ça marche trÚs bien sauf quand ça ne marche pas ...
Le fait est que l'optimiseur de requĂȘtes n'effectue pas forcĂ©ment des calculs de gauche Ă droite, donc les opĂ©rations d'assignation (: =) peuvent ĂȘtre effectuĂ©es dans le mauvais ordre conduisant au mauvais rĂ©sultat. Les gens sont souvent confrontĂ©s Ă ce problĂšme aprĂšs la mise Ă jour de MySQL.
Dans MySQL 8.0, cette fonctionnalité est en effet obsolÚte:
-- 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)'.
-- [...]
Eh bien, réglons la situation!
Approche moderne n ° 1: fonctions de fenĂȘtrage
L'introduction des fonctions de fenĂȘtre a Ă©tĂ© un Ă©vĂ©nement trĂšs attendu dans le monde MySQL.
D'une maniĂšre gĂ©nĂ©rale, la nature «glissante» des fonctions de fenĂȘtre fonctionne bien avec les fonctions cumulatives. Cependant, certaines fonctions cumulatives complexes nĂ©cessitent les rĂ©sultats de la derniĂšre expression - fonctionnalitĂ© que les fonctions de fenĂȘtre ne prennent pas en charge car elles opĂšrent sur des colonnes.
Cela ne veut pas dire que le problĂšme ne peut pas ĂȘtre rĂ©solu, il doit simplement ĂȘtre repensĂ©.
Dans notre cas, la tĂąche peut ĂȘtre divisĂ©e en deux parties. Le regroupement de chaque emplacement peut ĂȘtre considĂ©rĂ© comme la somme de deux valeurs:
- le numéro de série de chaque lieu,
- la valeur cumulée des incréments de toutes les places précédant celle-ci.
Ceux qui sont familiers avec les fonctions de fenĂȘtrage reconnaĂźtront ici les modĂšles typiques.
Le numéro de séquence de chaque siÚge est une fonction intégrée:
ROW_NUMBER() OVER <window>
Mais avec la valeur cumulée, tout est bien plus intéressant ... Pour le calculer, on effectue deux actions:
- compter l'incrément pour chaque lieu et l'écrire dans le tableau (ou CTE),
- puis, pour chaque emplacement, nous additionnons les incrĂ©ments pour cet emplacement Ă l'aide de la fonction de fenĂȘtre.
Jetons un Ćil Ă 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 |
-- +-------+---+---+----------+
GĂ©nial!
(Notez que j'omets désormais la mise à jour par souci de simplicité.)
Analysons la demande.
Logique de haut niveau
Le CTE suivant (édité) :
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 |
-- +-------+-----------+
⊠Calcule les incréments pour chaque emplacement à partir du précédent (plus d'informations
LAG()
plus tard). Il fonctionne sur chaque enregistrement et celui qui le précÚde et n'est pas cumulatif.
Maintenant, pour calculer les incrĂ©ments cumulatifs, nous allons simplement utiliser une fonction de fenĂȘtre pour calculer la somme jusqu'Ă et y compris chaque emplacement:
-- (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.)
Fonction de fenĂȘtre LAG ()
La fonction LAG, dans sa forme la plus simple (
LAG(x)
), renvoie la valeur prĂ©cĂ©dente d'une colonne donnĂ©e. L'inconvĂ©nient classique de ces fonctions est de gĂ©rer la ou les premiĂšres entrĂ©es de la fenĂȘtre. Puisqu'il n'y a aucun enregistrement prĂ©cĂ©dent, ils renvoient NULL. Dans le cas de LAG, vous pouvez spĂ©cifier la valeur souhaitĂ©e comme troisiĂšme paramĂštre:
LAG(x, 1, x - 1) -- `x -1`
LAG(y, 1, y) -- `y`
En spĂ©cifiant les valeurs par dĂ©faut, nous nous assurons que la toute premiĂšre place dans les limites de la fenĂȘtre aura la mĂȘme logique que pour la place suivant l'autre (x-1) et sans changer la ligne (y).
Une solution alternative consiste Ă utiliser
IFNULL
, cependant, les expressions sont assez lourdes:
-- , !
--
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)
Le deuxiĂšme paramĂštre
LAG()
est le nombre de positions Ă reculer dans la fenĂȘtre; 1 est la valeur prĂ©cĂ©dente (c'est aussi la valeur par dĂ©faut).
Aspects techniques
FenĂȘtres nommĂ©es
Notre requĂȘte utilise la mĂȘme fenĂȘtre plusieurs fois. Les deux requĂȘtes suivantes sont formellement Ă©quivalentes:
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;
Cependant, la seconde peut conduire Ă un comportement non optimal (que j'ai rencontrĂ© - du moins dans le passĂ©): l'optimiseur peut considĂ©rer les fenĂȘtres indĂ©pendantes et calculer chacune sĂ©parĂ©ment. Pour cette raison, je vous conseille de toujours utiliser des fenĂȘtres nommĂ©es (du moins lorsqu'elles sont rĂ©pĂ©tĂ©es).
Instruction PARTITION BY
Les fonctions de fenĂȘtrage sont gĂ©nĂ©ralement exĂ©cutĂ©es sur une partition. Dans notre cas, cela ressemblera Ă ceci:
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); -- !
Puisque la fenĂȘtre correspond Ă l'ensemble complet des enregistrements (qui est filtrĂ© par la condition
WHERE
), nous n'avons pas besoin de le spécifier (la partition).
Mais si cette requĂȘte devait ĂȘtre exĂ©cutĂ©e sur l'ensemble de la table
seats
, alors il faudrait le faire pour que la fenĂȘtre soit rĂ©initialisĂ©e pour tout le monde venue_id
.
Tri
La demande
ORDER BY
est dĂ©finie au niveau de la fenĂȘtre:
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)
Dans ce cas, le tri des fenĂȘtres est distinct de SELECT. Il est trĂšs important! Le comportement de cette demande:
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
⊠indéfini. Passons au manuel :
Les chaĂźnes de rĂ©sultat de la requĂȘte sont dĂ©terminĂ©es Ă partir de la clause FROM aprĂšs que les clauses WHERE, GROUP BY et HAVING ont Ă©tĂ© exĂ©cutĂ©es, et l'exĂ©cution dans la fenĂȘtre se produit avant ORDER BY, LIMIT et SELECT DISTINCT.
Quelques considérations
En termes généraux, pour ce type de problÚme, il est logique de calculer le changement d'état pour chaque enregistrement, puis de les additionner - au lieu de représenter chaque enregistrement en fonction du précédent.
Cette solution est plus complexe que la fonctionnalitĂ© qu'elle remplace, mais en mĂȘme temps elle est fiable. HĂ©las, cette approche n'est pas toujours possible ou facile Ă mettre en Ćuvre. C'est lĂ que les CTE rĂ©cursifs entrent en jeu.
Approche moderne n ° 2: CTE récursifs
Cette approche nécessite un peu de ruse en raison des capacités limitées du CTE dans MySQL. D'un autre cÎté, il s'agit d'une solution directe unique, qui ne nécessite donc pas de repenser une approche globale.
Commençons par une version simplifiĂ©e de la requĂȘte finale:
-- `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! Cette requĂȘte est (relativement) simple, mais surtout, elle exprime la fonction de regroupement cumulatif de la maniĂšre la plus simple possible:
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
La logique est claire mĂȘme pour ceux qui ne sont pas trop familiers avec CTE. La premiĂšre rangĂ©e est le premier siĂšge de la salle, dans l'ordre:
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
Dans la partie récursive, on itÚre:
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
Conditionnez
WHERE
avec les opérateurs ORDER BY
et LIMIT
trouvez simplement le prochain endroit, un endroit avec le mĂȘme venue_id
, mais utilisé pour les coordonnées lshimi (x, y) dans la séquence (venue_id, x, y).
La partie
s.venue_id
de l'expression de tri est trĂšs importante! Cela nous permet d'utiliser un index.
Opérateur
SELECT
:
- effectue l'accumulation (calcule
(p_)grouping
), - fournit des valeurs pour la position actuelle (
s.id
,s.venue_id
,s.y
,s.x
) dans le cycle suivant.
Nous choisissons
FROM groupings
de répondre aux exigences de récursivité CTE.
Ce qui est intéressant ici, c'est que nous utilisons un CTE récursif comme itérateur, en récupérant une table
groupings
dans une sous-requĂȘte rĂ©cursive et en la joignant seats
pour trouver les données pour un traitement ultérieur.
JOIN
est formellement croisé, mais LIMIT
un seul enregistrement est renvoyé à cause de l'opérateur .
Version de travail
Malheureusement, la requĂȘte ci-dessus ne fonctionne pas car elle
ORDER BY
n'est actuellement pas prise en charge dans les sous-requĂȘtes rĂ©cursives. De plus, la sĂ©mantique LIMIT
utilisĂ©e ici diffĂšre de la sĂ©mantique typique qui s'applique Ă une requĂȘte externe :
LIMIT est dĂ©sormais pris en charge [..] L'effet sur l'ensemble de donnĂ©es rĂ©sultant est le mĂȘme que l'utilisation de LIMIT avec un SELECT externe
Cependant, ce n'est pas un problĂšme si grave. Jetons un coup d'Ćil Ă une version de travail:
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 |
-- +-------+------+------+------------+
Il est un peu inconfortable d'utiliser une sous-requĂȘte, mais cette approche fonctionne et le passe-partout est minimal ici, car plusieurs expressions sont nĂ©cessaires de toute façon.
Ici, au lieu de faire le tri et la limitation associés à l'union
groupings
et seats
, nous le faisons dans la sous-requĂȘte et la passons Ă la requĂȘte externe, qui sĂ©lectionne alors uniquement l'enregistrement cible.
RĂ©flexions sur la performance
Examinons le plan d'exĂ©cution de la requĂȘte en utilisant EXPLAIN ANALYZE:
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)
Le plan est conforme aux attentes. Dans ce cas, la base du plan optimal réside dans les recherches d'index:
-> 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)
... d'une importance capitale. Les performances chuteront considérablement si vous scannez les index (c'est-à -dire que vous scannez linéairement les enregistrements d'index au lieu de rechercher les bons).
Ainsi, pour que cette stratĂ©gie fonctionne, les index associĂ©s doivent ĂȘtre en place et ĂȘtre utilisĂ©s le plus efficacement possible par l'optimiseur.
Si les restrictions sont levĂ©es Ă l'avenir, il ne sera pas nĂ©cessaire d'utiliser une sous-requĂȘte, ce qui simplifiera considĂ©rablement la tĂąche de l'optimiseur.
Alternative pour les plans sous-optimaux
Dans le cas oĂč le plan optimal ne peut pas ĂȘtre dĂ©terminĂ©, utilisez une table temporaire:
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;
MĂȘme si les analyses d'index sont passĂ©es dans cette requĂȘte, elles coĂ»tent cher, car la table est
selected_seats
trĂšs petite.
Conclusion
Je suis trĂšs heureux que le flux de travail efficace mais dĂ©fectueux puisse maintenant ĂȘtre remplacĂ© par la fonctionnalitĂ© assez simple introduite dans MySQL 8.0.
En attendant, le développement de nouvelles fonctionnalités pour 8.0 se poursuit, ce qui améliore encore une version déjà réussie.
Récursivité réussie!
PS du traducteur
Lisez aussi sur notre blog: