Usando funções de janela e CTEs no MySQL 8.0 para implementar um total cumulativo sem hacks





Aproximadamente. trad. : Neste artigo, o líder da equipe da empresa Ticketsolve do Reino Unido compartilha uma solução para seu problema muito específico, ao mesmo tempo em que demonstra abordagens gerais para a criação das chamadas funções de acumulação usando os recursos modernos do MySQL 8.0. Suas listagens são claras e com explicações detalhadas, o que ajuda a entender a essência do problema, mesmo para quem não se aprofundou nele.



Uma estratégia comum para realizar atualizações usando funções cumulativas no MySQL é usar variáveis ​​e padrões personalizadosUPDATE [...] SET mycol = (@myvar := EXPRESSION(@myvar, mycol)).



Esse padrão não funciona bem com o otimizador (levando a um comportamento não determinístico), então eles decidiram abandoná-lo. O resultado é uma espécie de vazio, uma vez que a lógica (relativamente) complexa é agora mais difícil de implementar (pelo menos com a mesma simplicidade).



O artigo irá discutir duas maneiras de implementá-lo: usando funções de janela (abordagem canônica) e usando CTEs recursivas (expressões de tabela gerais).



Requisitos e histórico



Embora os CTEs sejam bastante intuitivos, para aqueles que não estão muito familiarizados com eles, recomendo consultar meu post anterior sobre este tópico .



O mesmo é verdade para funções de janela: vou comentar sobre consultas / conceitos em detalhes, mas uma ideia geral ainda não faz mal. Um grande número de livros e publicações são dedicados a funções de janela (é por isso que ainda não escrevi sobre eles); entretanto, na maioria dos exemplos, os cálculos são realizados com base nos resultados financeiros ou nos indicadores demográficos. No entanto, neste artigo, usarei um caso real.



No lado do software, eu recomendo usar o MySQL 8.0.19 (mas não obrigatório). Todas as expressões devem ser executadas no mesmo console para serem reutilizadas @venue_id.



No mundo do software, existe um dilema arquitetônico bem conhecido: a lógica deve ser implementada no nível do aplicativo ou no nível do banco de dados? Embora essa seja uma questão perfeitamente válida, em nosso caso estou assumindo que a lógica deve permanecer no nível básico; a razão para isso pode ser, por exemplo, requisitos de velocidade (como foi o caso em nosso caso).



Tarefa



Nesta tarefa, alocamos assentos em uma certa sala (teatro).



Para fins comerciais, a cada local deve ser atribuído um denominado "agrupamento" - um número adicional que o representa.



Aqui está o algoritmo para determinar o valor de agrupamento:



  1. comece em 0 e superior esquerdo;
  2. se houver um espaço vazio entre a atual e a anterior, ou se esta for uma nova linha, então adicionamos 2 ao valor anterior (se esta não for a primeira posição absoluta), caso contrário, aumentamos o valor em 1;
  3. atribuir um agrupamento a um local;
  4. ir para um novo local na mesma linha ou para a próxima linha (se a anterior já tiver terminado) e repetir a partir do ponto 2; continuamos tudo até que as vagas acabem.


Algoritmo em 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


Na vida real, queremos que a configuração à esquerda forneça os valores mostrados à direita:



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


Treinamento



Deixe a tabela base ter a seguinte estrutura 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)
);


Não precisamos realmente das colunas rowe number. Por outro lado, não queremos usar uma tabela cujos registros estejam totalmente contidos no índice (apenas para estar mais perto de problemas reais).



Com base no diagrama acima, as coordenadas de cada local são (y, x):



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


Observe que estamos usando y como a primeira coordenada, pois torna mais fácil acompanhar as linhas.



Você deve carregar um número grande o suficiente de registros para evitar que o otimizador encontre caminhos curtos inesperados. Claro, usamos CTEs recursivos:



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;


Algumas notas:



  1. Aqui, CTE está sendo usado de uma forma interessante (espero!): Cada loop representa um local_id, mas como queremos que vários locais sejam gerados para cada local, fazemos uma junção cruzada com a tabela que contém os locais.
  2. O construtor de linha v8.0.19 ( VALUES ROW()...) é usado para representar uma tabela ( juntável ) sem realmente criá-la.
  3. Gera valores aleatórios para linha e número como marcadores de posição.
  4. Para simplificar, não fizemos nenhuma otimização (por exemplo, os tipos de dados são mais amplos do que o necessário; os índices são adicionados antes da inserção de registros, etc.).


