2012-06-19 17 views
6

Mam dwie tabele: „Serwery” i „Statystyka”MySQL - Średnie najnowsze kolumny w innej tabeli

serwery zawiera kolumnę o nazwie „id” z automatycznym przyrosty. stats ma kolumnę o nazwie "server", która odpowiada wierszowi w tabeli serwerów, kolumnie o nazwie "time", która reprezentuje czas, w którym została dodana, oraz kolumnie o nazwie "votes", którą chciałbym uzyskać średnią.

Chciałbym pobrać wszystkie serwery (SELECT * FROM servers) wraz ze średnią głosów z 24 ostatnich wierszy, które odpowiadają każdemu serwerowi. Uważam, że jest to pytanie "największa grupa na grupę".

To, co starałem się zrobić, ale to dało mi 24 wierszy sumie nie 24 wierszy na grupy:

SELECT servers.*, 
     IFNULL(AVG(stats.votes), 0) AS avgvotes 
FROM servers 
LEFT OUTER JOIN 
    (SELECT server, 
      votes 
    FROM stats 
    GROUP BY server 
    ORDER BY time DESC LIMIT 24) AS stats ON servers.id = stats.server 
GROUP BY servers.id 

Jak mówiłem, chciałbym, aby uzyskać 24 najnowszych wierszy dla każdego serwera , a nie 24 ostatnich ostatnich wierszy.

+1

