Uso de funciones de ventana y CTE en MySQL 8.0 para implementar un total acumulativo sin hacks





Aprox. transl. : En este artículo, el líder del equipo de la empresa británica Ticketsolve comparte una solución a su problema muy específico, mientras demuestra enfoques generales para crear las llamadas funciones acumulativas utilizando las capacidades modernas de MySQL 8.0. Sus listados son visuales y están provistos de explicaciones detalladas, lo que ayuda a comprender la esencia del problema, incluso para aquellos que no se sumergieron en él tan profundamente.



Una estrategia común para realizar actualizaciones usando funciones acumulativas en MySQL es usar variables y patrones personalizadosUPDATE [...] SET mycol = (@myvar := EXPRESSION(@myvar, mycol)).



Este patrón no funciona bien con el optimizador (lo que genera un comportamiento no determinista), por lo que decidieron abandonarlo. El resultado es una especie de vacío porque la lógica (relativamente) compleja ahora es más difícil de implementar (al menos con la misma simplicidad).



El artículo discutirá dos formas de implementarlo: usando funciones de ventana (enfoque canónico) y usando CTE recursivas (expresiones de tabla generales).



Requisitos y antecedentes



Aunque los CTE son bastante intuitivos, para aquellos que no estén muy familiarizados con ellos, recomiendo consultar mi publicación anterior sobre este tema .



Lo mismo es cierto para las funciones de ventana: comentaré las consultas / conceptos en detalle, pero una idea general aún no está de más. Hay una gran cantidad de libros y publicaciones dedicados a las funciones de las ventanas (por eso todavía no he escrito sobre ellos); sin embargo, en la mayoría de los ejemplos, los cálculos se realizan sobre resultados financieros o sobre indicadores demográficos. Sin embargo, en este artículo usaré un caso real.



En el lado del software, recomiendo usar MySQL 8.0.19 (pero no es obligatorio). Todas las expresiones deben ejecutarse en la misma consola para poder reutilizarse @venue_id.



En el mundo del software, existe un dilema arquitectónico bien conocido: ¿debería implementarse la lógica a nivel de aplicación o de base de datos? Si bien esta es una pregunta perfectamente válida, en nuestro caso asumo que la lógica debe permanecer en el nivel básico; la razón de esto puede ser, por ejemplo, los requisitos de velocidad (como fue el caso en nuestro caso).



Tarea



En esta tarea, asignamos asientos en una determinada sala (teatro).



Para fines comerciales, a cada ubicación se le debe asignar una denominada "agrupación", un número adicional que la representa.



Aquí está el algoritmo para determinar el valor de agrupación:



  1. comience en 0 y arriba a la izquierda;
  2. si hay un espacio vacío entre la actual y la anterior, o esta es una nueva fila, entonces sumamos 2 al valor anterior (si este no es el primer lugar absoluto), de lo contrario, aumentamos el valor en 1;
  3. asignar una agrupación a un lugar;
  4. vaya a un nuevo lugar en la misma fila o a la siguiente fila (si la anterior ha terminado) y repita desde el punto 2; Seguimos todo hasta que se acaben las plazas.


Algoritmo en pseudocódigo:



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


En la vida real, queremos que la configuración de la izquierda proporcione los valores que se muestran a la derecha:



 x→  0   1   2        0   1   2
y   ╭───┬───┬───╮    ╭───┬───┬───╮
↓ 0 │ x │ x │   │    │ 1 │ 2 │   │
    ├───┼───┼───┤    ├───┼───┼───┤
  1 │ x │   │ x │    │ 4 │   │ 6 │
    ├───┼───┼───┤    ├───┼───┼───┤
  2 │ x │   │   │    │ 8 │   │   │
    ╰───┴───┴───╯    ╰───┴───┴───╯


Formación



Deje que la mesa base tenga la siguiente estructura minimalista:



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


Realmente no necesitamos las columnas rowy number. Por otro lado, no queremos usar una tabla cuyos registros estén completamente contenidos en el índice (solo para estar más cerca de problemas reales).



Según el diagrama anterior, las coordenadas de cada ubicación son (y, x):



  • (0, 0), (0, 1)
  • (1, 0), (1, 2)
  • (20)


Tenga en cuenta que estamos usando y como la primera coordenada, ya que facilita el seguimiento de las filas.



Debe cargar una cantidad suficientemente grande de registros para evitar que el optimizador encuentre rutas cortas inesperadas. Por supuesto, usamos CTE recursivas:



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;


Algunas notas:



  1. Aquí, CTE se usa de una manera interesante (¡con suerte!): Cada bucle representa un ID de lugar, pero como queremos que se generen múltiples ubicaciones para cada lugar, hacemos una combinación cruzada con la tabla que contiene los datos de ubicación.
  2. El constructor de filas v8.0.19 ( VALUES ROW()...) se utiliza para representar una tabla ( unible ) sin crearla realmente.
  3. Genera valores aleatorios para filas y números como marcadores de posición.
  4. En aras de la simplicidad, no hicimos ninguna optimización (por ejemplo, los tipos de datos son más anchos de lo necesario; los índices se agregan antes de insertar registros, etc.).