Abordagem antiga



A boa e velha abordagem é bastante direta e direta:



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)


Bem, isso foi fácil (mas não se esqueça dos avisos)!



Uma pequena digressão: neste caso, uso as propriedades da aritmética booleana. As seguintes expressões são equivalentes:



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

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


Alguns acham isso intuitivo, outros não; é uma questão de gosto. De agora em diante, usarei uma expressão mais compacta.



Vamos ver o 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 |
-- +-------+------+------+----------+


Ótima abordagem!



Infelizmente, ele tem uma "pequena" desvantagem: funciona muito bem, exceto quando não funciona ...



O fato é que o otimizador de consulta não executa necessariamente cálculos da esquerda para a direita, portanto, as operações de atribuição (: =) podem ser realizadas na ordem errada levando ao resultado errado. As pessoas freqüentemente enfrentam esse problema após atualizar o MySQL.



No MySQL 8.0, essa funcionalidade está realmente obsoleta:



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


Bem, vamos consertar a situação!



Abordagem moderna nº 1: funções de janela



A introdução de funções de janela tem sido um evento altamente antecipado no mundo MySQL.



De modo geral, a natureza "deslizante" das funções de janela funciona bem com funções cumulativas. No entanto, algumas funções cumulativas complexas requerem os resultados da última expressão - funcionalidade que as funções de janela não suportam porque operam em colunas.



Isso não significa que o problema não possa ser resolvido, apenas precisa ser repensado.



No nosso caso, a tarefa pode ser dividida em duas partes. O agrupamento para cada local pode ser considerado a soma de dois valores:



  • o número de série de cada lugar,
  • o valor cumulativo dos incrementos de todos os lugares anteriores a este.


Aqueles familiarizados com as funções de janelamento reconhecerão os padrões típicos aqui.



O número de sequência de cada assento é uma função embutida:



ROW_NUMBER() OVER <window>


Mas com o valor cumulativo tudo fica muito mais interessante ... Para calculá-lo, realizamos duas ações:



  • conte o incremento para cada lugar e anote-o na mesa (ou CTE),
  • então, para cada local, somamos os incrementos para aquele local usando a função de janela.


Vamos dar uma olhada no 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 |
-- +-------+---+---+----------+


Ótimo!



(Observe que estou omitindo a ATUALIZAÇÃO de agora em diante por uma questão de simplicidade.)



Vamos analisar o pedido.



Lógica de alto nível



O seguinte 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 os incrementos para cada local a partir do anterior (mais LAG()adiante). Ele funciona em todos os registros e no que o precede, e não é cumulativo.



Agora, para calcular os incrementos cumulativos, vamos simplesmente usar uma função de janela para calcular a soma até e incluindo cada local:



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


Função de janela LAG ()



A função LAG, em sua forma mais simples ( LAG(x)), retorna o valor anterior de uma determinada coluna. O inconveniente clássico com tais funções é lidar com as primeiras entradas na janela. Como não há registro anterior, eles retornam NULL. No caso de LAG, você pode especificar o valor desejado como o terceiro parâmetro:



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


Ao especificar os valores padrão, garantimos que o primeiro lugar nos limites da janela terá a mesma lógica do lugar seguinte (x-1) e sem alterar a linha (y).



Uma solução alternativa é usar IFNULL, no entanto, as expressões são muito complicadas:



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


O segundo parâmetro LAG()é o número de posições para mover para trás na janela; 1 é o valor anterior (também é o padrão).



Aspectos tecnicos



Janelas nomeadas



Nossa consulta usa a mesma janela muitas vezes. As duas consultas a seguir são 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;


No entanto, o segundo pode levar a um comportamento não ideal (que encontrei - pelo menos no passado): o otimizador pode considerar as janelas independentes e calcular cada uma separadamente. Por esse motivo, aconselho sempre a usar janelas nomeadas (pelo menos quando se repetem).



Declaração PARTITION BY



Normalmente, as funções de janelamento são realizadas em uma partição. No nosso caso, será assim:



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


Como a janela corresponde ao conjunto completo de registros (que é filtrado pela condição WHERE), não precisamos especificá-lo (a partição).



Mas se essa consulta tivesse que ser executada em toda a tabela seats, isso teria que ser feito para que a janela fosse redefinida para todos venue_id.



Ordenação



A solicitação ORDER BYé definida no nível da janela:



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)


