¿Cómo obtengo el valor actual y el siguiente mayor en una selección?

18

Tengo una tabla InnoDB 'idtimes' (MySQL 5.0.22-log) con columnas

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

con una clave compuesta única

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

por lo que puede haber múltiples marcas de tiempo por ID y múltiples ID por marca de tiempo.

Estoy tratando de configurar una consulta donde obtengo todas las entradas más el siguiente tiempo mayor para cada entrada, si existe, por lo que debería devolver, por ejemplo:

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

En este momento estoy tan lejos:

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;

pero, por supuesto, esto devuelve todas las filas con r.time> l.time y no solo la primera ...

Creo que necesitaré una subselección 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;

pero no sé cómo referirme a la hora actual (sé que lo anterior no es SQL válido).

¿Cómo hago esto con una sola consulta (y preferiría no usar @variables que dependen de recorrer la tabla una fila a la vez y recordar el último valor)?

Martin Hennings
fuente

Respuestas:

20

Hacer una UNIÓN es una cosa que podría necesitar.

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

Supongo que la unión externa es deliberada, y desea obtener nulos. Más sobre eso más tarde.

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

Solo quieres la r. fila que tiene el tiempo más bajo (MIN) que es más alto que el tiempo l. Ese es el lugar donde necesita subconsulta.

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

Ahora a los nulos. Si "no hay tiempo siguiente más alto", entonces SELECT MIN () se evaluará como nulo (o peor), y eso nunca se compara igual a nada, por lo que su cláusula WHERE nunca se cumplirá, y el "tiempo más alto" para cada ID, nunca podría aparecer en el conjunto de resultados.

Lo resuelve eliminando su JOIN y moviendo la subconsulta escalar a la 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
fuente
4

Siempre evito usar subconsultas en SELECTbloque o en FROMbloque, porque hace que el código sea "más sucio" y, a veces, menos eficiente.

Creo que una forma más elegante de hacerlo es:

1. Encuentra los tiempos mayores que el tiempo de la fila

Puede hacer esto con una tabla de idtimesJOIN entre sí, restringiendo la unión a la misma id y a tiempos mayores que el tiempo de la fila actual.

Debe usar LEFT JOINpara evitar excluir filas donde no haya tiempos mayores que el de la fila actual.

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

El problema, como mencionó, es que tiene varias filas donde next_time es mayor que time .

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

2. Busque las filas donde Greater_time no solo es mayor sino next_time

La mejor manera de filtrar todas estas filas inútiles es averiguar si hay tiempos entre el tiempo (mayor que) y mayor_tiempo (menor que) para esta identificación .

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, todavía tenemos un falso next_time !

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

Simplemente filtre las filas donde ocurre este evento, agregando la WHERErestricción a continuación

WHERE
    i3.time IS NULL

¡Voilà, tenemos lo que necesitamos!

+-----+------------+--------------+---------------+
| 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 aún necesites una respuesta después de 4 años!

luisfsns
fuente
Eso es inteligente. Sin embargo, no estoy seguro de que sea más fácil de entender. Creo que si reemplazamos is nully unimos a i3 con where not exists (select 1 from itimes i3 where [same clause]), entonces el código reflejaría más de cerca lo que queremos expresar.
Andrew Spencer
Gracias amigo, salvaste mi (próximo) día!
Jakob
2

Antes de presentar la solución, debo señalar que no es bonita. Sería mucho más fácil si tuviera alguna AUTO_INCREMENTcolumna en su tabla (¿verdad?)

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

Explicación:

  • La misma combinación que la tuya: une dos tablas, la correcta solo obtiene los tiempos más altos
  • GROUP BY ambas columnas de la tabla izquierda: esto asegura que obtengamos todas las (id, time)combinaciones (que también se sabe que son únicas).
  • Para cada uno (l.id, l.time), obtenga el primero, r.time que es mayor que l.time. Esto sucede con el primer pedido de la r.timevía GROUP_CONCAT(r.time ORDER BY r.time), cortando el primer token vía SUBSTRING_INDEX.

Buena suerte y no esperes un buen rendimiento si esta tabla es grande.

Shlomi Noach
fuente
2

También se puede conseguir lo que quiere de una min()y GROUP BYsin la SELECT 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;

Yo casi apuesto a una gran suma de dinero que el optimizador convierte esto en lo mismo que la respuesta de Erwin Smout de todos modos, y es discutible si es más claro, pero no lo es para redondear ...

Andrew Spencer
fuente
1
Por lo que vale, a SSMS y SQLServer 2016 le gustó su consulta mucho más que a Erwin (tiempo de ejecución 2s versus tiempo de ejecución 24s en ~ 24k conjunto de resultados)
Nathan Lafferty
Parece que Andrew perdió la apuesta :-)
Erwin Smout
Interesante, porque debería ser un caso general que una subconsulta que se une a la tabla de consulta externa por una de las columnas PK es lo mismo que un grupo por. Me pregunto si alguna otra base de datos lo optimizaría mejor. (Sé muy poco sobre optimizadores de bases de datos, por cierto; solo por curiosidad)
Andrew Spencer