2016-08-10 5 views
7

Ich habe eine Abfrage, die wie folgt aussieht:Wie wählt man die oberste Zeile pro Gruppe basierend auf mehreren Ordnungsspalten aus?

SELECT time_start, some_count 
    FROM foo 
    WHERE user_id = 1 
    AND DATE(time_start) = '2016-07-27' 
    ORDER BY some_count DESC, time_start DESC LIMIT 1; 

Was das bedeutet ist, zurückkehren mir eine Zeile, wo some_count die höchste Anzahl für user_id = 1 ist. Es gibt mir auch den Zeitstempel, der der aktuellste für diesen some_count ist, wie some_count könnte für mehrere time_start gleiche Werte sein und ich möchte die aktuellste.

Jetzt versuche ich zu tun ist eine Abfrage ausführen, die dies für jede einzelne user_id, die mindestens einmal für ein bestimmtes Datum aufgetreten ist, in diesem Fall 2016-07-27. Letztendlich wird es wahrscheinlich eine GROUP BY erfordern, da ich nach einer Gruppenmaximum pro user_id suche

Was ist der beste Weg, um eine Abfrage dieser Art zu schreiben?

+0

SELECT DISTINCT (user_id), ... erhalten Sie einen Eintrag pro Benutzer ohne GROUP BY. In welcher Spalte soll der MAX() Wert für? – user3741598

+0

Ich möchte den MAX() -Wert für 'some_count', aber ich muss auch wissen, die MAX()' time_stop' für, wo diese bestimmte 'some_count' übereinstimmt, da es mehrere Zeilen geben könnte, wobei' some_count' ist gleich für 'user_id 'und' time_stop' – randombits

+0

Was ist der Primärschlüssel? –

Antwort

2

Ich teile zwei meiner Ansätze.

Ansatz # 1 (skalierbar):

Mit MySQL user_defined variables

SELECT 
    t.user_id, 
    t.time_start, 
    t.time_stop, 
    t.some_count 
FROM 
(
    SELECT 
     user_id, 
     time_start, 
     time_stop, 
     some_count, 
     IF(@sameUser = user_id, @rn := @rn + 1, 
      IF(@sameUser := user_id, @rn := 1, @rn := 1) 
     ) AS row_number 

    FROM foo 
    CROSS JOIN (
     SELECT 
      @sameUser := - 1, 
      @rn := 1 
    ) var 
    WHERE DATE(time_start) = '2016-07-27' 
    ORDER BY user_id, some_count DESC, time_stop DESC 
) AS t 
WHERE t.row_number <= 1 
ORDER BY t.user_id; 

skalierbar, weil, wenn Sie für jeden Benutzer neuesten n Zeilen wollen, dann nur diese Zeile ändern müssen:

... WHERE t.row_number <= n...

Ich kann Erklärung hinzufügen später, wenn die Abfrage erwartetes Ergebnis liefert


Ansatz # 2: (nicht skalierbar)

Mit INNER JOIN and GROUP BY

SELECT 
F.user_id, 
F.some_count, 
F.time_start, 
MAX(F.time_stop) AS max_time_stop 
FROM foo F 
INNER JOIN 
(
    SELECT 
     user_id, 
     MAX(some_count) AS max_some_count 
    FROM foo 
    WHERE DATE(time_start) = '2016-07-27' 
    GROUP BY user_id 
) AS t 
ON F.user_id = t.user_id AND F.some_count = t.max_some_count 
WHERE DATE(time_start) = '2016-07-27' 
GROUP BY F.user_id 
1

können Sie NOT EXISTS() verwenden:

SELECT * FROM foo t 
WHERE (DATE(time_start) = '2016-07-27' 
    OR DATE(time_stop) = '2016-07-27') 
    AND NOT EXISTS(SELECT 1 FROM foo s 
       WHERE t.user_id = s.user_id 
       AND (s.some_count > t.some_count 
        OR (s.some_count = t.some_count 
         AND s.time_stop > t.time_stop))) 

Die NOT EXISTS() werden nur Datensätze auswählen, die ein anderer Datensatz mit einer größeren Zahl oder eine andere Aufzeichnung mit der gleichen Zählung aber eine neuere time_stop nicht für sie vorhanden ist.

1

Sie können Ihre ursprüngliche Abfrage als korrelierte Unterabfrage in der WHERE-Klausel verwenden.

