Como obtenho o valor atual e o próximo maior em uma seleção?

18

Eu tenho uma tabela do InnoDB 'idtimes' (MySQL 5.0.22-log) com colunas

`id` int(11) NOT NULL,
`time` int(20) NOT NULL, [...]

com uma chave única composta

UNIQUE KEY `id_time` (`id`,`time`)

portanto, pode haver vários registros de data e hora por ID e vários IDs por registro de data e hora.

Estou tentando configurar uma consulta na qual obtenho todas as entradas e o próximo maior tempo para cada entrada, se existir, portanto, ele deve retornar, por exemplo:

+-----+------------+------------+
| id  | time       | nexttime   |
+-----+------------+------------+
| 155 | 1300000000 | 1311111111 |
| 155 | 1311111111 | 1322222222 |
| 155 | 1322222222 |       NULL |
| 156 | 1312345678 | 1318765432 |
| 156 | 1318765432 |       NULL |
+-----+------------+------------+

Agora eu estou tão longe:

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id
    WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

mas é claro que isso retorna todas as linhas com r.time> l.time e não apenas o primeiro ...

Acho que vou precisar de uma subseleção como

SELECT outer.id, outer.time, 
    (SELECT time FROM idtimes WHERE id = outer.id AND time > outer.time 
        ORDER BY time ASC LIMIT 1)
    FROM idtimes AS outer ORDER BY outer.id ASC, outer.time ASC;

mas não sei como me referir ao horário atual (sei que o acima não é SQL válido).

Como faço isso com uma única consulta (e eu preferiria não usar @ variáveis ​​que dependem de percorrer a tabela uma linha por vez e lembrar o último valor)?

Martin Hennings
fonte

Respostas:

20

Fazer um JOIN é uma coisa que você pode precisar.

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id

Suponho que a junção externa seja deliberada e você deseja obter nulos. Mais sobre isso mais tarde.

WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

Você quer apenas o r. linha que possui o menor tempo (MIN) maior que o tempo l.time. Esse é o lugar onde você precisa subconsultar.

WHERE r.time = (SELECT MIN(time) FROM idtimes r2 where r2.id = l.id AND r2.time > l.time)

Agora para os nulos. Se "não houver um próximo tempo mais alto", o SELECT MIN () será avaliado como nulo (ou pior), e isso nunca será comparado a nada, portanto sua cláusula WHERE nunca será satisfeita e o "tempo mais alto" para cada ID, nunca pôde aparecer no conjunto de resultados.

Você o resolve eliminando JOIN e movendo a subconsulta escalar para a lista SELECT:

SELECT id, time, 
    (SELECT MIN(time) FROM idtimes sub 
        WHERE sub.id = main.id AND sub.time > main.time) as nxttime
  FROM idtimes AS main 
Erwin Smout
fonte
4

Eu sempre evito usar subconsultas no SELECTbloco ou no FROMbloco, porque torna o código "mais sujo" e às vezes menos eficiente.

Eu acho que uma maneira mais elegante de fazer isso é:

1. Encontre os tempos maiores que o tempo da linha

Você pode fazer isso com uma tabela JOINentre idtimes consigo, restringindo a junção ao mesmo id e a tempos maiores que o tempo da linha atual.

Você deve LEFT JOINevitar para excluir linhas onde não há tempos maiores que o da linha atual.

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS greater_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time

O problema, como você mencionou, é que você tem várias linhas em que next_time é maior que o tempo .

+-----+------------+--------------+
| id  | time       | greater_time |
+-----+------------+--------------+
| 155 | 1300000000 | 1311111111   |
| 155 | 1300000000 | 1322222222   |
| 155 | 1311111111 | 1322222222   |
| 155 | 1322222222 |       NULL   |
| 156 | 1312345678 | 1318765432   |
| 156 | 1318765432 |       NULL   |
+-----+------------+--------------+

2. Encontre as linhas em que Greater_time não é apenas maior, mas next_time

A melhor maneira de filtrar todas estas linhas inúteis é descobrir se há tempos entre tempo (maior que) e greater_time (menor do que) para este id .

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS next_time,
    i3.time AS intrudor_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time
    LEFT JOIN idtimes AS i3 ON i2.id = i3.id AND i3.time > i1.time AND i3.time < i2.time

ops, ainda temos um false_time next !

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1300000000 | 1322222222   |    1311111111 |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Apenas filtre as linhas onde esse evento acontece, adicionando a WHERErestrição abaixo

WHERE
    i3.time IS NULL

Voilà, nós temos o que precisamos!

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Espero que você ainda precise de uma resposta após 4 anos!

luisfsns
fonte
Isso é esperto. Não tenho certeza se é mais fácil entender. Penso que se substituíssemos o is nulle a junção ao i3 por where not exists (select 1 from itimes i3 where [same clause]), o código refletiria mais de perto o que queremos expressar.
Andrew Spencer
thx cara você salvou meu (próximo) dia!
Jakob
2

Antes de apresentar a solução, devo observar que não é bonito. Seria muito mais fácil se você tivesse alguma AUTO_INCREMENTcoluna na sua mesa (não é?)

SELECT 
  l.id, l.time, 
  SUBSTRING_INDEX(GROUP_CONCAT(r.time ORDER BY r.time), ',', 1)
FROM 
  idtimes AS l 
  LEFT JOIN idtimes AS r ON (l.id = r.id)
WHERE 
  l.time < r.time
GROUP BY
  l.id, l.time

Explicação:

  • A mesma junção que a sua: junte-se a duas mesas, a certa apenas obtém os tempos mais altos
  • GRUPO POR ambas as colunas da tabela esquerda: isso garante que obtemos todas as (id, time)combinações (que também são conhecidas por serem únicas).
  • Para cada um (l.id, l.time), obtenha o primeiro r.time maior que l.time. Isso acontece com a primeira ordem do r.times via GROUP_CONCAT(r.time ORDER BY r.time), o fatiamento do primeiro token via SUBSTRING_INDEX.

Boa sorte e não espere um bom desempenho se esta tabela for grande.

Shlomi Noach
fonte
2

Você também pode obter o que deseja de um min()e GROUP BYsem seleção interna:

SELECT l.id, l.time, min(r.time) 
FROM idtimes l 
LEFT JOIN idtimes r on (r.id = l.id and r.time > l.time)
GROUP BY l.id, l.time;

Eu quase apostaria uma grande quantia em dinheiro que o otimizador transforma isso na mesma coisa que a resposta de Erwin Smout de qualquer maneira, e é discutível se é mais claro, mas existe a perfeição ...

Andrew Spencer
fonte
11
Para que o seu valor, SSMS e SQLServer 2016 gostado sua consulta muito mais do que Erwin do (2s tempo de execução contra 24s tempo de execução em ~ 24k conjunto de resultados)
Nathan Lafferty
Andrew parece que você perdeu a aposta :-)
Erwin Smout
Interessante, porque deve ser um caso geral que uma subconsulta que se junte à tabela de consulta externa por uma das colunas PK seja igual a um grupo de. Gostaria de saber se outros bancos de dados o otimizariam melhor. (Eu sei muito pouco sobre otimizadores de banco de dados BTW, basta ser curioso.)
Andrew Spencer