Wählen Sie die Zeile mit dem letzten Datum pro Benutzer aus


125

Ich habe eine Tabelle ("lms_attendance") der Ein- und Auscheckzeiten der Benutzer, die folgendermaßen aussieht:

id  user    time    io (enum)
1   9   1370931202  out
2   9   1370931664  out
3   6   1370932128  out
4   12  1370932128  out
5   12  1370933037  in

Ich versuche, eine Ansicht dieser Tabelle zu erstellen, die nur den neuesten Datensatz pro Benutzer-ID ausgibt, während ich den Wert "in" oder "out" erhalte.

id  user    time    io
2   9   1370931664  out
3   6   1370932128  out
5   12  1370933037  in

Ich bin bisher ziemlich nah dran, aber mir wurde klar, dass Ansichten keine Unterwahlen akzeptieren, was es sehr viel schwieriger macht. Die nächste Anfrage, die ich bekam, war:

select 
    `lms_attendance`.`id` AS `id`,
    `lms_attendance`.`user` AS `user`,
    max(`lms_attendance`.`time`) AS `time`,
    `lms_attendance`.`io` AS `io` 
from `lms_attendance` 
group by 
    `lms_attendance`.`user`, 
    `lms_attendance`.`io`

Aber was ich bekomme ist:

id  user    time    io
3   6   1370932128  out
1   9   1370931664  out
5   12  1370933037  in
4   12  1370932128  out

Welches ist nah, aber nicht perfekt. Ich weiß, dass die letzte Gruppe von nicht vorhanden sein sollte, aber ohne sie gibt sie die letzte Zeit zurück, aber nicht mit ihrem relativen E / A-Wert.

Irgendwelche Ideen? Vielen Dank!



Gehen Sie zurück zum Handbuch. Sie werden sehen, dass es Lösungen für dieses Problem sowohl mit als auch ohne (korrelierte und nicht korrelierte) Unterabfragen bietet.
Erdbeere

@Barmar, technisch gesehen, ist dies, wie ich in meiner Antwort betont habe, ein Duplikat aller 700 Fragen mit dem Tag " Größte n pro Gruppe" .
TMS

@Prodikl, was ist 'io (enum)'?
Monica Heddneck

Ich hatte eine Spalte namens "IO", die für "in or out" steht. Es war ein Aufzählungstyp mit möglichen Werten "in" oder "out". Dies wurde verwendet, um zu verfolgen, wann Personen in eine Klasse ein- und auscheckten.
Keith

Antworten:


199

Abfrage:

SQLFIDDLEExample

SELECT t1.*
FROM lms_attendance t1
WHERE t1.time = (SELECT MAX(t2.time)
                 FROM lms_attendance t2
                 WHERE t2.user = t1.user)

Ergebnis:

| ID | USER |       TIME |  IO |
--------------------------------
|  2 |    9 | 1370931664 | out |
|  3 |    6 | 1370932128 | out |
|  5 |   12 | 1370933037 |  in |

Lösung, die jedes Mal funktioniert:

SQLFIDDLEExample

SELECT t1.*
FROM lms_attendance t1
WHERE t1.id = (SELECT t2.id
                 FROM lms_attendance t2
                 WHERE t2.user = t1.user            
                 ORDER BY t2.id DESC
                 LIMIT 1)

2
Beeindruckend! Dies hat nicht nur funktioniert, ich durfte auch eine Ansicht mit dieser Abfrage erstellen, obwohl sie Unterabfragen enthält. Als ich zuvor versuchte, eine Ansicht mit Unterabfragen zu erstellen, ließ es mich nicht. Gibt es Regeln, warum dies erlaubt ist, eine andere jedoch nicht?
Keith

sehr merkwürdig. Danke vielmals! Vielleicht lag es daran, dass meine Unterabfrage eine Pseudotabelle war, die ich FROM auswählte, wo sie in diesem Beispiel in der WHERE-Klausel verwendet wird.
Keith

4
Keine Unterabfragen erforderlich! Darüber hinaus funktioniert diese Lösung nicht, wenn zwei Datensätze mit genau derselben Zeit vorhanden sind . Es ist nicht erforderlich, das Rad jedes Mal neu zu erfinden, da dies ein häufiges Problem ist. Suchen Sie stattdessen nach bereits getesteten und optimierten Lösungen. @Prodikl siehe meine Antwort.
TMS

ah, danke für den einblick! Ich werde den neuen Code ausprobieren, wenn ich morgen im Büro bin.
Keith

