web-dev-qa-db-fra.com

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

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

18
Martin Hennings

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 lui-même 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 
20
Erwin Smout

J'évite toujours d'utiliser des sous-requêtes soit dans le bloc SELECT soit dans le bloc FROM, 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 JOIN entre idtimes avec elle-même, contraignant la jointure à la même id et jusqu'à fois supérieur à fois de la ligne actuelle.

Tu devrais utiliser LEFT JOIN pour éviter d'exclure des lignes où il n'y a pas fois 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 à time .

+-----+------------+--------------+
| 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ù plus_heure est non seulement plus grande mais next_time

La meilleure façon de filtrer toutes ces lignes inutiles est de savoir s'il y a fois entre fois (supérieur à) et plus grand temps (inférieur à) pour cet 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, nous avons encore un false 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 contrainte WHERE 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!

4
luisfsns

Vous pouvez également obtenir ce que vous voulez à partir d'une min() et GROUP BY sans 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 voudrais presque parier 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 il est discutable que ce soit plus clair, mais là c'est pour être complet ...

2
Andrew Spencer

Avant de présenter la solution, je dois noter qu'elle n'est pas jolie. Ce serait beaucoup plus facile si vous aviez une colonne AUTO_INCREMENT 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
  • GROUP BY les deux colonnes du tableau de gauche: cela garantit que nous obtenons toutes les combinaisons (id, time) (Qui sont également connues pour être uniques).
  • Pour chaque (l.id, l.time), Obtenez le premierr.time Qui est supérieur à l.time. Cela se produit lors de la première commande des r.time S via GROUP_CONCAT(r.time ORDER BY r.time), puis en coupant le premier jeton via SUBSTRING_INDEX.

Bonne chance et ne vous attendez pas à de bonnes performances si cette table est grande.

2
Shlomi Noach