Nesse caso, a classificação da janela é separada de SELECT. É muito importante! O comportamento desta solicitação:



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. Vamos voltar ao manual :



As cadeias de caracteres de resultado da consulta são determinadas a partir da cláusula FROM após as cláusulas WHERE, GROUP BY e HAVING terem sido executadas, e a execução da janela ocorre antes de ORDER BY, LIMIT e SELECT DISTINCT.


Algumas considerações



Em termos gerais, para esse tipo de problema, faz sentido calcular a mudança de estado de cada registro e, em seguida, somá-los - em vez de representar cada registro em função do anterior.



Esta solução é mais complexa do que a funcionalidade que substitui, mas ao mesmo tempo é confiável. Infelizmente, essa abordagem nem sempre é possível ou fácil de implementar. É aqui que os CTEs recursivos entram em jogo.



Abordagem moderna 2: CTEs recursivos



Essa abordagem requer um pouco de truque devido aos recursos limitados do CTE no MySQL. Por outro lado, é uma solução única e direta, portanto, não requer nenhum repensar de uma abordagem global.



Vamos começar com uma versão simplificada da solicitação 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 é (relativamente) simples, mas o mais importante, ela expressa a função de agrupamento cumulativo da maneira mais simples possível:



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


A lógica é clara mesmo para quem não conhece muito o CTE. A primeira fila é o primeiro assento no corredor, na ordem:



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


Na 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 com os operadores ORDER BYe LIMITsimplesmente encontre o próximo local, um local com o mesmo venue_id, mas usado para coordenadas lshimi (x, y) na sequência (local_id, x, y).



A parte s.venue_idda expressão de classificação é muito importante! Isso nos permite usar um índice.



Operador SELECT:



  • realiza acumulação (calcula (p_)grouping),
  • fornece valores para a posição actual ( s.id, s.venue_id, s.y, s.x) no ciclo seguinte.


Optamos FROM groupingspor cumprir os requisitos de recursividade CTE.



O que é interessante aqui é que usamos um CTE recursivo como um iterador, obtendo de uma tabela groupingsem uma subconsulta recursiva e concatenando-o seatspara encontrar os dados para processamento posterior.



JOINé formalmente cruzado, mas LIMITapenas um registro é retornado por causa do operador .



Versão de trabalho



Infelizmente, a consulta acima não funciona, pois ORDER BYatualmente não é compatível com subconsultas recursivas. Além disso, a semântica LIMITconforme usada aqui difere da semântica típica que se aplica a uma consulta externa :



LIMIT é agora suportado [..] O efeito no conjunto de dados resultante é o mesmo que usar LIMIT com um SELECT externo




No entanto, este não é um problema tão sério. Vamos dar uma olhada em uma versão 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 |
-- +-------+------+------+------------+


É um pouco desconfortável usar uma subconsulta, mas essa abordagem funciona e o clichê é mínimo aqui, já que várias expressões são necessárias de qualquer maneira.



Aqui, em vez de fazer a ordenação e limitação associada à união groupingse seats, fazemos isso dentro da subconsulta e passamos para a consulta externa, que então seleciona apenas o registro de destino.



Reflexões sobre desempenho



Vamos examinar o plano de execução da 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)


O plano está de acordo com as expectativas. Nesse caso, a base do plano ideal está nas pesquisas de índice:



-> 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 importância. O desempenho cairá significativamente se você verificar os índices (ou seja, verificar linearmente os registros de índice em vez de procurar os corretos).



Portanto, para que essa estratégia funcione, os índices associados devem estar em vigor e ser usados ​​da forma mais eficiente possível pelo otimizador.



Se as restrições forem suspensas no futuro, não haverá necessidade de usar uma subconsulta, o que simplificará muito a tarefa para o otimizador.



Alternativa para planos abaixo do ideal



Caso o plano ideal não possa ser determinado, use uma tabela temporária:



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;


Mesmo que haja varreduras de índice nesta consulta, elas custam selected_seatsmuito , pois a tabela é muito pequena.



Conclusão



Estou muito satisfeito que o fluxo de trabalho eficiente, mas falho, agora pode ser substituído pela funcionalidade bastante simples introduzida no MySQL 8.0.



Enquanto isso, o desenvolvimento de novos recursos para o 8.0 continua, o que torna um lançamento já bem-sucedido ainda melhor.



Recursão bem-sucedida!



PS do tradutor



Leia também em nosso blog:






All Articles