Grupowanie wyników

Dosyć często spotykam się z pytaniem, jak napisać grupowanie wyników, czyli np. mamy produkty które należą do kategorii i jak wyświetlić wszystkie kategorie wraz z należącymi do nich produktami. Początkujący programiści albo nie wiedzą jak to zrobić, albo robią to przy użyciu wielu zagnieżdżonych zapytań, co niezbyt dobrze wpływa na wydajność aplikacji.

Rozwiązanie problemu opiszę na dwóch przykładach. Działanie obydwu będzie bardzo podobne: najpierw jednym zapytaniem pobieramy wszystkie dane, następnie w php grupujemy je według potrzeb i na koniec tworzymi stronę wynikową.

Przykład 1
Mamy kategorie. Do każdej kategorii może należeć kilka produktów. Chcemy wyświetlić wszystkie kategorie wraz z produktami jakie do nich należą.

Zaczniemy od utworzenia tabel i wypełnienia ich danymi:

-- utworzenie tabel
CREATE TABLE `category` (
  `ID` int unsigned NOT NULL auto_increment,
  `NAME` varchar(32) NOT NULL ,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `NAME` (`NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `product` (
  `ID` int unsigned NOT NULL auto_increment,
  `FK_CATEGORY` int unsigned NOT NULL ,
  `NAME` varchar(32) NOT NULL ,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `NAME` (`NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- zasilenie tabel danymi
insert  into `category` (`ID`,`NAME`) values 
(1,'kategoria 1'),
(2,'kategoria 2'),
(3,'kategoria 3'),
(4,'kategoria bez produktów');

insert  into `product` (`ID`,`FK_CATEGORY`,`NAME`) values 
(1,1,'Produkt1 z kategori 1'),
(2,1,'Produkt2 z kategori 1'),
(3,2,'Produkt1 z kategori 2'),
(4,3,'Produkt1 z kategori 3'),
(5,3,'Produkt2 z kategori 3'),
(6,3,'Produkt3 z kategori 3');

Teraz skrypt, który pobierze dane z bazy i utworzy odpowiednią tablicę:

mysql_connect('localhost','root','') or die(mysql_error());
mysql_selectdb('test') or die(mysql_error());

//pobranie wszystkich danych
$sql = 'select p.NAME PNAME,p.ID PID, c.ID CID, c.NAME CNAME from category c '.
  'left join product p on p.FK_CATEGORY = c.ID '.
  'order by c.NAME asc, p.NAME asc'; //sortujemy po nazwie kategorii i po nazwie produktu

//jeśli chcemy pobrać tylko kategorie z produktami to zmieniamy trochę zapytanie:
//$sql = 'select p.NAME PNAME,p.ID PID, c.ID CID, c.NAME CNAME from product p '.
//  'left join category c on p.FK_CATEGORY = c.ID '.
//  'order by c.NAME asc, p.NAME asc'; //sortujemy po nazwie kategorii i po nazwie produktu

$res = mysql_query($sql) or die(mysql_error());

//najpierw pozyskamy niezbędne dane
$categories = array();
while ($row = mysql_fetch_array($res)){
  $cid = $row['CID'];
  
  //jeśli nie było jeszcze danej kategorii, to ją tworzymy
  if (!isset($categories[$cid]))
    $categories[$cid] = array('name' => $row['CNAME'], 'products' => array());
  
  //dodajemy do kategorii kolejne produkty
  if (!empty($row['PNAME'])) //jeśli istnieje produkt
    $categories[$cid]['products'][] = array('name' => $row['PNAME'], 'id'=>$row['PID']);
}
//print_r($categories); //do obejrzenia jak wygląda wygenerowana tablica

W wyniku tego skryptu otrzymamy taką tablicę:

Array
(
    [1] => Array
        (
            [name] => kategoria 1
            [products] => Array
                (
                    [0] => Array
                        (
                            [name] => Produkt1 z kategori 1
                            [id] => 1
                        )

                    [1] => Array
                        (
                            [name] => Produkt2 z kategori 1
                            [id] => 2
                        )

                )

        )

    [2] => Array
        (
            [name] => kategoria 2
            [products] => Array
                (
                    [0] => Array
                        (
                            [name] => Produkt1 z kategori 2
                            [id] => 3
                        )

                )

        )

    [3] => Array
        (
            [name] => kategoria 3
            [products] => Array
                (
                    [0] => Array
                        (
                            [name] => Produkt1 z kategori 3
                            [id] => 4
                        )

                    [1] => Array
                        (
                            [name] => Produkt2 z kategori 3
                            [id] => 5
                        )

                    [2] => Array
                        (
                            [name] => Produkt3 z kategori 3
                            [id] => 6
                        )

                )

        )

    [4] => Array
        (
            [name] => kategoria bez produktów
            [products] => Array
                (
                )

        )

)

Jedyne co należy teraz zrobić to na podstawie uzyskanej tablicy wygenerować stronę:

//a teraz wygenerujemy stronę na podstawie uzyskanych danych
echo '<ul>';
foreach ($categories as $idCat => $category){ //petla, która leci po kategoriach
  echo '<li>'.$category['name'].' (liczba produktow: '.count($category['products']).')<ul>';
  foreach ($category['products'] as $product){ //pętla, która leci po produktach w kategorii
    echo '<li>Produkt o nazwie <b>'.$product['name'].'</b> i id <b>'.$product['id'].'</b></li>';
  }
  echo '</ul></li>';
}
echo '</ul>';

Oto wynik:

  • kategoria 1 (liczba produktow: 2)
    • Produkt o nazwie Produkt1 z kategori 1 i id 1
    • Produkt o nazwie Produkt2 z kategori 1 i id 2
  • kategoria 2 (liczba produktow: 1)
    • Produkt o nazwie Produkt1 z kategori 2 i id 3
  • kategoria 3 (liczba produktow: 3)
    • Produkt o nazwie Produkt1 z kategori 3 i id 4
    • Produkt o nazwie Produkt2 z kategori 3 i id 5
    • Produkt o nazwie Produkt3 z kategori 3 i id 6
  • kategoria bez produktów (liczba produktow: 0)

    Przykład 2
    Mamy listę miast. Chcemy wyświetlić te miasta pogrupowane względem liter, od których się zaczynają.
    Przykładowe dane:

    -- utworzenie tabel
    CREATE TABLE `city` (
      `ID` int unsigned NOT NULL auto_increment,
      `NAME` varchar(32) NOT NULL ,
      PRIMARY KEY  (`ID`),
      UNIQUE KEY `NAME` (`NAME`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    -- zasilenie tabel danymi
    insert  into `city` (`ID`,`NAME`) values 
    (1,'Białystok'),
    (2,'Olsztyn'),
    (3,'Białowieża'),
    (4,'Augustów'),
    (5,'Warszawa'),
    (6,'Olecko'),
    (7,'Wrocław');

    Skrypt, który pobierze wszystkie miasta i utworzy właściwą tablicę:

    mysql_connect('localhost','root','') or die(mysql_error());
    mysql_selectdb('test') or die(mysql_error());
    
    //pobranie wszystkich danych
    $sql = 'select * from city '.
      'order by NAME asc'; //sortujemy po nazwie
    $res = mysql_query($sql) or die(mysql_error());
    
    //najpierw pozyskamy niezbędne dane
    $cities = array();
    while ($row = mysql_fetch_array($res)){
      $name = $row['NAME'];
    
      //jeśli będziemy w bazie mieli miasta z nazwami zaczynającymi
      //się od polskich liter należy użyć modułu mb_string
      $firstLetter = strtoupper(substr($name,0,1));
      
      //jeśli nie było jeszcze danej litery, to ją tworzymy
      if (!isset($cities[$firstLetter]))
        $cities[$firstLetter] = array();
      
      //dodajemy kolejne miasta do danej litery
      $cities[$firstLetter][] = $name;
    }
    //print_r($cities); //do obejrzenia jak wygląda wygenerowana tablica

    W wyniku otrzymamy tablicę:

    Array
    (
        [A] => Array
            (
                [0] => Augustów
            )
    
        [B] => Array
            (
                [0] => Białowieża
                [1] => Białystok
            )
    
        [O] => Array
            (
                [0] => Olecko
                [1] => Olsztyn
            )
    
        [W] => Array
            (
                [0] => Warszawa
                [1] => Wrocław
            )
    
    )

    Teraz wystarczy już tylko dane wyświetlić:

    //a teraz wygenerujemy stronę na podstawie uzyskanych danych
    echo '<ul>';
    foreach ($cities as $letter => $_cities){ //petla, która leci po literach
      echo '<li>'.$letter.'<ul>';
      foreach ($_cities as $city){ //pętla, która leci po miastach w literze
        echo '<li>'.$city.'</li>';
      }
      echo '</ul></li>';
    }
    echo '</ul>';

    Oto wynik:

    • A
      • Augustów
    • B
      • Białowieża
      • Białystok
    • O
      • Olecko
      • Olsztyn
    • W
      • Warszawa
      • Wrocław



    Edit
    Na uwagę matipl zmodyfikowałem przykład 1 o kod kładący większy nacisk na udział bazy danych w obliczeniach:

    <?php
    mysql_connect('localhost','root','') or die(mysql_error());
    mysql_selectdb('test') or die(mysql_error());
    
    //pobranie wszystkich danych i ich pogrupowanie
    $sql = 'select GROUP_CONCAT(p.NAME order by p.NAME asc SEPARATOR \';;;\') PRODUCTS, '. //łączymy produkty poprzez ;;;. Po tym będziemy później dzielić
      'c.ID CID, c.NAME CNAME from category c '.
      'left join product p on p.FK_CATEGORY = c.ID '.
      'group by c.ID '.
      'order by c.NAME asc'; //sortujemy po nazwie kategorii
    
    $res = mysql_query($sql) or die(mysql_error());
    
    //najpierw pozyskamy niezbędne dane
    //teraz dostaniemy tylko tyle rekordów ile jest kategorii
    $categories = array();
    while ($row = mysql_fetch_array($res)){
      $cid = $row['CID'];
      
      $categories[$cid] = array('name' => $row['CNAME'], 'products' => array());
      
      if (!empty($row['PRODUCTS'])) { //jesli są produkty dla tej kategorii
        //rozbicie tekstu z produktami na tablicę produktów 
        $products = explode(';;;',$row['PRODUCTS']);
        foreach ($products as $product){
          $categories[$cid]['products'][] = $product;
        }
      }
    }
    //print_r($categories); //do obejrzenia jak wygląda wygenerowana tablica
    
    //a teraz wygenerujemy stronę na podstawie uzyskanych danych
    echo '<ul>';
    foreach ($categories as $idCat => $category){ //petla, która leci po kategoriach
      echo '<li>'.$category['name'].'<ul>';
      foreach ($category['products'] as $product){ //pętla, która leci po produktach w kategorii
        echo '<li>'.$product.'</li>';
      }
      echo '</ul></li>';
    }
    echo '</ul>';?>

    Powiedzmy, że teraz baza nie będzie nam zwracała nadmiarych danych w postaci powtarzanych informacji o kategoriach.
    Do złączenia produktów użyłem ;;;. Jeśli jakimś cudem w nazwie waszego produktu pojawi się też ;;; to na wyniku otrzymacie o jeden produkt więcej, powstały z podziału tego "felernego" produktu na dwa. Miejcie to na uwadze
    Jako ćwiczenie dla Was pozostawiam zmianę przykładu 2.

    Komentarze

     

    2009-03-21 01:24 gość_LBO

    Hej nosporze

    Na Twoim miejscu przykład zaimplementowałbym przy użyciu PDO. Posiada ono bardzo fajny modyfikator PDO::FETCH_GROUP.
    Z jego pomocą byłoby znacznie mniej kodu, a także mógłbyś pokazać przeniesienie części odpowiedzialności na bazę (litery w pierwszej kolumnie).

    Pozdrawiam

    2009-03-21 10:17 nospor

    @LBO po części masz rację.
    Wolałem jednak pokazać to od podstaw, by osoby początkujące zobaczyły jak to się robi tak na "sucho". Pozatym wielu "czytelników" nie używa PDO.

    2009-03-28 08:49 gość_m4tryk5

    A tu użyjemy funkcji substring i zwalimy troszkę większe obciążenie na bazę danych.
    <? 
    mysql_connect('localhost','root','') or die(mysql_error()); 
    mysql_selectdb('test') or die(mysql_error()); 
    
    //pobranie wszystkich danych 
    $sql = 'select ID, NAME, substring(NAME,1,1) as FIRSTLETTER from city order by NAME asc'; //sortujemy po nazwie 
    $res = mysql_query($sql) or die(mysql_error()); 
    
    //najpierw pozyskamy niezbędne dane 
    $cities = array(); 
    while ($row = mysql_fetch_array($res)){ 
      $name = $row['NAME']; 
    
      //jeśli będziemy w bazie mieli miasta z nazwami zaczynającymi 
      //się od polskich liter należy użyć modułu mb_string 
      $firstLetter = $row['FIRSTLETTER']; 
       
      //jeśli nie było jeszcze danej litery, to ją tworzymy 
      if (!isset($cities[$firstLetter])) 
        $cities[$firstLetter] = array(); 
       
      //dodajemy kolejne miasta do danej litery 
      $cities[$firstLetter][] = $name; 
    } 
    print_r($cities); //do obejrzenia jak wygląda wygenerowana tablica 
    ?>

    2009-03-28 13:49 nospor

    Eeeee tam, jakie to obciążenie pobrać pierwszą litere wyrazu
    Oczywiście Twój sposób jest równie dobry co i mój z małym ale:
    zapomniałeś z litery zrobić dużą

    2009-03-29 09:37 gość_m4tryk5

    Z tą dużą literą to fakt. Chciałem tylko pokazać, że niektóre rzeczy da się robić w SQL. Jakby ktoś się interesował Blind SQL Injection to substirng i benchmark są mu bardzo potrzebne (do tego jeszcze char i ord aby zobaczyć wielkość litery).

    2009-03-30 09:09 gość_matipl

    Że jak? Spodziewałem się, że zaprezentujesz rozwiązanie za pomocą SQL (group by), a nie prymitywne składanie w PHP z zastosowaniem jeszcze left join.
    Jeśli masz 10 produktów, wyłącznie 1-1 to spoko...Ale jak już to lepiej w 2 zapytaniach zrobić, jeśli logika ma być wyłącznie w PHP.

    2009-03-30 09:36 nospor

    Ale jak już to lepiej w 2 zapytaniach zrobić, jeśli logika ma być wyłącznie w PHP.

    Że jak? Hihi, zrób mi to na dwóch zapytaniach. Nawet jeśli jakimś cudem ci się uda (za bardzo nie wiem jak - nie myl dwóch zapytań od dwóch tekstów zapytań, z czego jeden jest wykonywany w pętli) to niby czemu to mam slużyć?
    ps: no można niby dwoma zapytaniami:
    jednym pobierasz wszystkie dane z jednej tabeli, drugim wszystkie dane z drugiej tabeli. W czym to ma być lepsze?
    ps2: chcąc zrobić to głównie na bazie, musiałbym sie bawić w CONCAT i temu podobne. Nie koniecznie byłoby to super fajne, szczególnie przy większej ilości informacji jakie chciałbyś w ten sposób uzyskać.
    Napisałem ten artykuł z myślą o osobach, które nagminnie stosują tutaj zapytania w pętli.

    2009-03-30 10:04 gość_matipl

    To za 2 zapytaniami, na odczepnego napisałem, że ostatecznie można jakoś w PHP to ująć, podobnie jak Ty to zrobiłeś...

    Mimo wszystko jak mówimy o grupowaniu, powinniśmy starać się jak najbardziej przerzucić logikę na bazę. Do pierwszego przykładu pasuje jak ulał...

    2009-03-30 10:07 nospor

    No ok, masz rację. Nie chciałem jednak zaciemniać kodu. Jak pisałem chodziło mi tu bardziej o pokazanie alternatywy dla x zapytań wykonywanych w pętli.
    Jednak ruszyło mnie sumienie po twoim komentarzu i podałem rozwiązanie operujące w dużej mierze na bazie danych. Mam nadzieję, iż teraz jest lepiej

    Nadal jednak nie jestem przekonany nad "wyższością" tego rozwiązania. Jeśli byśmy chcieli pobrać nie tylko nazwy produktów, ale też 10 innych pól z tabeli produktów, to rozwiązanie to staje się poprostu upierdliwe, gdyż zmusza nas do ciągłych CONCAT a później ich rozbijaniu w php.
    Rozwiązanie, z zostawieniem wszystkiego na php, jest przyjaźniejsze. Nie będziemy też tutaj wyświetlać milion rekordów by miało to mieć jakiś wpływa na wydajność.

    No chyba, że widzisz inny sposób na pogrupowanie i pobranie wszystkich danych, to chętnie posłucham.

    2009-04-16 16:04 gość_Sekwer

    Bardzo wartościowy artykuł. Przyznam, że kiedyś rozwiązywałem to w inny sposób, który bardziej obciążał bazę danych. Dzięki...

    2010-11-21 08:54 gość_Patryk

    Hej, a w jaki sposób wyciągnąć tylko X rekordów z danej kategorii? Czyli tworzymy normalnie drzewko, ale w tablicy products są tylko 5 rekordów (sortowanie według czasu dodania) - i tak w każdej tablicy...

    2010-11-22 07:57 nospor

    Musisz zrobić podzapytanie z głównego, a następnie pobierać tylko x rekordów z tego podzapytania. Podam ci inny przykład, z którego korzystam (chwilowo brak czasu)
    set @row = 0,@tmp=0;

    SELECT * from (
      SELECT *, IF( @tmp <> ID_KAT, @row := 1, @row := @row + 1 ) AS row_number, @tmp := ID_KAT from table) t
    WHERE t.row_number <= 5;

    ID_KAT określa jakąś rzecz po której "grupujesz".

    2010-11-22 17:19 gość_Patryk

    Houston mamy problem! Zapytanie nie spełnia warunku "<= 5", otóż zwraca więcej jak 5 wyników.
    set @row = 0,@tmp=0;
    SELECT * from (
      SELECT *, IF( @tmp <> forum_id, @row := 1, @row := @row + 1 ) AS row_number, @tmp := forum_id from phpbb_topics) t
    WHERE t.forum_id in(30,31) and t.row_number <= 5;


    Zapytanie związane z pobraniem tematów z kategorii (forum_id) o identyfikatorze 30,31.
    Zrzut: http://img228.imageshack.us/img228/4305/beztytuumjm.png - Wszystkie rekordy dla forum_id = 30, i tylko jeden dla 31 (bo tylko jeden temat mam w dziale 31)

    2010-11-22 19:58 nospor

    1) IN daj w podzapatyniu. Szanuj mysql swego jak siebie samego
    2) Zapomniałem dodać sortowania
    set @row = 0,@tmp=0; 
    SELECT * from ( 
      SELECT *, IF( @tmp <> forum_id, @row := 1, @row := @row + 1 ) AS row_number, @tmp := forum_id from phpbb_topics where forum_id in(30,31)  order by forum_id asc) t 
    WHERE t.row_number <= 5;

    2010-11-23 13:21 gość_Patryk

    Świetnie, dziękuje za pomoc nospor. Pozdrawiam.

    2010-12-03 14:08 gość_adam

    Może się komuś sprzyda.

    Pierwsze zapytanie ma jedną wadę, otóż subselect musi pobrać wszystkie rekordy i ponumerować od 1 do n, dopiero w selekcie zewnętrznym są brane interesujące nas wyniki w naszym przypadku do 2 odpowiedzi na posta.

    mysql> SELECT forumId, parentForumId from (
    -> SELECT forumId, parentForumId, IF( @tmp <> parentForumId, @row := 1, @row := @row + 1 ) AS row_number, @tmp := parentForumId from `groupForum` where parentForumId in(30,31) order by parentForumId asc) t
    -> WHERE t.row_number <= 2;
    +---------+---------------+
    | forumId | parentForumId |
    +---------+---------------+
    | 106 | 30 |
    | 134 | 30 |
    | 632 | 31 |
    | 638 | 31 |
    +---------+---------------+
    4 rows in set (0.00 sec)

    mysql> set @row = 0,@tmp=0;
    Query OK, 0 rows affected (0.00 sec)

    Wynik można uzyskać przenosząc liczenie odpowiedzi do where, zysk mamy taki że jeżeli @row przekracza 2 pomijane są następne rekordy aż napotkamy następny post i liczenie zacznie się od nowa

    mysql> SELECT forumId, @tmp := parentForumId as parentForumId from `groupForum`
    -> where parentForumId in(30,31) and IF( @tmp <> parentForumId, @row := 1, @row := @row + 1 ) <=2 order by parentForumId asc;
    +---------+---------------+
    | forumId | parentForumId |
    +---------+---------------+
    | 106 | 30 |
    | 134 | 30 |
    | 632 | 31 |
    | 638 | 31 |
    +---------+---------------+
    4 rows in set (0.00 sec)

    Ten sposób można wykorzystać zamiast GROUP BY lub DISTINCT i całkiem dobrze sobie to radzi

    2011-01-20 19:36 gość_oj

    A jak to zrobić na PDO?

    2011-01-20 20:23 nospor

    Dokładnie tak samo. Tylko zamiast mysql_query używać składni PDO

    2011-06-06 10:31 gość_Andrzej

    a co jak mam jedną tabelę
    id|strona|tytul|id_rodzica
    1|strona1.html|Tytul strony 1|0
    2|strona2.html|Tytul strony 2|0
    3|strona3.html|Tytul strony 3|0
    4|strona4.html|Tytul strony 4|2
    5|strona5.html|Tytul strony 5|0


    i chce taki efekt uzyskać w kodzie wynikowym:

    <ul>
    <li><a href="strona1.html">Tytul strony 1</a></li>
    <li><a href="strona2.html">Tytul strony 2</a>
    <ul>
    <li><a href="strona4.html">Tytul strony 4</a></li>
    </ul>
    </li>
    <li><a href="strona3.html">Tytul strony 3</a>
    <ul>
    <li><a href="strona5.html">Tytul strony 5</a></li>
    </ul>
    </li>
    <ul>


    Mogę prosić o podpowiedz jak za pomocą tego kodu ze strony to osiągnąć?
    Ten przykład z artykułu pokazuje działanie na 2 tabelach a ja mam jedną.

    2011-06-06 11:23 nospor

    Robisz dokładnie tak samo, z tym że Twoją drugą tablicą jest pierwsza
    select t2.strona t2strona,t2.id t2id,t2.tytul t2tytul, t1.id t1id, t1.strona t1strona, t1.tytul t1tytul from tabela t1 
      left join tabela t2 on t2.id_rodzica = t1.id 
      where t1.id_rodzica=0 order by t1.tytul asc, t2.tytul asc

    2011-06-06 22:25 gość_Artur

    OOO w końcu znalazłem coś co prawie idealnie robi to co bym chciał, dlaczego w przykładzie który podałeś jak mam tylko jeden id_rodzica większy od 0 to wyświetla się dobrze a jak już dam tak jak poniżej to nie mam id 4 w drzewie, czemu tak się dzieje?
    1|strona1.html|Tytul strony 1|0
    2|strona2.html|Tytul strony 2|1
    3|strona3.html|Tytul strony 3|0
    4|strona4.html|Tytul strony 4|2
    5|strona5.html|Tytul strony 5|0

    $sql = 'select t2.strona t2strona,t2.id t2id,t2.tytul t2tytul, t1.id t1id, t1.strona t1strona, t1.tytul t1tytul from tabela t1 
      left join tabela t2 on t2.id_rodzica = t1.id 
      where t1.id_rodzica=0 order by t1.tytul asc, t2.tytul asc'; //sortujemy po nazwie kategorii i po nazwie produktu
    
    $res = mysql_query($sql) or die(mysql_error());
    
    //najpierw pozyskamy niezbędne dane
    $categories = array();
    while ($row = mysql_fetch_array($res)){
      $cid = $row['t1id'];
      
      //jeśli nie było jeszcze danej kategorii, to ją tworzymy
      if (!isset($categories[$cid]))
        $categories[$cid] = array('tytul' => $row['t1tytul'], 'tytul' => $row['t1tytul'], 'tytul' => $row['t1tytul'], 'child_nodes' => array());
      
      //dodajemy do kategorii kolejne produkty
      if (!empty($row['t2tytul'])) //jeśli istnieje produkt
        $categories[$cid]['child_nodes'][] = array('tytul' => $row['t2tytul'], 'id'=>$row['t2id']);

    to dostaje:

    Array
    (
        [1] => Array
            (
                [tytul] => Tytuł 1
                [child_nodes] => Array
                    (
                        [0] => Array
                            (
                                [tytul] => Tytuł 2
                                [id] => 2
                            )
                    )
            )
        [3] => Array
            (
                [tytul] => Tytuł 3
                [child_nodes] => Array
                    (
                    )
            )
        [5] => Array
            (
                [tytul] => Tytuł 5
                [child_nodes] => Array
                    (
                    )
            )
    )

    2011-06-07 06:59 nospor

    Bo to, co ci tu pokazałem, działa przy założeniu, że jest tylko jeden poziom dzieci. Ty natomiast u siebie masz rodzica, dziecko i wnuczka: 1->2->4

    2011-06-07 07:43 gość_Artur

    rano na to też wpadłem, a jak duży to problem rozbudować do powiedzmy 5 poziomów?
    jeszcze jedno: dodałem do tej przykładowej tabeli pole 'rola', i do where and t1.rola='wartosc' ale to mi chyba wyszuka z rola ='wartosc' dla 1 poziomu, a co zrobic aby szukał we wszystkich poziomach?

    2011-06-07 08:12 nospor

    and t2.rola='wartosc'

    Co do pięciu poziomów to trzeba by to już w ogóle inaczej napisać oraz zaprząc do tego prawdopodobnie rekurencję.

    2011-09-21 23:47 gość_Łukasz Tkacz

    A co, jeżeli pobieram te same dane z jednej tabeli mam sporo pól (wymienianie wszystkiego nie ma sensu). Poziom ogranicza się do rodzica i dzieci, wygląda to tak:

    SELECT tp.*, tch.* 
    FROM table1 tp 
    LEFT JOIN table1 tch ON tch.parent = tp.id
    ORDER BY tp.name ASC, tch.name ASC


    Wyniki wydają się zgadzać, ale kolumny nazywają się tak samo, a więc w php nic rozdzielić nie mogę. Czy dla pobierania wszystkich wyników mogę jakąś "z automatu" zmodyfikować nazwy kolumn jednej z nich, np. dopisek "parent_"? Jak do tej pory realizowałem to przez dwa zapytania - z pierwsze w php tworzyło tablicę z kategoriami, po drugim jest ona wypełniana danymi.

    2011-09-22 08:41 nospor

    Musisz tworzyć aliasy, np
    ...... tch.pole1 TCHPOLE1, tch.pole2 TCHPOLE2,.....

    2011-09-22 10:21 gość_Łukasz Tkacz

    No to mam dylemacik... w sumie wynikający z lenistwa. Gdy jest sporo pól, to nadzwyczajnie nie che mi się czegoś takiego robić. Poza tym dochodzi drugi aspekt przy sprawdzeniu z EXPLAIN - gdy mam dwa zapytania oba używają WHERE (pole parent, indeks), oba są sortowane. Gdy mam jedno zapytanie, dodatkowo tworzona jest tablica tymczasowa. Sumarycznie czas jednego zapytania jest jednak krótszy niż tamtych dwóch.
    No to nic, na coś będę musiał się zdecydować, dziękuję za informację i interesujący wpis.

    2011-09-22 10:39 nospor

    W Twoim zapytaniu brakuje jeszcze jednej ważnej rzeczy. Teraz pobierasz kilkukrotnie te same dane z pierwszej i z drugiej tabeli. Z pierwszej tabeli powinieneś brać tylko te dane, które nie mają rodzica, czyli coś takiego:
    SELECT tp.*, tch.*  
    FROM table1 tp  
    LEFT JOIN table1 tch ON tch.parent = tp.id 
    where tp.parent=0
    ORDER BY tp.name ASC, tch.name ASC

    założyłem, że gdy nie ma rodzica to jest ustawione 0.

    Co do dużej ilości aliasów - no niestety, tego się nie przeskoczy. Lepiej jednak raz to napisać i mieć szybki sql niż w pętli zażynać bazę.

    2011-12-08 16:10 gość_Rafik89

    Dzięki, bardzo mi pomogłeś, nie chciałem umieszczać już zapytania w zapytaniu

    2017-11-10 09:38 gość_Piotr

    A jeśli do tabeli z produktami dodamy pole id_skladnik i utworzymy kolejną tabele skladniki: id, id_skladnik, skladnik_name,
    to czy możemy do zapytania z 1 przykładu dodać kolejnego left join'a? Czy to ma prawo na zadziałać?
    left join skladnik s on s.ID_SKLADNIK= p.ID_SKLADNIK '

    i następnie pobrać składniki:
    if (!empty($row['SKLADNIKName'])) //jeśli istnieje skladnik
        $categories[$cid]['skladniki'][] = array(.....);
    }

    Czy raczej powinno to już być rozbite na dwa osobne zapytania.?

    2017-11-10 10:51 nospor

    @Piotr powinno zadzialac. Tylko w kodzie co pokazales to bedziesz mial w wyniku skladniki przypisane do kategorii. Jesli o to ci chodzilo to ok

    2017-11-10 11:06 gość_Piotr

    Przepraszam, pomyliłem się.
    is_skladnik powinien być w tabeli 'category' a nie 'product'.
    Czyli drugie powiązanie jest między tabelą category oraz skladniki.
    I tu niestety gdzieś mam błąd, bo jeżeli w danej kategorii mam jeden składnik i dwa produkty, to tablica wyświetla mi również dwa razy składnik.

    2017-11-10 11:11 gość_Piotr

    Pomijając zatem zapytanie, mam:
    //jeśli nie było jeszcze danej kategorii, to ją tworzymy
      if (!isset($categories[$cid]))
        $categories[$cid] = array('name' => $row['CNAME'], 'products' => array(), 'skladniki'=>array());
      
      //dodajemy do kategorii kolejne produkty
      if (!empty($row['PNAME'])) //jeśli istnieje produkt
        $categories[$cid]['products'][] = array('name' => $row['PNAME'], 'id'=>$row['PID']);
    
    //dodajemy do kategorii kolejne skladniki
      if (!empty($row['SKLADNIKNAME'])) //jeśli istnieje produkt
        $categories[$cid]['skladniki'][] = array('name' => $row['SKLADNIKNAME']]);

    2017-11-10 13:29 nospor

    Musisz zrobic dokladnie taki sam myk jak z kategoria. Spojrz, do tablicy kategorii wkladam kategorie z indexem bedacym ID kategorii: $cid
    Teraz do skladnikow dodajesz poprostu elementy. Dodaj rowniez id skladnika i bedzie smigac

    $categories[$cid]['skladniki'][$sid] = array('name' => $row['SKLADNIKNAME']]);


    gdzie $sid to id skladnika

    2017-11-10 13:59 gość_Piotr

    Super, jest OK.
    Dzięki za pomoc.

    2017-12-15 18:03 gość_Piotr

    W temacie drugiego przykładu z miastami.
    A gdyby tak pole NAME zawierało więcej niż jedno miasto i rozdzielone np. przecinkami to bardzo to komplikuje sytuację?
    Gdzieś po drodze muszę zrobić?
    $miasto=explode (',', NAME);

    Dodaj komentarz

     

    Dostępne bbcode: b, u, i, url, code, php, css, html, sql, js

    Skrypty użytkowników

    1. Klasa obsługi szablo... Lirdoner
    2. Sekcje user76
    3. Klasa walidująca for... user76
    4. Licznik Gości online korey
    5. Form Builder Comandeer
    6. Dynamiczny licznik z... korey
    7. Captcha Comandeer