Enfoque antiguo



El buen enfoque antiguo es bastante sencillo y directo:



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)


Bueno, eso fue fácil (¡pero no olvides las advertencias)!



Una pequeña digresión: en este caso, utilizo las propiedades de la aritmética booleana. Las siguientes expresiones son equivalentes:



SELECT seats.x > @x + 1 OR seats.y != @y `increment`;

SELECT IF (
  seats.x > @x + 1 OR seats.y != @y,
  1,
  0
) `increment`;


Algunos encuentran esto intuitivo, otros no; es cuestión de gustos. De ahora en adelante usaré una expresión más compacta.



Veamos el resultado:



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


¡Gran enfoque!



Por desgracia, tiene un inconveniente "menor": funciona muy bien excepto cuando no funciona ...



El hecho es que el optimizador de consultas no necesariamente realiza cálculos de izquierda a derecha, por lo que las operaciones de asignación (: =) pueden realizarse en el orden incorrecto conduciendo a un resultado incorrecto. Las personas a menudo enfrentan este problema después de actualizar MySQL.



En MySQL 8.0, esta funcionalidad está en desuso:



--    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)'.
-- [...]


Bueno, ¡arreglemos la situación!



Enfoque moderno n. ° 1: funciones de ventanas



La introducción de funciones de ventana ha sido un evento muy esperado en el mundo de MySQL.



En términos generales, la naturaleza "deslizante" de las funciones de ventana funciona bien con funciones acumulativas. Sin embargo, algunas funciones acumulativas complejas requieren los resultados de la última expresión: funcionalidad que las funciones de ventana no admiten porque operan en columnas.



Esto no significa que el problema no se pueda resolver: solo hay que repensarlo.



En nuestro caso, la tarea se puede dividir en dos partes. La agrupación para cada ubicación se puede considerar como la suma de dos valores:



  • el número de serie de cada lugar,
  • el valor acumulado de los incrementos de todos los lugares que preceden a este.


Aquellos familiarizados con las funciones de ventanas reconocerán los patrones típicos aquí.



El número de secuencia de cada asiento es una función incorporada:



ROW_NUMBER() OVER <window>


Pero con el valor acumulativo, todo es mucho más interesante ... Para calcularlo, realizamos dos acciones:



  • cuente el incremento para cada lugar y anótelo en la tabla (o CTE),
  • luego, para cada ubicación, sumamos los incrementos para esa ubicación usando la función de ventana.


Echemos un vistazo a 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 |
-- +-------+---+---+----------+


¡Excelente!



(Tenga en cuenta que estoy omitiendo la ACTUALIZACIÓN de ahora en adelante por simplicidad).



Analicemos la solicitud.



Lógica de alto nivel



El siguiente CTE (editado) :



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


… Calcula los incrementos para cada ubicación desde la anterior (más LAG()adelante). Funciona en todos los registros y en el que le precede y no es acumulable.



Ahora, para calcular los incrementos acumulativos, simplemente usaremos una función de ventana para calcular la suma hasta e incluyendo cada ubicación:



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


Función de ventana LAG ()



La función LAG, en su forma más simple ( LAG(x)), devuelve el valor anterior de una columna determinada. El inconveniente clásico con estas funciones es el manejo de las primeras entradas en la ventana. Como no hay ningún registro anterior, devuelven NULL. En el caso de LAG, puede especificar el valor deseado como tercer parámetro:



LAG(x, 1, x - 1) --    `x -1`
LAG(y, 1, y)     --    `y`


Al especificar los valores predeterminados, nos aseguramos de que el primer lugar en los límites de la ventana tenga la misma lógica que para el lugar que sigue al otro (x-1) y sin cambiar la fila (y).



Una solución alternativa es utilizar IFNULL, sin embargo, las expresiones son muy engorrosas:



--  ,  !
--
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)


El segundo parámetro LAG()es el número de posiciones para retroceder dentro de la ventana; 1 es el valor anterior (también es el predeterminado).



Aspectos técnicos



Ventanas con nombre



Nuestra consulta usa la misma ventana muchas veces. Las siguientes dos consultas son formalmente equivalentes:



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;


Sin embargo, el segundo puede conducir a un comportamiento no óptimo (que he encontrado, al menos en el pasado): el optimizador puede considerar las ventanas independientes y calcular cada una por separado. Por esta razón, le aconsejo que utilice siempre ventanas con nombre (al menos cuando se repiten).



PARTICIÓN POR declaración



Por lo general, las funciones de ventana se realizan en una partición. En nuestro caso, se verá 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 (PARTITION BY venue_id ORDER BY y, x); -- !


Dado que la ventana coincide con el conjunto completo de registros (que se filtra por la condición WHERE), no es necesario especificarlo (la partición).



