Utilisation des fonctions de fenĂȘtre et des CTE dans MySQL 8.0 pour implĂ©menter un total cumulatif sans hacks





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ésUPDATE [...] 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:



  1. commencer Ă  0 et en haut Ă  gauche;
  2. 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;
  3. attribuer un groupement Ă  un lieu;
  4. 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 rowet 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:



  1. 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.
  2. Le constructeur de lignes v8.0.19 ( VALUES ROW()...) est utilisé pour représenter une table ( joignable ) sans la créer.
  3. GénÚre des valeurs aléatoires pour la ligne et le nombre comme espaces réservés.
  4. 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 BYest 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 WHEREavec les opĂ©rateurs ORDER BYet LIMITtrouvez 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_idde 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 groupingsde 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 groupingsdans une sous-requĂȘte rĂ©cursive et en la joignant seatspour trouver les donnĂ©es pour un traitement ultĂ©rieur.



JOINest formellement croisé, mais LIMITun seul enregistrement est renvoyé à cause de l'opérateur .



Version de travail



Malheureusement, la requĂȘte ci-dessus ne fonctionne pas car elle ORDER BYn'est actuellement pas prise en charge dans les sous-requĂȘtes rĂ©cursives. De plus, la sĂ©mantique LIMITutilisĂ©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 groupingset 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_seatstrĂš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:






All Articles