2016-08-10 62 views
7

Mam kwerendę, która wygląda tak:Jak WYBIERZ górny wiersz na grupę na podstawie wielu kolumn zamówienia?

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; 

Co to robi to powrót mi jeden wiersz, gdzie some_count to najwyższa liczba dla user_id = 1. Daje mi również znacznik czasu, który jest najbardziej aktualny dla tego some_count, ponieważ some_count może być taki sam dla wielu wartości time_start i chcę najbardziej aktualny.

Teraz próbuję wykonać to zapytanie, które to rozwiąże dla każdego pojedynczego user_id, które wystąpiło co najmniej raz dla określonej daty, w tym przypadku 2016-07-27. Ostatecznie prawdopodobnie będzie to wymagać GROUP BY, ponieważ szukam grupy maksymalnej na user_id

Jaki jest najlepszy sposób napisania zapytania o takim charakterze?

+0

SELECT DISTINCT (user_id), ... dostarczy Ci jeden wpis na użytkownika bez GROUP BY. Jaką kolumnę ma mieć wartość MAX()? – user3741598

+0

Chcę, aby wartość MAX() dla 'some_count', ale także muszę znać MAX()' time_stop' dla gdzie ten konkretny 'some_count' pasuje ponieważ może być wiele wierszy gdzie' some_count' jest identyczne dla 'user_id 'i' time_stop' – randombits

+0

Co to jest klucz podstawowy? –

Odpowiedz

2

Podzielam dwa z moich podejść.

Podejście nr 1 (skalowalne):

Korzystanie 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; 

Scalable bo jeśli chcesz najnowsze n wierszy dla każdego użytkownika, a następnie po prostu trzeba zmienić tę linię:

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

Mogę dodać wyjaśnienie później, jeśli kwerenda zapewnia oczekiwany rezultat


Podejście nr 2: (Nie skalowalne)

Korzystanie 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

Można użyć NOT EXISTS():

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

NOT EXISTS() wybierze tylko rekordy, które kolejny rekord z większej liczby lub inny zapis o tej samej liczbie, ale nowsza time_stop nie istnieje dla nich.

1

Możesz użyć pierwotnego zapytania jako skorelowanego podzapytania w klauzuli WHERE.

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 powinien być w stanie buforować wynik podzapytania dla każdego odrębnego user_id.

Innym sposobem jest użycie zagnieżdżonych zapytań GROUP BY:

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; 

Wyjaśniając od wewnątrz na zewnątrz: Najbardziej wewnętrzna tabela (b) daje max some_count na użytkownika. to nie wystarcza, ponieważ chcesz maksimum dla dwóch kolumn - więc dołączam do niego z pełną tabelą (a), aby uzyskać rekordy, które mają te maksymalne wartości (c), i z tego biorę maksymalny time_start dla każda kombinacja user/some_count.

+0

Musiałem edytować mój OP. Musiałem "DESC" przez 'time_start'. Sposób, w jaki teraz działa twoje zapytanie, otrzymuję wiersze pasujące do 'time_start', które nie pasują:' WHERE DATE (time_start) = '2016-07-27'' – randombits

+0

@randombits - Edytowałem moje zapytanie, zmiana polega na użyciu 'time_start 'zamiast' time_stop'.Nie wiem, czy podążam za tym, co masz na myśli w swoim komentarzu, dostaniesz wpisy od tego dnia, ale maksimum dziennie. co masz na myśli dopasowywania 'time_start'? –

0

wierzę, nie musisz robić nic wymyślnego dla zapytania. Wystarczy posortować tabelę przez user_id w kolejności rosnącej i some_count i TIME_START w kolejności malejącej i wybierz oczekiwane pola z uporządkowanej tabeli grupy BY user_id. To proste. Spróbuj i daj mi znać, jeśli działa.

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

Wystąpił błąd w poprzedniej odpowiedzi. przepraszam za niechciany błąd. Rozwiązałem problemy i sprawdziłem. Wydaje się, że działa dobrze :) –

1

Strategia

Na ogół jest to bardziej skuteczne, aby znaleźć maksymalne wartości zamiast grupy rejestrów sortowania. W takim przypadku porządek jest liczbą całkowitą (some_count), a następnie datą/czasem (time_start) - aby znaleźć pojedynczy maksymalny wiersz, musimy połączyć je w jakiś sposób.

Prostym sposobem na połączenie tych dwóch elementów w ciąg znaków jest zwykle zwykły szkopuł o porównywaniu ciągów o wartości "4", który jest na przykład wyższy niż "12". Można to łatwo pokonać, używając LPAD, aby dodać zera wiodące, aby 4 stał się "0000000004", który jest niższy niż "0000000012" w porównaniu łańcuchów. Założenie, że time_start jest polem DATETIME, może być po prostu dołączone do tego dla wtórnego uporządkowania, ponieważ jego konwersja w postaci ciągu daje w wyniku sortowalny format (yyyy-mm-dd hh:MM:ss).

SQL

Stosując tę ​​strategię, możemy ograniczyć za pomocą prostego podselekcji:

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 tutaj: http://rextester.com/HCGY1362

0

Twój problem może być rozwiązany z czymś, co nazywa się funkcje okienkowe, ale MySQL nie obsługuje tej funkcji.

Mam dla ciebie dwa rozwiązania. Jedna jest symulacją funkcji okna, a druga jest typowym sposobem pisania zapytań, aby rozwiązać te sytuacje w MySQL.

Jest to pierwszy jeden, który mi odpowiedział this question:

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

Zasadniczo, można grupować dane według user_id i skleja wszystkie wartości z określonej kolumny przy użyciu , separator, zamówionej przez kolumny, które chcesz, dla każdej grupy, a następnie podciąga tylko pierwszą uporządkowaną wartość. Nie jest to optymalne podejście ...

A to druga, co odpowiedziałem this question:

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 
; 

Zasadniczo sprawdza podzapytanie dla każdego user_id jeśli istnieją some_count im wyższy był obecny jeden sprawdzane w tym dniu, ponieważ zapytanie główne oczekuje na NOT EXISTS. Zostawisz wszystkie najwyższe wartości some_count na user_id w jednym dniu, ale dla tej samej najwyższej wartości od użytkownika może istnieć kilka różnych time_start w tej dacie. Teraz wszystko jest proste. Możesz bezpiecznie GROUP BY użytkownika i liczyć, ponieważ są one już dane, które chcesz, i uzyskać z grupy maksymalnie time_start.

Tego rodzaju podzapytanie jest popularnym sposobem rozwiązywania takich problemów, jak w MySQL. Polecam wypróbować oba rozwiązania, ale wybierz drugi i zapamiętaj podkwerendę sintax, aby rozwiązać każdy przyszły problem.

Ponadto w MySQL stosuje się domyślnie ORDER BY <columns> we wszystkich zapytaniach mających GROUP BY <columns>. Jeśli nie zawracasz sobie głowy kolejnością wyników, możesz zapisać trochę przetwarzania, deklarując ORDER BY NULL, co spowoduje wyłączenie funkcji niejawnego odwołania w zapytaniu.

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 

I dodać je do lepszej wydajności:

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

Test zakresie time_start został zmieniony tak, że może być stosowany drugi indeks.

To było luźno wyprowadzone z bloga pod adresem groupwise max.