Pero si esta consulta tuviera que ejecutarse en toda la tabla seats, entonces tendría que hacerse para que la ventana se restableciera para todos venue_id.



Clasificación



La solicitud ORDER BYse establece en el nivel de la ventana:



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)


En este caso, la clasificación de ventanas es independiente de SELECT. ¡Es muy importante! El comportamiento de esta solicitud:



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


… Indefinido. Pasemos al manual :



Las cadenas de resultados de la consulta se determinan a partir de la cláusula FROM después de que se hayan ejecutado las cláusulas WHERE, GROUP BY y HAVING, y la ejecución dentro de la ventana se produce antes de ORDER BY, LIMIT y SELECT DISTINCT.


Algunas consideraciones



En términos generales, para este tipo de problemas, tiene sentido calcular el cambio de estado para cada registro y luego sumarlos, en lugar de representar cada registro como una función del anterior.



Esta solución es más compleja que la funcionalidad a la que reemplaza, pero al mismo tiempo es confiable. Por desgracia, este enfoque no siempre es posible o fácil de implementar. Aquí es donde entran en juego las CTE recursivas.



Enfoque moderno n. ° 2: CTE recursivas



Este enfoque requiere un pequeño truco debido a las capacidades limitadas del CTE en MySQL. Por otro lado, es una solución directa única para todos, por lo que no requiere repensar un enfoque global.



Comencemos con una versión simplificada de la solicitud final:



-- `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! Esta consulta es (relativamente) simple, pero lo que es más importante, expresa la función de agrupación acumulativa de la manera más simple posible:



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 lógica es clara incluso para aquellos que no están demasiado familiarizados con CTE. La primera fila es el primer asiento del pasillo, en orden:



SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1


En la parte recursiva, iteramos:



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


Condicione WHEREjunto con los operadores ORDER BYy LIMITsimplemente encuentre el siguiente lugar, un lugar con el mismo venue_id, pero utilizado para las coordenadas de lshimi (x, y) en la secuencia (lugar_id, x, y).



¡La parte s.venue_iden la expresión de clasificación es muy importante! Nos permite utilizar un índice.



Operador SELECT:



  • realiza acumulación (calcula (p_)grouping),
  • proporciona valores para la posición actual ( s.id, s.venue_id, s.y, s.x) en el siguiente ciclo.


Elegimos FROM groupingscumplir con los requisitos de recursividad de CTE.



Lo interesante aquí es que usamos un CTE recursivo como iterador, obteniendo de una tabla groupingsen una subconsulta recursiva y uniéndolo seatspara encontrar los datos para su procesamiento posterior.



JOINes formalmente cruzado, pero LIMITsolo se devuelve un registro debido al operador .



Versión de trabajo



Desafortunadamente, la consulta anterior no funciona ya que ORDER BYactualmente no es compatible con subconsultas recursivas. Además, la semántica LIMITque se usa aquí difiere de la semántica típica que se aplica a una consulta externa :



LIMIT ahora es compatible [..] El efecto en el conjunto de datos resultante es el mismo que usar LIMIT con un SELECT externo




Sin embargo, este no es un problema tan grave. Echemos un vistazo a una versión funcional:



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 un poco incómodo usar una subconsulta, pero este enfoque funciona y el texto estándar es mínimo aquí, ya que de todos modos se requieren múltiples expresiones.



Aquí, en lugar de hacer el pedido y la limitación asociada con el sindicato groupingsy seats, lo hacemos dentro de la subconsulta y pasarlo a la consulta externa, que luego se selecciona sólo el registro de destino.



Reflexiones sobre el desempeño



Examinemos el plan de ejecución de la consulta usando 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)


El plan está en línea con las expectativas. En este caso, la base del plan óptimo radica en las búsquedas de índices:



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


... de suma importancia. El rendimiento se reducirá significativamente si escanea los índices (es decir, escanea linealmente los registros del índice en lugar de buscar los correctos).



Por lo tanto, para que esta estrategia funcione, los índices asociados deben estar en su lugar y el optimizador los debe usar de la manera más eficiente posible.



Si las restricciones se eliminan en el futuro, no será necesario utilizar una subconsulta, lo que simplificará enormemente la tarea para el optimizador.



Alternativa para planes subóptimos



En caso de que no se pueda determinar el plan óptimo, use una tabla temporal:



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;


Incluso si se pasan escaneos de índice en esta consulta, cuestan selected_seatsmucho , ya que la tabla es muy pequeña.



Conclusión



Estoy muy contento de que el flujo de trabajo eficiente pero defectuoso ahora pueda ser reemplazado por la funcionalidad bastante simple introducida en MySQL 8.0.



Mientras tanto, continúa el desarrollo de nuevas funciones para 8.0, lo que hace que una versión ya exitosa sea aún mejor.



¡Recursión exitosa!



PD del traductor



Lea también en nuestro blog:






All Articles