SELECT user_id, time_stop, some_count 
FROM foo f 
WHERE f.id = (
    SELECT f1.id 
    FROM foo f1 
    WHERE f1.user_id = f.user_id -- correlate 
    AND DATE(f1.time_start) = '2016-07-27' 
    ORDER BY f1.some_count DESC, f1.time_stop DESC LIMIT 1 
) 

MySQL sollte das Ergebnis der Unterabfrage für jede unterschiedliche user_id zwischenzuspeichern können.

Ein anderer Weg ist verschachtelte GROUP BY-Abfragen zu verwenden:

select f.user_id, f.some_count, max(f.time_stop) as time_stop 
from (
    select f.user_id, max(f.some_count) as some_count 
    from foo f 
    where date(f.time_start) = '2016-07-27' 
    group by f.user_id 
) sub 
join foo f using(user_id, some_count) 
where date(f.time_start) = '2016-07-27' 
group by f.user_id, f.some_count 
1
SELECT user_id, 
     some_count, 
     max(time_start) AS time_start 
FROM 
    (SELECT a.* 
    FROM foo AS a 
    INNER JOIN 
    (SELECT user_id, 
      max(some_count) AS some_count 
     FROM foo 
     WHERE DATE(time_start) = '2016-07-27' 
     GROUP BY user_id) AS b ON a.user_id = b.user_id 
    AND a.some_count = b.some_count) AS c 
GROUP BY user_id, 
     some_count; 

von innen Erklären heraus: Die innere Tabelle (b) Sie pro Benutzer die max some_count geben. Das ist nicht genug, da Sie das Maximum für zwei Spalten haben wollen - also schließe ich mich der vollständigen Tabelle (a) an, um die Datensätze zu erhalten, die diese Maximalwerte (c) haben, und davon nehme ich den max time_start für jede Benutzer/Some_count Kombination.

+0

bearbeiten musste. Ich brauchte 'DESC' von' time_start'. Die Art und Weise, wie Ihre Abfrage jetzt ausgeführt wird, bekomme ich Zeilen, die mit 'time_start' übereinstimmen:' WHERE DATE (time_start) = '2016-07-27'' – randombits

+0

@randombits - Ich habe meine Abfrage bearbeitet, die Änderung soll 'time_start' sein 'anstelle von' time_stop'.nicht sicher, ich folge, was Sie in Ihrem Kommentar meinen, Sie erhalten Einträge von diesem Tag, aber die maximale pro Tag. Was meinst du mit "time_start"? –

0

Ich glaube, Sie brauchen nichts Besonderes zu tun für die Abfrage. sortieren einfach die Tabelle von User_id aufsteigend und some_count und TIME_START in absteigender Reihenfolge und wählen Sie erwartete Felder aus der geordneten Tabelle GROUP BY User_id. Es ist einfach. Versuchen Sie es und lassen Sie mich wissen, ob es funktioniert.

SELECT user_id, some_count, time_start 
FROM (SELECT * FROM foo ORDER BY user_id ASC, some_count DESC, time_start DESC)sorted_foo 
WHERE DATE(time_start) = '2016-07-27' 
GROUP BY user_id 
+0

In der vorherigen Antwort ist ein Fehler aufgetreten. Entschuldigung für den unerwünschten Fehler. Ich habe die Probleme gelöst und überprüft. Scheint jetzt klappt es gut :) –

1

Strategie

Im Allgemeinen ist es effizienter, eher Maximalwerte zu finden, als Gruppen von Datensätzen zu sortieren. In diesem Fall befindet sich die Reihenfolge in einer Ganzzahl (some_count) gefolgt von einem Datum/einer Uhrzeit (time_start). Um also eine einzelne maximale Zeile zu finden, müssen wir diese in irgendeiner Weise kombinieren.

Eine einfache Möglichkeit, dies zu tun, besteht darin, die beiden zu einer Zeichenkette zu kombinieren, aber es gibt den üblichen Fehler des Zeichenkettenvergleichs, der beispielsweise "4" als höher als "12" bewertet. Dies lässt sich leicht umgehen, indem Sie LPAD verwenden, um führende Nullen hinzuzufügen, sodass 4 zu "0000000004" wird, was niedriger als "0000000012" in einem Zeichenfolgenvergleich ist. Wenn time_start ein Feld ist, kann es einfach an dieses für eine sekundäre Reihenfolge angehängt werden, da seine String-Konvertierung ein sortierbares Format (yyyy-mm-dd hh:MM:ss) ergibt.

SQL

dieser Strategie können wir über eine einfache subselect beschränken:

