Wie erhalte ich den aktuellen und den nächsthöheren Wert in einer Auswahl?


18

Ich habe eine InnoDB-Tabelle 'idtimes' (MySQL 5.0.22-log) mit Spalten

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

mit einem zusammengesetzten eindeutigen Schlüssel

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

Es kann also mehrere Zeitstempel pro ID und mehrere IDs pro Zeitstempel geben.

Ich versuche, eine Abfrage einzurichten, bei der ich für jeden Eintrag alle Einträge sowie die nächsthöhere Zeit erhalte, falls vorhanden. Daher sollte diese Abfrage beispielsweise Folgendes zurückgeben:

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

Im Moment bin ich soweit:

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;

aber das gibt natürlich alle Zeilen mit r.time> l.time zurück und nicht nur die erste ...

Ich denke, ich brauche eine Unterauswahl wie

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;

aber ich weiß nicht, wie ich mich auf die aktuelle Zeit beziehen soll (ich weiß, dass das oben Gesagte kein gültiges SQL ist).

Wie mache ich das mit einer einzelnen Abfrage (und ich würde es vorziehen, keine @ -Variablen zu verwenden, die davon abhängen, zeilenweise durch die Tabelle zu springen und den letzten Wert zu speichern)?

Antworten:


20

Ein JOIN ist eine Sache, die Sie vielleicht brauchen.

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

Ich nehme an, die äußere Verknüpfung ist absichtlich und Sie möchten Nullen erhalten. Dazu später mehr.

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

Sie wollen nur die r. Zeile mit der niedrigsten (MIN) Zeit, die höher ist als die l.time. Dies ist der Ort, an dem Sie sich unterwerfen müssen.

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

Nun zu den Nullen. Wenn "es gibt keine nächsthöhere Zeit", wird SELECT MIN () mit null (oder schlechter) bewertet, und dieser Wert wird niemals mit irgendetwas gleichgesetzt, sodass Ihre WHERE-Klausel niemals erfüllt wird, und die "höchste Zeit". konnte nie für jede ID in der Ergebnismenge angezeigt werden.

Sie lösen es, indem Sie Ihren JOIN entfernen und die skalare Unterabfrage in die SELECT-Liste verschieben:

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 

4

Ich vermeide es immer, Unterabfragen entweder im SELECTBlock oder im FROMBlock zu verwenden, da dies den Code "schmutziger" und manchmal weniger effizient macht.

Ich denke, eine elegantere Art dies zu tun ist:

1. Ermitteln Sie die Zeiten, die größer als die Zeit der Zeile sind

Sie können dies mit einer Tabelle für JOINZwischen- IDs für sich selbst tun , wobei der Join auf dieselbe ID und auf Zeiten beschränkt wird, die größer als die Zeit der aktuellen Zeile sind.

Sie sollten verwenden LEFT JOINReihen zu vermeiden , außer , wo es keine gibt mal größer als die der aktuellen Zeile.

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

Wie Sie bereits erwähnt haben, besteht das Problem darin, dass Sie mehrere Zeilen haben, in denen next_time größer als time ist .

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

2. Suchen Sie die Zeilen, in denen " Größere_Zeit" nicht nur größer ist, sondern " Nächste_Zeit"

Der beste Weg, um all diese nutzlosen Zeilen zu filtern, besteht darin, herauszufinden, ob zwischen der Zeit (größer als) und der Zeit (kleiner als) für diese ID Zeiten liegen .

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, wir haben noch ein falsches 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  |
+-----+------------+--------------+---------------+

Filtern Sie einfach die Zeilen, in denen dieses Ereignis auftritt, und fügen Sie die folgende WHEREEinschränkung hinzu

WHERE
    i3.time IS NULL

Voilà, wir haben was wir brauchen!

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

Ich hoffe, dass Sie nach 4 Jahren noch eine Antwort brauchen!


Das ist schlau. Ich bin mir nicht sicher, ob es einfacher zu verstehen ist. Ich denke, wenn wir die is nullund die Verknüpfung zu i3 durch ersetzen where not exists (select 1 from itimes i3 where [same clause])würden, würde der Code besser das widerspiegeln, was wir ausdrücken möchten.
Andrew Spencer

Danke, du hast meinen (nächsten) Tag gerettet!
Jakob

2

Bevor ich die Lösung vorstelle, sollte ich beachten, dass sie nicht hübsch ist. Es wäre viel einfacher, wenn Sie eine AUTO_INCREMENTSpalte auf Ihrem Tisch hätten (oder?)

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

Erläuterung:

  • Gleicher Join wie bei dir: Verbinde zwei Tische, der rechte bekommt nur die höheren Zeiten
  • GROUP BY beide Spalten der linken Tabelle: Dies stellt sicher, dass wir alle (id, time)Kombinationen erhalten (die auch als eindeutig bekannt sind).
  • Holen Sie sich für jede (l.id, l.time)die erste, r.time die größer ist als l.time. Dies geschieht mit der ersten Bestellung des r.times via GROUP_CONCAT(r.time ORDER BY r.time), dem ersten Token via SUBSTRING_INDEX.

Viel Glück und erwarten Sie keine gute Leistung, wenn diese Tabelle groß ist.


2

Sie können auch bekommen, was Sie wollen, min()und GROUP BYohne innere Auswahl:

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;

Ich würde fast eine große Summe Geld wetten, dass der Optimierer dies sowieso in das Gleiche wie Erwin Smouts Antwort umwandelt, und es ist fraglich, ob es klarer ist, aber da ist es der Vollständigkeit halber ...


1
Für was auch immer, SSMS & SQLServer 2016 mochte Ihre Abfrage viel mehr als Erwins (2s Laufzeit versus 24s Laufzeit auf ~ 24k Ergebnismenge)
Nathan Lafferty

Andrew scheint, als hättest du die Wette verloren :-)
Erwin Smout

Interessant, da eine Unterabfrage, die über eine der PK-Spalten mit der äußeren Abfragetabelle verknüpft wird, in der Regel einer Gruppe von entspricht. Ich frage mich, ob andere Datenbanken es besser optimieren würden. (Ich weiß sehr wenig über Datenbankoptimierer BTW; nur neugierig zu sein.)
Andrew Spencer
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.