Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Grupowanie i kolejność wyników
Forum PHP.pl > Forum > Bazy danych > MySQL
johnson
Są takie oto rekordy (tabela uzytkownicy):

Kod
+---------+-----------+-------------+
|    id   | zespol_id |    imie     |
+---------+-----------+-------------+
|    1    |     3     |  Karolina   |
|    2    |     3     |  Mateusz    |
|    3    |     3     |  Magda      |
|    4    |     3     |  Andrzej    |
+---------+-----------+-------------+


Teraz grupując zapytaniem:
  1. SELECT count(*) AS liczba, imie, id
  2. FROM uzytkownicy GROUP BY zespol_id


otrzymujemy liczbę użytkowników w danym zespole oraz imię jednego użytkownika w tym teamie oraz jego id, problem polega jednak na tym, że w żaden sposób nie można wpłynąć na to, które imię się wyświetli

Czy można w jakiś sposób wpłynąć na to, które imię się wyświetli (np. posortować alfabetycznie i zwróci się pierwsze; dobrze by też było jakby zwracane było właściwe dla tego użytkownika id)questionmark.gif
amwoan
Takie zapytanie jest nielogiczne, a większość enginów chyba zwraca błąd, jeśli wybierane pola nie są uwzględnione w klauzuli GROUP BY lub nie jest użyta na nich żadna funkcja agregująca (na pewno mogę to jednak tylko powiedzieć o Postgre). Jeśli potrzebujesz jedynie liczby ludzi w zespole zostaw samo COUNT, jeśli również dane konkretnych osób, daj normalne zapytanie z klauzulą WHERE i odpowiednim id zespołu a potem zlicz zwrócone wiersze.
johnson
Może i wydaje się nielogiczne, ale ja potrzebuje uzysać taką odpowiedź.

Przypuśćmy, że istnieje dodatkowe pole 'wiek' i chcemy, uzyskać liczbę użytkowników w grupie i imię oraz wiek najstarszego użytkownika w grupie. Jak to osiągnąć?
amwoan
Cytat(johnson @ 2005-05-22 17:58:17)
Przypuśćmy, że istnieje dodatkowe pole 'wiek' i chcemy, uzyskać liczbę użytkowników w grupie i imię oraz wiek najstarszego użytkownika w grupie. Jak to osiągnąć?

Wiek najstarszego użytkownika uzyskasz używając MAX(wiek), natomiast co do pozostałych informacji... Powiem inaczej, a co jeśli w danej grupie jest więcej ludzi o takim samym wieku? Dlatego takie zapytanie jest nielogiczne. Jeżeli coś grupujesz, to aby uzyskać jednoznaczność, musisz albo wybierać pola, po których następuje grupowanie albo używać funkcji agregujących na pozostałych polach.

To co chcesz uzyskać można robić wieloma zapytaniami, bądź użyć MySQL'a w wersji minimum 4.1, gdzie można stosować zagnieżdżone SELECTy. Można też kombinować z tablicami tymczasowymi, dla starszych wersji. Poza tym nie widzę problemu, aby jednak po prostu zliczyć ilość zwróconych wierszy.
johnson
Cytat(amwoan @ 2005-05-22 16:40:29)
a co jeśli w danej grupie jest więcej ludzi o takim samym wieku?

To już nieważne, wybrało by jednego z nich i było by dobrze, problem w tym, żeby zrobić to w jednym zapytaniu sql, rekordów jest kilka tysięcy, wyniki trzeba będzie dzielić na strony, więc w celu uzyskania wyników wolałbym nic nie obliczać w php.

Pomyślę jeszcze jak to rozwiązać, ale jeśli ktoś mógłby coś zasugerować w tym temacie będę wdzięczny.
amwoan
Cytat(johnson @ 2005-05-22 19:15:54)
Cytat(amwoan @ 2005-05-22 16:40:29)
a co jeśli w danej grupie jest więcej ludzi o takim samym wieku?

To już nieważne...

To jest ważne, z punktu widzenia spójności danych. To, że w Twoim konkretnym przypadku może akurat nie jest, nie ma nic wspólnego ze standardami i tym, że ta "logika" musi się sprawdzać w każdej sytuacji.

Poza tym czegoś tu nie rozumiem chyba... Jeśli musisz dzielić wyniki (a więc jakiś LIMIT, jak mniemam) musisz znać ilość rekordów przed wykonaniem zapytania i potem dopiero pobierać dane porcjami (czyli musisz dwa razy wywołać SELECTa, raz z samym COUNT, drugi raz już z wszystkimi potrzebnymi danymi), natomiast, jeśli pobierasz w szystkie rekordy spełniające jakieś warunki, użyj po prostu mysql_num_rows (skoro wspomniałeś, iż jest to aplikacja php). Nie potrafię nic powiedzieć szczegółowo, bo też tak naprawdę nie wiem jakie dokładnie są wymogi dla Twojej aplikacji. Nie mniej istnieje jeszcze jedna "ciekawostka", która może Ci w czymś pomoże. Od MySQL w wersji 4.0 istnieje coś takiego jak SQL_CALC_FOUND_ROWS, które używa się przykładowo w następujący sposób:
  1. SELECT SQL_CALC_FOUND_ROWS *
  2. FROM tabela WHERE jakies_pole > 50 LIMIT 10 ;