SELECT time_start, some_count 
FROM foo f1 
WHERE DATE(time_start) = '2016-07-27' 
    AND CONCAT(LPAD(some_count, 10, '0'), time_start) = 
     (SELECT MAX(CONCAT(LPAD(some_count, 10, '0'), time_start)) 
     FROM foo f2 
     WHERE DATE(f2.time_start) = '2016-07-27' 
     AND f2.user_id = f1.user_id); 

Demo

Rextester Demo hier: http://rextester.com/HCGY1362

0

Ihr Problem sein könnte gelöst mit etwas namens Fensterfunktionen, aber MySQL hat keine Unterstützung für diese Art von Funktion.

Ich habe zwei Lösungen für Sie. Einer simuliert eine Fensterfunktion und der andere ist der übliche Weg, um einige Abfragen zu schreiben, um diese Situationen in MySQL anzugehen.

Dies ist die erste ist, die ich this question antwortete:

-- simulates the window function 
-- first_value(<col>) over(partition by user_id order by some_count DESC, time_start DESC) 
SELECT 
    user_id, 
    substring_index(group_concat(time_start ORDER BY some_count DESC, time_start DESC), ',', 1) time_start, 
    substring_index(group_concat(some_count ORDER BY some_count DESC, time_start DESC), ',', 1) some_count 
FROM foo 
WHERE DATE(time_start) = '2016-07-27' 
GROUP BY user_id 
; 

Grundsätzlich Sie gruppieren Sie Ihre Daten von user_id und verkettet alle Werte aus einer bestimmten Spalte mit der , Separator, geordnet durch die Spalten, die Sie möchten, für jede Gruppe und substringt dann nur den ersten geordneten Wert. Dies ist kein optimaler Ansatz ...

Und das ist die zweite, die ich this question antwortete:

SELECT 
    user_id, 
    some_count, 
    MAX(time_start) time_start 
FROM foo outq 
WHERE 1=1 
    AND DATE(time_start) = '2016-07-27' 
    AND NOT EXISTS 
    (
    SELECT 1 
    FROM foo 
    WHERE 1=1 
     AND user_id = outq.user_id 
     AND some_count > outq.some_count 
     AND DATE(time_start) = DATE(outq.time_start) 
) 
GROUP BY 
    user_id, 
    some_count 
; 

Grundsätzlich sind die Unterabfrage überprüft für jeden user_id wenn es welche gibt some_count höher sie die aktuelle gewesen An diesem Datum überprüft, wie die Hauptabfrage es zu NOT EXISTS erwartet. Sie werden mit allen höchsten some_count pro user_id in einem Datum verlassen, aber für den gleichen höchsten Wert von einem Benutzer können mehrere verschiedene time_start in diesem Datum existieren. Jetzt sind die Dinge einfach. Sie können sicher GROUP BY Benutzer und zählen, denn sie sind bereits die Daten, die Sie wollen, und erhalten von der Gruppe das Maximum time_start.

Diese Art von Unterabfrage ist die gängige Methode, um Probleme wie MySQL zu lösen. Ich empfehle Ihnen, beide Lösungen auszuprobieren, aber wählen Sie die zweite und erinnern Sie sich an die Unterabfrage sintax, um ein zukünftiges Problem zu lösen.

Auch in MySQL wird eine implizite ORDER BY <columns> in allen Abfragen mit einem GROUP BY <columns> angewendet. Wenn Sie sich nicht um die Ergebnisreihenfolge kümmern, können Sie einige Verarbeitungen speichern, indem Sie ORDER BY NULL deklarieren, wodurch die implizite Ordnungsfunktion in Ihrer Abfrage deaktiviert wird.

0
SELECT c1.user_id, c1.some_count, MAX(c1.time_start) AS time_start 
    FROM foo AS c1 
    JOIN 
     (SELECT user_id, MAX(some_count) AS some_count 
      FROM foo 
      WHERE time_start >= '2016-07-27' 
       AND time_start < '2016-07-27' + INTERVAL 1 DAY 
      GROUP BY user_id 
    ) AS c2 USING (user_id, some_count) 
    GROUP BY c1.user_id, c1.some_count 

Und, fügen Sie diese für eine bessere Leistung:

INDEX(user_id, some_count, time_start) 
INDEX(time_start) 

Der Test für die time_start Bereich geändert wurde, so dass der zweite Index verwendet werden könnte.

Dies wurde lose von Blog auf groupwise max abgeleitet.

Verwandte Themen