3
@TMS Diese Lösung funktioniert, wenn die Datensätze genau dieselbe Zeit haben, da die Abfrage den Datensatz mit der größten ID findet. Dies impliziert, dass die Zeit in der Tabelle die Einfügezeit ist, was möglicherweise keine gute Annahme ist. Ihre Lösung vergleicht stattdessen Zeitstempel. Wenn zwei Zeitstempel identisch sind, geben Sie auch die Zeile mit der größten ID zurück. Daher geht Ihre Lösung auch davon aus, dass der Zeitstempel in dieser Tabelle mit der Reihenfolge des Einfügens zusammenhängt. Dies ist der größte Fehler bei beiden Abfragen.
WebWanderer

73

Sie müssen nicht versuchen, das Rad neu zu erfinden, da dies das häufigste Problem für jede Gruppe ist . Sehr schöne Lösung wird vorgestellt .

Ich bevorzuge die einfachste Lösung ( siehe SQLFiddle, aktualisierte Justins ) ohne Unterabfragen (daher in Ansichten einfach zu verwenden):

SELECT t1.*
FROM lms_attendance AS t1
LEFT OUTER JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND (t1.time < t2.time 
         OR (t1.time = t2.time AND t1.Id < t2.Id))
WHERE t2.user IS NULL

Dies funktioniert auch in einem Fall, in dem es zwei verschiedene Datensätze mit demselben größten Wert innerhalb derselben Gruppe gibt - dank des Tricks mit (t1.time = t2.time AND t1.Id < t2.Id). Ich möchte hier nur sicherstellen, dass für den Fall, dass zwei Datensätze desselben Benutzers dieselbe Zeit haben, nur einer ausgewählt wird. Es spielt eigentlich keine Rolle, ob die Kriterien Idoder etwas anderes sind - im Grunde genommen würden alle Kriterien, die garantiert einzigartig sind, den Job hier machen.


1
Die maximale Verwendung t1.time < t2.timeund die minimale Verwendung t1.time > t2.timesind das Gegenteil meiner anfänglichen Intuition.
Keine

1
@ J.Money, weil implizite Negation versteckt ist: Sie wählen alle Datensätze aus t1 aus, die keinen entsprechenden Datensatz aus t2 haben, wo die t1.time < t2.timeBedingung gilt :-)
TMS

4
WHERE t2.user IS NULList ein bisschen seltsam. Welche Rolle spielt diese Linie?
tumultous_rooster

1
Die akzeptierte Antwort von Justin ist möglicherweise optimaler. Die akzeptierte Antwort verwendet einen Rückwärtsindex-Scan für den Primärschlüssel der Tabelle, gefolgt von einem Grenzwert, gefolgt von einem Sequenz-Scan der Tabelle. Daher kann die akzeptierte Antwort mit einem zusätzlichen Index stark optimiert werden. Diese Abfrage könnte auch durch einen Index optimiert werden, da sie zwei Sequenzscans durchführt, aber auch einen Hash und einen "Hash-Anti-Join" der Ergebnisse des Sequenzscans und den Hash des anderen Sequenzscans enthält. Mich würde eine Erklärung interessieren, welcher Ansatz wirklich optimaler ist.
WebWanderer

@TMS könnten Sie bitte OR (t1.time = t2.time AND t1.Id < t2.Id))Abschnitt klären ?
Oleg Kuts

6

Basierend auf der @ TMS-Antwort gefällt es mir, weil keine Unterabfragen erforderlich sind, aber ich denke, dass das Auslassen des 'OR'Teils ausreichend und viel einfacher zu verstehen und zu lesen ist.

SELECT t1.*
FROM lms_attendance AS t1
LEFT JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND t1.time < t2.time
WHERE t2.user IS NULL

Wenn Sie nicht an Zeilen mit Nullzeiten interessiert sind, können Sie diese in der folgenden WHEREKlausel filtern :

SELECT t1.*
FROM lms_attendance AS t1
LEFT JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND t1.time < t2.time
WHERE t2.user IS NULL and t1.time IS NOT NULL

Das Weglassen des ORTeils ist eine wirklich schlechte Idee, wenn zwei Datensätze dasselbe haben können time.
TMS

Ich würde diese Lösung aus Leistungsgründen vermeiden. Wie @OlegKuts erwähnt, wird dies bei mittleren bis großen Datenmengen sehr langsam.
Peter Meadley