Taka opcja powoduje, że obliczana jest całkowita liczba wierszy, która byłaby zwrócona w zapytaniu, gdyby nie ograniczający LIMIT. Po wykonaniu takiego zapytania zwracane są normalnie rekordy (odpowiednio limitowana ilość), natomiast, aby dostać całkowitą liczbę wszystkich spełniających warunek należy wykonać kolejne zapytanie do bazy:
  1. SELECT FOUND_ROWS();

To chyba tyle ode mnie, mam nadzieję, że to co napisałem jest w miarę zrozumiałe i w jakiś sposób pomoże. winksmiley.jpg
johnson
Spróbuje nieco bardziej zobrazować o co mi chodzi.

Jest sobie taka tabela:

id | domena | url | rate |
1 | www.php.pl | www.php.pl/strona2 | 90
2 | www.php.pl | www.php.pl/strona1 | 30
3 | www.php.pl | www.php.pl/strona4 | 70
4 | www.php.pl | www.php.pl/strona3 | 100
5 | forum.php.pl | forum.php.pl/strona3 | 20
6 | forum.php.pl | forum.php.pl/strona2 | 50
7 | forum.php.pl | forum.php.pl/strona1 | 60

Jakie pytanie należy zadać aby otrzymać 2 rekordy:
4 | www.php.pl | www.php.pl/strona3 | 100
7 | forum.php.pl | forum.php.pl/strona1 | 60

Czyli po jednym wyniku dla każdej domeny o największym rate.
Czy da się to uzyskać jednym zapytaniem sql?
Uwaga! Zapytanie musi być kompatybilne z mysql <= 4.0.xx
nocnyMark()
johnson
odnosnie Twojego ostatniego posta:

  1. SELECT domena, url, max(rate)
  2. FROM tabela GROUP BY domena


czy jest kompatybilne z mysql 4.0 i w dol, nie wiem, sprawdz

pole rate musi byc typu np. int, wtedy funkcja max wskazuje poprawnie najwieksza wartosc
johnson
Twoja odpowiedź jest niestety zbyt pochopna i nieprawidłowa, owszem zwróci największe 'rate', ale nie zwróci odpowiedniego 'id' i 'url' dla tego 'rate' a o to głównie tutaj chodzi, czyli niezależnie czy podasz max(rate) czy min(rate)zwracane w wynikach 'id' i 'url' będą takie same i nie będą odpowiadały rekordowi z max lub min rate.

Jak rozwiązać ten problem?
amwoan
Przykładowe rozwiązanie w php:
  1. <?php
  2. mysql_connect('127.0.0.1', 'user', 'pass');
  3.  
  4. $sql = 'CREATE TEMPORARY TABLE tmptbl SELECT t1.domena, MAX( t1.rate ) AS r
  5. FROM domeny AS t1
  6. GROUP BY t1.domena';
  7.  
  8. mysql_query($sql) or die(mysql_error());
  9.  
  10. $sql='SELECT t1.* FROM domeny AS t1 
  11. INNER JOIN tmptbl 
  12. ON t1.domena=tmptbl.domena AND t1.rate = tmptbl.r';
  13.  
  14. $res = mysql_query($sql);
  15.  
  16. echo '<pre>';
  17. while($row = mysql_fetch_assoc($res)){
  18. print_r($row);
  19. }
  20. echo '</pre>';
  21. ?>

Najpierw jest tworzona tymczasowa tabela z domeną i największym ratem dla niej. Później wykonywany jest właściwy SELECT, który wybiera pola z tabeli domeny (tak ją sobie nazwałem) połączonej za pomocą INNER JOINa z tymczasową tabelą tmptbl odpowiednio na polach `domena`<->`domena` oraz `rate`<->`r`. Musisz tylko pamiętać, że to zapytanie wybierze wszystkie strony z danej domeny, które posiadają rate równy największemu. Szczerze mówiąc, nie wiem w tej chwili czy istnieje prosty sposób wyfiltrowania w samym SQLu tylko pojedynczych wpisów dla danej domeny, ale to już można prosto załatwić w samym php.

