Comment puis-je obtenir la valeur actuelle et ensuite supérieure en une seule sélection?

18

J'ai une table InnoDB 'idtimes' (MySQL 5.0.22-log) avec des colonnes

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

avec une clé unique composée

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

il peut donc y avoir plusieurs horodatages par identifiant et plusieurs identifiants par horodatage.

J'essaye de mettre en place une requête où j'obtiens toutes les entrées plus la prochaine fois plus grande pour chaque entrée, si elle existe, donc elle devrait retourner par exemple:

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

En ce moment je suis si loin:

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;

mais bien sûr, cela renvoie toutes les lignes avec r.time> l.time et pas seulement la première ...

Je suppose que j'aurai besoin d'une sous-sélection comme

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;

mais je ne sais pas comment me référer à l'heure actuelle (je sais que ce qui précède n'est pas du SQL valide).

Comment puis-je faire cela avec une seule requête (et je préférerais ne pas utiliser des variables @ qui dépendent de la progression dans le tableau une ligne à la fois et de la mémorisation de la dernière valeur)?

Martin Hennings
la source

Réponses:

20

Faire un JOIN est une chose dont vous pourriez avoir besoin.

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

Je suppose que la jointure externe est délibérée et que vous voulez obtenir des valeurs nulles. Plus sur cela plus tard.

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

Vous ne voulez que le r. ligne dont le temps (MIN) le plus bas est supérieur au temps l. C'est l'endroit où vous avez besoin de sous-interroger.

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

Passons maintenant aux null. Si "il n'y a pas de temps supérieur suivant", alors SELECT MIN () sera évalué à null (ou pire), et cela ne se compare jamais à rien, donc votre clause WHERE ne sera jamais satisfaite, et le "temps le plus élevé" pour chaque ID, n'a jamais pu apparaître dans le jeu de résultats.

Vous le résolvez en éliminant votre JOIN et en déplaçant la sous-requête scalaire dans la liste 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
la source
4

J'évite toujours d'utiliser des sous-requêtes en SELECTbloc ou en FROMbloc, car cela rend le code "plus sale" et parfois moins efficace.

Je pense qu'une façon plus élégante de le faire est de:

1. Trouvez les temps supérieurs au temps de la ligne

Vous pouvez le faire avec une table JOINentre idtimes avec elle-même, contraignant la jointure au même id et à des temps supérieurs à l' heure de la ligne actuelle.

Vous devez utiliser LEFT JOINpour éviter d'exclure des lignes où il n'y a pas de temps supérieur à celui de la ligne actuelle.

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

Le problème, comme vous l'avez mentionné, est que vous avez plusieurs lignes où next_time est supérieur au temps .

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

2. Trouvez les lignes où la plus grande_heure est non seulement plus grande mais la prochaine_heure

La meilleure façon de filtrer toutes ces lignes inutiles est de savoir s'il y a des temps entre le temps (supérieur à) et le temps supérieur (inférieur à) pour cet identifiant .

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, nous avons encore un faux 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  |
+-----+------------+--------------+---------------+

Il suffit de filtrer les lignes où cet événement se produit, en ajoutant la WHEREcontrainte ci-dessous

WHERE
    i3.time IS NULL

Voilà, nous avons ce qu'il nous faut!

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

J'espère que vous avez toujours besoin d'une réponse après 4 ans!

luisfsns
la source
C'est malin. Je ne suis pas sûr que ce soit plus facile à comprendre. Je pense que si nous remplacions le is nullet la jointure avec i3 where not exists (select 1 from itimes i3 where [same clause]), le code refléterait plus précisément ce que nous voulons exprimer.
Andrew Spencer
thx mec vous avez sauvé mon (lendemain) jour!
Jakob
2

Avant de présenter la solution, je dois noter qu'elle n'est pas jolie. Ce serait beaucoup plus facile si vous aviez une AUTO_INCREMENTcolonne sur votre table (pensez-vous?)

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

Explication:

  • Même jointure que la vôtre: rejoignez deux tables, la bonne n'obtient que les temps les plus élevés
  • GROUPE PAR les deux colonnes du tableau de gauche: cela garantit que nous obtenons toutes les (id, time)combinaisons (qui sont également connues pour être uniques).
  • Pour chacun (l.id, l.time), obtenez le premier r.time qui est supérieur à l.time. Cela se produit lors de la première commande du r.times via GROUP_CONCAT(r.time ORDER BY r.time), en coupant le premier jeton via SUBSTRING_INDEX.

Bonne chance et ne vous attendez pas à de bonnes performances si ce tableau est volumineux.

Shlomi Noach
la source
2

Vous pouvez également obtenir ce que vous voulez d'un min()et GROUP BYsans sélection interne:

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;

Je parierais presque une grosse somme d'argent que l'optimiseur transforme cela en la même chose que la réponse d'Erwin Smout de toute façon, et on peut se demander si c'est plus clair, mais là c'est pour être complet ...

Andrew Spencer
la source
1
Pour ce que ça vaut, SSMS & SQLServer 2016 ont beaucoup plus aimé votre requête que celle d'Erwin (temps d'exécution de 2 s contre temps d'exécution de 24 s sur un ensemble de résultats ~ 24 k)
Nathan Lafferty
Andrew semble avoir perdu le pari :-)
Erwin Smout
Intéressant, car il devrait être général qu'une sous-requête qui rejoint la table de requête externe par l'une des colonnes PK soit identique à un groupe par. Je me demande si d'autres bases de données pourraient mieux l'optimiser. (Je sais très peu de choses sur les optimiseurs de base de données BTW; je suis juste curieux.)
Andrew Spencer