Wierzę, że to (http://sqlfiddle.com/#!2/d908f/5) jest strukturą tabeli w tabeli . Dobrze? –

Odpowiedz

1

To kolejne podejście.

To zapytanie ma takie same problemy z wydajnością, jak inne zapytania, które zwracają poprawne wyniki, ponieważ plan wykonania dla tego zapytania będzie wymagał operacji SORT na KAŻDYM wierszu w tabeli statystyk. Ponieważ nie ma predykatu (ograniczenia) w kolumnie czasu, będzie brany pod uwagę KAŻDY wiersz w tabeli statystyk. Dla NAPRAWDĘ dużego stołu stats, to spowoduje zdmuchnięcie całej dostępnej tymczasowej przestrzeni, zanim umrze straszliwa śmierć. (więcej Uwagi dotyczące wydajności poniżej).

SELECT r.* 
    , IFNULL(s.avg_votes,0) 
    FROM servers r 
    LEFT 
    JOIN (SELECT t.server 
       , AVG(t.votes) AS avg_votes 
      FROM (SELECT CASE WHEN u.server = @last_server 
          THEN @i := @i + 1 
          ELSE @i := 1 
         END AS i 
         , @last_server := u.server AS `server` 
         , u.votes AS votes 
        FROM (SELECT @i := 0, @last_server := NULL) i 
        JOIN (SELECT v.server, v.votes 
          FROM stats v 
          ORDER BY v.server DESC, v.time DESC 
         ) u 
       ) t 
      WHERE t.i <= 24 
      GROUP BY t.server 
     ) s 
    ON s.server = r.id 

Co robi ta kwerenda jest sortowanie tabeli statystyki przez serwer i malejącej na kolumnie czasu. (Widok śródliniowany jako u.)

W posortowanym zestawie wyników przyporządkowujemy numery rzędów 1,2,3 itd. Do każdego rzędu dla każdego serwera. (Widok śródliniowy z aliasami jako t.)

Z tym zestawem wyników odfiltrowujemy wszystkie wiersze z rownumber> 24 i obliczamy średnią z kolumny votes dla "najnowszych" 24 wierszy dla każdego serwera. (Widok śródliniowany jako s.)

Ostatnim krokiem jest połączenie tego z tabelą serwerów, aby zwrócić żądany zestaw wyników.


UWAGA:

plan wykonania dla tego zapytania będzie kosztowne dla dużej liczby wierszy w tabeli stats.

Aby poprawić wydajność, możemy przyjąć kilka podejść.

Najprostszym moc należy uwzględnić w zapytaniu do orzeczenie Wyklucza znaczna liczba rzędów z tabeli stats (np rzędy z time wartości około 2 dni, lub ponad 2 tygodni). To znacznie zmniejszyłoby liczbę wierszy, które muszą być posortowane, aby określić "najnowsze" 24 wiersze.

Ponadto, z indeksem na stats(server,time), możliwe jest również, że MySQL może wykonać względnie efektywne "skanowanie wsteczne" na indeksie, unikając operacji sortowania.

Możemy również rozważyć wdrożenie indeksu w tabeli statystyk na (server,"reverse_time"). Ponieważ MySQL nie obsługuje jeszcze indeksów malejących, implementacja byłaby naprawdę regularnym (rosnącym) indeksem dla pochodnej wartości rtime (wyrażenie "odwróconego czasu", które jest rosnące dla malejących wartości time (na przykład -1*UNIX_TIMESTAMP(my_timestamp) lub -1*TIMESTAMPDIFF('1970-01-01',my_datetime).

Innym sposobem na zwiększenie wydajności byłoby zachowanie tabeli cienia zawierającej ostatnie 24 wiersze dla każdego serwera. Byłoby to najprostsze rozwiązanie, gdybyśmy mogli zagwarantować, że "najnowsze wiersze" nie zostaną usunięte z tabeli stats Możemy utrzymać tę tabelę z wyzwalaczem.Zasadniczo, gdy wiersz jest wstawiany do tabeli stats, sprawdzamy, czy time w nowych wierszach jest późniejszy niż najwcześniejszy time przechowywany dla serwera w cieniu Jeśli tak jest, zastępujemy najwcześniejszy wiersz w tabeli cieni nowym wierszem, pamiętając o tym, aby w tabeli cienia przechowywać nie więcej niż 24 wiersze dla każdego serwera.

I jeszcze innym podejściem jest napisanie procedury lub funkcji, która daje wynik. Podejściem tutaj byłoby przechodzenie przez każdy serwer i uruchamianie oddzielnego zapytania względem tabeli statystyk, aby uzyskać średnią votes dla ostatnich 24 wierszy i zebranie wszystkich tych wyników razem. (Takie podejście może być raczej obejściem, aby uniknąć pewnego rodzaju ogromnego zestawu tymczasowego, aby umożliwić powrót zestawu wyników, niekoniecznie powodując niesamowicie szybki powrót zestawu wyników.)

Najważniejsze dla wydajności ten typ zapytania w tabeli LARGE ogranicza liczbę wierszy rozpatrywanych przez zapytanie I unika operacji sortowania na dużym zestawie. Tak otrzymujemy kwerendę do wykonania.


DODATEK

Aby dostać „skanowanie reverse index” operacji (aby uzyskać wiersze z stats zamówione przy użyciu indeksu BEZ operacji filesort), musiałem podać malejącej na obu wyrażeń w Klauzula ORDER BY. Poprzednio powyższe zapytanie miało ORDER BY server ASC, time DESC, a MySQL zawsze chciał zrobić plik, nawet określając podpowiedź FORCE INDEX FOR ORDER BY (stats_ix1).

Jeśli wymagane jest zwrócenie "średniej liczby głosów" dla serwera tylko, jeśli w tabeli statystyk znajduje się co najmniej 24 powiązane wiersze, możemy wykonać bardziej efektywne zapytanie, nawet jeśli jest ono nieco bardziej niechlujny. (Większość bałaganu w zagnieżdżonych funkcjach IF() polega na radzeniu sobie z wartościami NULL, które nie są uwzględniane w średniej, może być znacznie mniej niechlujny, jeśli mamy gwarancję, że votes NIE jest NULL, lub jeśli wykluczymy jakiekolwiek wiersze gdzie votes jest puste.)

SELECT r.* 
    , IFNULL(s.avg_votes,0) 
    FROM servers r 
    LEFT 
    JOIN (SELECT t.server 
       , t.tot/NULLIF(t.cnt,0) AS avg_votes 
      FROM (SELECT IF(v.server = @last_server, @num := @num + 1, @num := 1) AS num 
         , @cnt := IF(v.server = @last_server,IF(@num <= 24, @cnt := @cnt + IF(v.votes IS NULL,0,1),@cnt := 0),@cnt := IF(v.votes IS NULL,0,1)) AS cnt 
         , @tot := IF(v.server = @last_server,IF(@num <= 24, @tot := @tot + IFNULL(v.votes,0)  ,@tot := 0),@tot := IFNULL(v.votes,0)  ) AS tot 
         , @last_server := v.server AS SERVER 
        -- , v.time 
        -- , v.votes 
        -- , @tot/NULLIF(@cnt,0) AS avg_sofar 
        FROM (SELECT @last_server := NULL, @num:= 0, @cnt := 0, @tot := 0) u 
        JOIN stats v FORCE INDEX FOR ORDER BY (stats_ix1) 
        ORDER BY v.server DESC, v.time DESC 
       ) t 
      WHERE t.num = 24 
     ) s 
    ON s.server = r.id 

o wskaźniku powłoką na stats(server,time,votes), wyjaśniania wykazały MySQL uniknąć operacji filesort, więc musi on być używany „skanowanie” odwrotnej indeksów wierszy powrót w porządku. Bez indeksu obejmującego i indeksu na "(serwer, czas) , MySQL used the index if I included an index hint, with the SIŁA INDEKS NA ZAMÓWIENIE (stats_ix1)" podpowiedź, MySQL również unikał plików. (Ale ponieważ mój stół miał mniej niż 100 wierszy, nie sądzę, aby MySQL kładł duży nacisk na unikanie operacji na plikach.)

Wyrażenia czasu, głosów i wyrazów avg_sofar są komentowane (w widoku liniowym pod aliasem t); nie są potrzebne, ale służą do debugowania.

Sposób, w jaki zapytanie się znajduje, wymaga co najmniej 24 wierszy w statystykach dla każdego serwera, aby zwrócić średnią. (Może to być dopuszczalne.) Ale myślałem, że ogólnie rzecz biorąc, możemy zwrócić sumę bieżącą, sumę do tej pory (tot) i liczbę uruchomień (cnt).

(Gdybyśmy wymienić WHERE t.num = 24 z WHERE t.num <= 24, widzimy uruchomiony średnio w akcji.)

Aby powrócić średnią tam, gdzie nie są na co najmniej 24 wierszy w statystykach, to naprawdę kwestia identyfikacji wiersz (dla każdego serwera) z maksymalną wartością liczby równą < = 24.

+0

Przepraszamy za spóźnioną odpowiedź. To zapytanie działa i działa szybciej niż poprzednie odpowiedzi.Również bardzo doceniam twoje szczegółowe wyjaśnienie i twoje liczne rozwiązania zwiększające prędkość. Obecnie jest 40 000 wierszy, jednak istnieje potencjał do zwiększenia do kilku milionów. Będę używał indeksu ('stats (server, time)') na razie i jeśli wystąpi znaczny spadek wydajności, prawdopodobnie zaimplementuję twoją propozycję cienia. Dziękuje bardzo! – fruitcup

+0

Indeks obejmujący "statystyki (serwer, czas, głosy)" byłby jeszcze lepszy pod względem wydajności. Dodałem załącznik do mojej odpowiedzi, z innym zapytaniem, które może być jeszcze szybsze. Ma ograniczenie (jak pisano), że w tabeli statystyk musi być co najmniej 24 wiersze dla serwera, aby średnia była zwracana. – spencer7593

2

Dzięki za ten wielki post.

alter table add index(server, time) 
set @num:=0, @server:=''; 
select servers.*, IFNULL(AVG(stats.votes), 0) AS avgvotes 
from servers left outer join (
select server, 
     time,votes, 
     @num := if(@server = server, @num + 1, 1) as row_number, 
     @server:= server as dummy 
from stats force index(server) 
group by server, time 
having row_number < 25) as stats 
on servers.id = stats.server 
group by servers.id 

edit 1

Właśnie zauważyłem, że powyższe zapytanie daje najstarsze 24 rejestry dla poszczególnych grup.

set @num:=0, @server:=''; 
select servers.*, IFNULL(AVG(stats.votes), 0) AS avgvotes 
from servers left outer join (
select server, 
     time,votes, 
     @num := if(@server = server, @num + 1, 1) as row_number, 
     @server:= server as dummy 
from (select * from stats order by server, time desc) as t 
group by server, time 
having row_number < 25) as stats 
on servers.id = stats.server 
group by servers.id 

który daje średnio 24 Najnowszy jednostki dla każdej grupy

Edit2

@DrAgonmoray można spróbować wewnętrzną część zapytań pierwszy i zobaczyć, czy to zwraca najnowsze 24 rekordów dla każdej grupy. W moim mysql 5.5 działa poprawnie.

select server, 
     time,votes, 
     @num := if(@server = server, @num + 1, 1) as row_number, 
     @server:= server as dummy 
from (select * from stats order by server, time desc) as t 
group by server, time 
having row_number < 25 
+0

Otrzymuję tutaj błąd składni: "wybierz serwery. *, IFNULL (AVG (stats.votes), 0) AS avgvotes z lef serwera" w linii 2 – fruitcup

+1

@DrAgonmoray, umieść ';' po 'alter tabeli dodaj ... 'linię oraz linię' set @num ... ', ponieważ są to oddzielne polecenia od rzeczywistego zapytania. –

+0

Teraz kod działa, ale wydaje się, że podaje mi średnią wszystkich rekordów dla każdego serwera, zamiast tylko ostatnich 24. Testowałem to używając kilku różnych serwerów. – fruitcup

0

Spróbuj tego rozwiązania, z techniki top-n-per-grupy w INNER JOIN podselekcji zaliczony do Bill Karwin i jego post o niej here.

SELECT 
    a.*, 
    AVG(b.votes) AS avgvotes 
FROM 
    servers a 
INNER JOIN 
    (
     SELECT 
      aa.server, 
      aa.votes 
     FROM 
      stats aa 
     LEFT JOIN stats bb ON 
      aa.server = bb.server AND 
      aa.time < bb.time 
     GROUP BY 
      aa.time 
     HAVING 
      COUNT(*) < 24 
    ) b ON a.id = b.server 
GROUP BY 
    a.id 
+0

To zapytanie jest bardzo powolne z jakiegoś powodu. Wykonuję go i pozwalam mu siedzieć przez kilka minut, a to się nie kończy. Nie potrzebuję ekstremalnej prędkości, ale to zdecydowanie za długo. – fruitcup

+0

@DrAgonmoray Okay, rozumiem. Spróbuję lepszego rozwiązania. Jaka jest twoja struktura indeksowania? Czy masz ustawiony indeks w polu 'time'? –

+0

Nie Nie mam ustawionego indeksu w polu czasu, jednak mogę dodawać/usuwać indeksy zgodnie z wymaganiami rozwiązania. Obecnie nie zdefiniowano indeksów dla statystyk. – fruitcup

Powiązane problemy