To jest rozwiązanie dla MySQL 4.0 i niższej, od wersji 4.1 można używać zagnieżdżonych SELECTów i wtedy nie ma potrzeby tworzenia tymczasowej tabeli. Tak to się właśnie robi. Tabela tymczasowa istnieje tylko na czas konkretnego połączenia z bazą (czyli czas działania skryptu) oraz jest widoczna tylko dla danego połączenia (nie ma konfliktu nazw tabeli tymczasowej, nawet w przypadku kilku równoległych połączeń do bazy). Aha, jeszcze jeden wymóg, który tyczy się tylko wersji 4.0, to konieczność posiadania uprawnień do tworzenia tabel tymczasowych.

Jeszcze raz też przestrzegam przed używaniem pól nie poddanych funkcji agregującej, bądź nie będących częścią GROUP BY, kiedy właśnie ta klauzula jest używana.
FiDO
Cytat(johnson @ 2005-05-22 22:05:36)
Spróbuje nieco bardziej zobrazować o co mi chodzi.

id | domena | url | rate |
1 | www.php.pl | www.php.pl/strona2 | 90
2 | www.php.pl | www.php.pl/strona1 | 30
3 | www.php.pl | www.php.pl/strona4 | 70
4 | www.php.pl | www.php.pl/strona3 |  100
5 | forum.php.pl | forum.php.pl/strona3 | 20
6 | forum.php.pl | forum.php.pl/strona2 | 50
7 | forum.php.pl | forum.php.pl/strona1 | 60

Jakie pytanie należy zadać aby otrzymać 2 rekordy:
4 | www.php.pl | www.php.pl/strona3 |  100
7 | forum.php.pl | forum.php.pl/strona1 | 60

Da rade, aczkolwiek juz sobie nie uzyjesz funkcji grupujacych, bo rozwiazanie nie opiera sie na grupowaniu (widzialem gdzies w necie rozwiazanie tego z grupowaniem).
A wiec:
  1. SELECT t1.*
  2. FROM tabela t1
  3. LEFT JOIN tabela t2 ON (t1.domena = t2.domena AND t1.rate < t2.rate)
  4. WHERE t2.id IS NULL

Nie sprawdzilem, bo nie mam danych, ale chyba powinno byc dobrze. Taki calkiem przydatny trick.. tylko koniecznie trzeba zalozyc indeks na pole, po ktorym "grupujemy".



---edit:
Powalczylem chwile i doszedlem jak uruchomic tu jeszcze grupowanie...
  1. SELECT t1.*, COUNT(t3.article) ile, MIN(t3.price) min FROM shop t1
  2. LEFT JOIN shop t2 ON (t1.article = t2.article AND t1.price < t2.price)
  3. INNER JOIN shop t3 ON (t1.article = t3.article)
  4. WHERE t2.article IS NULL
  5. GROUP BY t3.article

To jest na innych danych, bo akurat taka tabele mialem pod reka do testow, ale sytuacja jest analogiczna do tej powyzszej.
Tabela i dane:
Kod
article | dealer | price
0001       A       3.45
0001       B       3.99
0002       A       10.99
0003       B       1.45
0003       C       1.69
0003       D       1.25
0004       D       19.95

Jak widac wyciagnelismy wiersz z najwyzsza cena dla danego artykulu, jednoczesnie umozliwiajac sobie wyciagniecie ilosci wierszy w tej grupie artykulow oraz najmniejszej ceny.
Wynik:
Kod
article dealer  price  ile      min
0001    B       3.99    2       3.45
0002    A       10.99   1       10.99
0003    C       1.69    3       1.25
0004    D       19.95   1       19.95
amwoan
Hm, ciekawy trick z tym LEFT JOINEM, muszę przyznać, że go nie znałem. smile.gif Człowiek się uczy całe życie. smile.gif

Natomiast co do dalszej części z grupowaniem to, pomijając, że też ciekawe rozwiązanie, na pewno nie zadziała na PostgreSQL (oraz innych enginach nie pozwalających na wyświetlanie pól bez użycia funkcji agregującej) oraz ma w sobie pewną wadę (możliwe, że do wyeliminowania). Tą wadą jest to, że jeśli istnieją np dwa wiersze z tą samą wartością odpowiadającą jednocześnie MIN, bądź MAX, to COUNT zwróci podwojoną ilość wierszy (im więcej będzie tych wierszy "źródłowych", tym większy będzie iloczyn). Dzieje się tak ze względu na to, że LEFT JOIN zwróci wszystkie wiersze z maksymalną, bądź minimalną wartością jakiegoś pola, a one następnie będą łączone poprzez INNER JOINA. Z tego wynika, że końcowa ilość wierszy, przed zgrupowaniem, jest równa "normalnej" ilości wierszy dla danej wartości pola, po którym następuje grupowanie (np omawiana domena) razy ilość wierszy spełniających warunek max czy min (pozostałe po dokonaniu LEFT JOIN). Nie wiem, czy nie zbyt zawile to opisałem, ale chyba można zrozumieć o co mi chodzi. tongue.gif Jak nie, a kogoś to interesuje, to postaram się wyjaśnić jaśniej, popierając może przykładem . smile.gif
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.