4

Bereits gelöst, aber nur für die Aufzeichnung, wäre ein anderer Ansatz, zwei Ansichten zu erstellen ...

CREATE TABLE lms_attendance
(id int, user int, time int, io varchar(3));

CREATE VIEW latest_all AS
SELECT la.user, max(la.time) time
FROM lms_attendance la 
GROUP BY la.user;

CREATE VIEW latest_io AS
SELECT la.* 
FROM lms_attendance la
JOIN latest_all lall 
    ON lall.user = la.user
    AND lall.time = la.time;

INSERT INTO lms_attendance 
VALUES
(1, 9, 1370931202, 'out'),
(2, 9, 1370931664, 'out'),
(3, 6, 1370932128, 'out'),
(4, 12, 1370932128, 'out'),
(5, 12, 1370933037, 'in');

SELECT * FROM latest_io;

Klicken Sie hier, um es bei SQL Fiddle in Aktion zu sehen


1
danke für das Follow-up! Ja, ich wollte mehrere Ansichten erstellen, wenn es keinen einfacheren Weg gäbe.
Keith

0
select b.* from 

    (select 
        `lms_attendance`.`user` AS `user`,
        max(`lms_attendance`.`time`) AS `time`
    from `lms_attendance` 
    group by 
        `lms_attendance`.`user`) a

join

    (select * 
    from `lms_attendance` ) b

on a.user = b.user
and a.time = b.time

Vielen Dank. Ich weiß, dass ich es mit einer Unterabfrage tun kann, aber ich hatte gehofft, dies in eine Ansicht umzuwandeln, und es werden keine Unterabfragen in Ansichten AFAIK zugelassen. müsste ich jede Unterabfrage in eine Ansicht usw. verwandeln?
Keith

join (select * from lms_attendance ) b= join lms_attendance b
Azerafati

0
 select result from (
     select vorsteuerid as result, count(*) as anzahl from kreditorenrechnung where kundeid = 7148
     group by vorsteuerid
 ) a order by anzahl desc limit 0,1

0

Wenn Sie mit MySQL 8.0 oder höher arbeiten, können Sie folgende Fensterfunktionen verwenden :

Abfrage:

DBFiddleExample

SELECT DISTINCT
FIRST_VALUE(ID) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS ID,
FIRST_VALUE(USER) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS USER,
FIRST_VALUE(TIME) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS TIME,
FIRST_VALUE(IO) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS IO
FROM lms_attendance;

Ergebnis:

| ID | USER |       TIME |  IO |
--------------------------------
|  2 |    9 | 1370931664 | out |
|  3 |    6 | 1370932128 | out |
|  5 |   12 | 1370933037 |  in |

Der Vorteil, den ich gegenüber der von Justin vorgeschlagenen Lösung sehe , besteht darin, dass Sie die Zeile mit den neuesten Daten pro Benutzer (oder pro ID oder was auch immer) auch aus Unterabfragen auswählen können, ohne dass eine Zwischenansicht oder Tabelle erforderlich ist.

Und falls Sie eine HANA betreiben, ist diese auch ~ 7-mal schneller: D.


-1

Ok, das könnte entweder ein Hack oder fehleranfällig sein, aber irgendwie funktioniert das auch.

SELECT id, MAX(user) as user, MAX(time) as time, MAX(io) as io FROM lms_attendance GROUP BY id;

-2

Versuchen Sie diese Abfrage:

  select id,user, max(time), io 
  FROM lms_attendance group by user;

Versuchen Sie, daraus eine SQLFiddle zu machen. Sie werden wahrscheinlich feststellen , dass idund iosind nicht aggregierten Spalten, die nicht in einem verwendet werden können group by.
Dewi Morgan

1
Es gibt keine Garantie, dass die ID die ID mit max (Zeit) ist. Es kann sich um eine der IDs innerhalb der Gruppe handeln. Dies ist das Problem, das ich hier lösen wollte, immer noch auf der Suche
Robisrob

-3

Möglicherweise können Sie nach Benutzer gruppieren und dann nach Zeit absteigen. So etwas wie unten

  SELECT * FROM lms_attendance group by user order by time desc;

-3

Das hat bei mir funktioniert:

SELECT user, time FROM 
(
    SELECT user, time FROM lms_attendance --where clause
) AS T 
WHERE (SELECT COUNT(0) FROM table WHERE user = T.user AND time > T.time) = 0
ORDER BY user ASC, time DESC
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.