Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [SQL] Kategorie - optymalnie?
Forum PHP.pl > Forum > Bazy danych > MySQL
sniver
Pierwowzorem w kategoriach N zagłębienia dla mnie są duże serwisy takie jak allegro, amazon, beltal, alibaba, ebay itp.

Chciałem stworzyć coś co będzie działać w/g zasad jakie panują na allegro. Udało mi się zmontować następujące rzeczy:

Struktura tabeli w bazie danych:
  1. CREATE TABLE IF NOT EXISTS `categories` (
  2. `CAT_Id` int(11) NOT NULL AUTO_INCREMENT,
  3. `PARENT_Id` int(11) NOT NULL DEFAULT '0',
  4. `CAT_Name` varchar(155) NOT NULL,
  5. `CAT_Url` varchar(75) NOT NULL,
  6. `CAT_Description` varchar(255) NOT NULL,
  7. `CAT_Sort` int(11) NOT NULL DEFAULT '0',
  8. `CAT_Counter` int(11) NOT NULL,
  9. `CAT_Active` int(11) NOT NULL DEFAULT '1',
  10. PRIMARY KEY (`CAT_Id`),
  11. KEY `INDEX_GLOWNY` (`CAT_Id`,`PARENT_Id`,`CAT_Counter`,`CAT_Active`) USING BTREE,
  12. KEY `SORT` (`PARENT_Id`,`CAT_Sort`)
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC AUTO_INCREMENT=106 ;



Oraz zapytanie które obsługuje:
  1. SELECT
  2. `CAT_Id` AS `id`,
  3. `PARENT_Id` AS `parent`,
  4. `CAT_Name` AS `name`,
  5. `CAT_Url` AS `url`,
  6. `CAT_Counter` AS `licznik`
  7.  
  8. FROM
  9. `categories` AS `c`
  10.  
  11. WHERE
  12. (
  13. (`c`.`CAT_Active` = 1)
  14. AND (`c`.`CAT_Counter` > 0)
  15. )
  16. AND (
  17. (`c`.`CAT_Id` = {#ID})
  18. OR (`c`.`PARENT_Id` = {#ID})
  19. OR (`c`.`PARENT_Id` = (
  20. SELECT
  21. `c2`.`CAT_Id`
  22. FROM
  23. `categories` AS `c2`
  24. WHERE
  25. (`c2`.`CAT_Id` = (
  26. SELECT
  27. `c3`.`PARENT_Id`
  28. FROM
  29. `categories` AS `c3`
  30. WHERE
  31. (`c3`.`CAT_Id` = {#ID})
  32. LIMIT 1
  33. )
  34. )
  35. LIMIT 1
  36. )
  37. )
  38.  
  39. OR (`c`.`PARENT_Id` = (
  40. SELECT
  41. `c3`.`PARENT_Id`
  42. FROM
  43. `categories` AS `c3`
  44. WHERE
  45. (`c3`.`CAT_Id` = {#ID})
  46. LIMIT 1
  47. )
  48. )
  49. )
  50.  
  51. ORDER BY
  52. `c`.`PARENT_Id` ASC,
  53. `c`.`CAT_Sort` ASC


W miejscu gdzie występuje ciąg {#ID} należy wprowadzić wybraną kategorie...

Licznik jest wyliczany cyklicznie w cronie raz na kilka minut...

Póki co chcę się dowiedzieć co sądzicie o takim czymś - dobrze czy źle?
nospor
zastosowana przez ciebie metoda jest najgorsza ze wszystkich mozliwych. Nie wiem jakim cudem sie wzorowales na alegro winksmiley.jpg

poczytaj o strukturach drzewiastych w bazie danych, np. o drzewkach IP
sniver
wzorować czyt. efekt końcowy biggrin.gif
no bo jak sie tu inaczej wzorować - skoro ich kod nie jest dostępny smile.gif
Wykrywacz
Myślę że ktokolwiek kto będzie czekał na wynik tego zapytania zaśnie... Nie wiem na dobrą sprawę po co ci te zagnieżdżenia, skoro odbierasz od nich tylko 1 element. i to co ciekawe cały czas z tej samej tablicy.
Weź rozrysuj sobie to na kartce
phpion
Cytat(nospor @ 23.04.2010, 11:46:30 ) *
zastosowana przez ciebie metoda jest najgorsza ze wszystkich mozliwych. Nie wiem jakim cudem sie wzorowales na alegro winksmiley.jpg

Czy przypadkiem Allegro nie ma również struktury opartej o parent_id? Pobierając strukturę kategorii mamy identyfikator kategorii nadrzędnej oraz poziom zagłębienia czyli sugeruje to jednak rozwiązanie z parent_id. Zakładam oczywiście, że zwracane dane są w miarę wiernym odwzorowaniem bazy danych.
nospor
@phpion moze i ma parentId. Ale na pewno nie ma tylko parentId smile.gif
Ja tez uzywam parentId ale tylko pomocniczo, by przyspieszyc pewne rzeczy. Tutaj zas, w tym temacie, mamy tylko i wyłącznie parentId. Przeciez na czyms takim nie da sie napisac serwisu pokroju alegro - baza zostanie zarżnieta zywcem smile.gif
phpion
Może to dobry moment na debatę smile.gif sam jestem jednak zwolennikiem rozwiązania z parent_id. Podaj może przykład, w którym to rozwiązanie jest gorsze od Twojego/innego. Żeby było jasne: nie chcę na upartego bronić "mojej" teorii, chciałbym dojść do jak najlepszego rozwiązania.
nospor
ok, pierwszy lepszy przyklad z brzegu:
Masz produkt ktory nalezy do kategori o id 5.
Kategoria 5 ma rodzica 4, ten ma rodzica 3,ten ma rodzica 2, ten ma rodzica 1
Podaj mi teraz kod, którym pobierze wszystkich przodkow kategori 5, czyli 1/2/3/4/5
sniver
ale w tym przypadku ten kod ma tak nie działać smile.gif

gdyby miał tak działać odwołał bym sie prostymi 2 funkcjami które od "najmłodszego" dziecka idą w stronę najstarszego rodzica.. - tak by zbudować drzewo, to ma być lista winksmiley.jpg
phpion
dry.gif może coś bardziej życiowego? Nie spotkałem się jeszcze z koniecznością pobierania produktów należących do kategorii nadrzędnych, a jedynie podrzędnych hehe winksmiley.jpg Tak więc dla mnie osobiście ten problem nie istnieje.
sniver
Sam box na stronie z listą "aktualnych" kategorii mam zrobiony. Mogę na priv podesłać zainteresowanym adres i porównanie względem np. allegro (na priv ze względu na erotyczny typ serwisu który robię...)
nospor
@phpion "niezyciowy" problem?

http://allegro.pl/item996992105_aurora_g01...ka_4_wzory.html
Cytat
Biżuteria i Zegarki › Ozdoby do włosów › Gumki
myslisz ze ten Breadcrumb to czym jest? To jest wlasnie po kolei lista kategorii nadrzednych tongue.gif

Cytat
gdyby miał tak działać odwołał bym sie prostymi 2 funkcjami które od "najmłodszego" dziecka idą w stronę najstarszego rodzica.. - tak by zbudować drzewo
Super...zrob to i policz ile w rekurencji wykonales zapytan - wlasnie dlatego ten algorytm jest nieoptymalny.

Podalem tu tylko jeden z przykladow. Jest tego masa. Weźcie poczytajcie o strukturach drzewiastych w bazie danych, o np. drzewkach IP. Jak to zrobicie to wroccie i wtedy porozmawiamy, bo teraz nie widze wiekszego sensu smile.gif
sniver
znalazłem ciekawą opowieść na ten temat winksmiley.jpg

http://blog.mwojcik.pl/2008/02/17/drzewa-k...-php-metoda-ip/

mówiąc szczerze jest to ciekawe, ale w praktyce powiem czy lepiej sie to sprawuje od parent_id
nospor
Cytat
ale w praktyce powiem czy lepiej sie to sprawuje od parent_id
No nie wiem.... spytaj sie allegro. Napewno korzystają tylko z parentID winksmiley.jpg

Tak, drzewka IP powstaly wlasnie po to, by w praktyce sprawowaly się znacznie lepiej od zwyklego parentID.

Jak zaczynalem to sam przy pierwszym projekcie uzywalem tylko parentID. Nikt mi nie powidzial o niczym innym. Juz przy tym pierwszym projekcie okazalo sie, ze ma same wady taka struktura. Poprawialem wiec ją, modyfikowalem. Nie szukalem w necie zadnych rozwiązan. Po kilku latach okazalo sie, ze to do czego doprowadzily moje wlasne modyfikacje to ni mniej nie wiecej a wlasnie drzewko IP.
Tak wiem, skromny jestem smile.gif
sniver
hehehe...już przerobilem w głowie o co w tym chodzi biggrin.gif
do drzewa nie ma problemów - to się zgadzam :-)

wysyłam ci na priv moich kawałek wypocin biggrin.gif
phpion
Cytat(nospor @ 23.04.2010, 12:56:53 ) *
@phpion "niezyciowy" problem?

http://allegro.pl/item996992105_aurora_g01...ka_4_wzory.html
myslisz ze ten Breadcrumb to czym jest? To jest wlasnie po kolei lista kategorii nadrzednych tongue.gif

Aj, źle Cię zrozumiałem: myślałem, że chcesz wyświetlić produkty ze wszystkich kategorii nadrzędnych dla aktualnie przeglądanej stąd napisałem, że przykład lekko naciągany (zazwyczaj wyświetla się produkty ze wszystkich podkategorii, a nie "nadkategorii"). Breadcrumb można pobrać jedynie rekurencyjnie, zgadza się. Jednak można ścieżkę zapisać do cache i później odwoływać się do gotowych danych smile.gif
nospor
Cytat
Jednak można ścieżkę zapisać do cache i później odwoływać się do gotowych danych
Wszystko mozna zapisac do cache i olac wogole baze, nie zakladac indexow, nie dbac o optymalne zapytania - przeciez zawsze mozna zapisac do cache.... winksmiley.jpg
cache jednak sie czysci, niektorych zapytan nie zapiszesz do cache i wiele innych.

Jak tylko z parentID rozwiązesz liczbę produktów w kategoriach?
zakladam ze uzyjesz dodatkowe pole na liczbę produktów. No ale to pole też trzeba aktualizowac - znowu rekurencja. A jak nie daj boze trzeba bedzie zaktualizowac z jakiegos powodu te pole we wszystkich kategoriach.... winksmiley.jpg

Cytat
zazwyczaj wyświetla się produkty ze wszystkich podkategorii, a nie "nadkategorii"
Podsunales kolejny przyklad:
wyswietl mi proszę wszystkie produkty dla danej kategorii. Zauwaz, ze podam ci kategorię poczatkową, a produkty są przypisywane tylko do kategorii glęboko podrzędnych. Znowu cache? Ok, niech bedzie cache (juz mocno naciągane ale niech bedzie). Dodaj do tego wyszukiwanie. Znowu cache?
phpion
W obu przypadkach można zastosować to samo rozwiązanie: przypisując produkt do kategorii równocześnie przypisujemy go do wszystkich kategorii nadrzędnych czyli mając strukturę kategorii:
Kod
1
-- 1.1
-- 1.2
----1.2.1

i chcąc przypisać produkt do kategorii 1.2.1 zaznaczamy 1.2.1, 1.2 oraz 1.1. Można to rozwiązać ładnym JSem, który automatycznie zaznaczy całą ścieżkę do kategorii. Co nie zmienia jednak faktu, że pewnie przyjrzę się bliżej drzewkom IP. Może, bazując na doświadczeniu, podałbyś ich minusy?
nospor
Cytat
chcąc przypisać produkt do kategorii 1.2.1 zaznaczamy 1.2.1, 1.2 oraz 1.1. Można to rozwiązać ładnym JSem, który automatycznie zaznaczy całą ścieżkę do kategorii.
a co ma js do tego?
czyli ze co, w bazie tworzysz tabele: kategoria-produkt i robisz tyle wpisow dla danego produktu ile ma on kategorii nadrzędnych?
Mozna i tak, nie wnikam czy to dobre czy nie. Ale juz sam widzisz, ze nie korzystasz w tym momecne z samego parentID. Juz kombinujesz. I bardzo dobrze, to juz jest rozwiniecie metody "samo parentID".
A ja wlasnie to mowie przez caly czas: samo "parentID" jest cholernie nieoptymalne smile.gif

Cytat
Może, bazując na doświadczeniu, podałbyś ich minusy?
drzewek IP?
phpion
rolleyes.gif pewnie masz rację, aż sobie poczytam o tych drzewkach smile.gif

PS: Tak, minusy drzewek IP.

// Edit:
No i poczytałem tutaj:
http://blog.mwojcik.pl/2008/02/17/drzewa-k...-php-metoda-ip/
Wygląda to całkiem prosto i ciekawie, nie to co nested sets dry.gif. Co prawda "troszkę" naruszona jest 3. postać normalna, ale coś za coś.
nospor
minus drzewek IP... w porównaniu z parentID nie widze winksmiley.jpg

A juz powaznie:
ja drzewka IP uzywam lekko zmodyfikowane. jak juz pisalem sam na to wpadlem a dopiero potem sie okazalo ze do drzewko IP jest. Ja dodaje do tego jeszcze parentID oraz firstID.
firstID to id kategorii pierwszej. Czyli jak mam kategorie na poziomi 10 to firstID wskazuje na kategorię z poziomu 1. I tak jest niezaleznie od poziomu kategorii.

Zabawy jest trochę przy zarządzaniu: np. przy przenoszeniu kategorii trzeba pamietac, by zmodyfikowac rownież wpisy we wszystkich jej kategoriach podrzednych. Przy samym parentID juz bys tego nie mial. No ale trudno to uznac za wadę - trzeba to zrobic i tyle. przy kolejnych projektach jest tylko kopiuj-wklej i juz smile.gif

Żeby powaznie pomowic o wadach, musialbym uzywac innych struktur drzewiastych i miec jakies porownanie. A ja nie uzywam. Kiedys doszedlem do tego etapu struktury i jak do tej pory mi się sprawdzał. Jak kiedys na trafie na sytuacje ze nie bedzie sie sprawdzal - zmienie strukture smile.gif
Mchl
A jeśli ma się możliwość, to najlepiej z drzewkami itp radzi sobie to ustrojstwo winksmiley.jpg

http://openquery.com/graph-engine-mysql-mariadb-and-drizzle
sniver
ma ktoś gotowy kod który przerobi parent id na te ip biggrin.gif

jak byłby gotowy to nie musiał bym pisać biggrin.gif
aio
Cytat(sniver @ 23.04.2010, 16:21:11 ) *
ma ktoś gotowy kod który przerobi parent id na te ip biggrin.gif

jak byłby gotowy to nie musiał bym pisać biggrin.gif

query napisane w zasadzie do odczytu hierarchii 'na żywca' a nie jako konwerter ale może się przyda
INPUT
- nodes - tabela wejściowa (id, parent)
- kolumna sort - umozliwia własne sortowanie, aby tworzyć path z id najprościej potrzeba zamienić '(@S:=@S+1) sort' na 'id sort' (można wyrzucić całe sortowanie ale więcej tłumaczenia)
- w parametrach (setup) @XS oznacza separator ścieżki

OUTPUT
- kolumn hierarchy - path do danego id
- kolumna level - wiadomo

Kod
--
-- Solution 2 optimizing 1 - S2.1 - WINNER Solution
--
-- One Query Hierarchy Harvester (OQHH), by mba/aio(c)2010, SolutionID:OQHH/S2.1
--
--      Features:
--          - needs parent-child relation only
--          - no level nor other preordered data structure (i.e. left+right) needed
--          - sorting of sibling nodes (see notes below for info)
--          - speed optimization on preordered data
--          - produces level of depth of a node
--;
SELECT  id, parent, @L level, CONCAT(REPEAT('    ', @L - 1),CAST(id AS CHAR)) name, @H hierarchy
FROM    (
        SELECT  @H:='', @F:=0, @L:=0, -- internals
                @XS:='.', @XP:='0', -- xpath options
                @R:= LENGTH((IFNULL(@S,(SELECT MAX(id) FROM nodes)))) -- auto xpath size
        ) setup, nodes
WHERE   ((@L:=0) | (@H:='') | @P:=id) AND
        (
        SELECT  1
        FROM    nodes
        WHERE   (
                SELECT  1
                FROM    (
                        SELECT  id, parent, sort
                        FROM    (
                                    SELECT  id, parent, (@S:=@S+1) sort -- replace with 'id sort' to build xpath by id
                                    FROM    (SELECT @S:=0) init, nodes
                                    ORDER BY id DESC -- custom order: see notes on sorting techniques
                                ) sorting
                        ORDER BY id DESC
                        ) core -- optimization sequence 1: see notes
                WHERE   @P=id AND
                        ((@H:=CONCAT(REPEAT(@XP,@R-LENGTH(sort)),sort,IF(@H,@XS,''),@H)) | (@P:=parent) | (@L:=@L+1)) AND
                        NOT @P
                LIMIT 1 -- optimization trap 1: root found
                ) AND
                NOT @P
        LIMIT 1 -- optimization trap 2: root found
        )
ORDER BY hierarchy
--
-- Notes
-- * not used optimization trap 0 (see S1.2) - to allow sorting at lower level subquery
-- * optimization sequence 1 - assumes the child id > parent id, otherwise can slow down, needs core subquery to be ordered DESC
-- * CONCAT+REPEAT+LENGTH executes faster than LPAD
-- TODO
--
--;


O dziwo ten (po)twór przy dużej ilości rekordów jest 2x szybszy od obsługi drzewek Left Right - no i sortuje! winksmiley.jpg
Pozdro
cojack
Ja bym poprosił o źródło pochodzenia tego przykładu powyżej.
aio
Cytat(cojack @ 26.04.2010, 09:38:03 ) *
Ja bym poprosił o źródło pochodzenia tego przykładu powyżej.

źródło samo przyszło napisać przykład powyżej! Jak z czegoś korzystałem zawsze podawałem źródło!
pozdrawiam
cojack
Hmmm to ja jeszcze poproszę funkcje do przesuwania kategorii wraz z dziećmi, dodawania nowych kategorii, usuwania kategorii, aktualizacji kategorii i sprawdzimy wydajność Twojego działa.

Najchętniej to zobaczyłbym to wszystko w postaci pl/sql + widoki bym się za dużo bawić nie musiał przy portowaniu tego do postgresql.
aio
Cytat(cojack @ 26.04.2010, 16:53:42 ) *
Hmmm to ja jeszcze poproszę funkcje do przesuwania kategorii wraz z dziećmi, dodawania nowych kategorii, usuwania kategorii, aktualizacji kategorii i sprawdzimy wydajność Twojego działa.

Najchętniej to zobaczyłbym to wszystko w postaci pl/sql + widoki bym się za dużo bawić nie musiał przy portowaniu tego do postgresql.

Ty się chociaż zastanowiłeś nad swoim postem? Nie proś tylko sam napisz i wklej.
sniver
Napisałem bardzo byle jakąś procedure (można odpalić z cron'a) aby przerobić drzewo typu Parent Id na Drzewo IP, oto kod - może sie komuś przyda:

plik np. cron/Category-ParentId-to-TreeIp.php
  1. <?php
  2.  
  3. include_once 'db.class.php';
  4. cronDB::categoriesIP( $link );
  5.  
  6. ?>


plik db.class.php
  1. abstract
  2. class cronDBExp {
  3.  
  4.  
  5. protected function categoriesIP( $category, $id ) {
  6. try {
  7.  
  8. if( $category[$id]['isset'] ) {
  9. $parent = self::categoriesIP( $category, $category[$id]['parent'] );
  10. }
  11.  
  12. $parent.= $id;
  13. $parent.= '.';
  14.  
  15. return $parent;
  16. }
  17. catch( Exception $e ) {
  18. err($e);
  19. }
  20. }
  21.  
  22.  
  23. }
  24.  
  25.  
  26. /**
  27. * Klasa łączy modul z baza danych
  28. */
  29. class cronDB
  30. extends cronDBExp {
  31.  
  32.  
  33. /**
  34. * Zmienna przechowuje informacje o drzewie IP
  35. *
  36. * @var array
  37. */
  38. static public $categoryIp = array();
  39.  
  40.  
  41. /**
  42. * Budowanie drzewa IP
  43. *
  44. * @param object $link
  45. * @param int $catId
  46. */
  47. public function categoriesIP( $link ) {
  48. try {
  49.  
  50. $query = '
  51. SELECT
  52. `c`.`CAT_Id` AS `id`,
  53. `c`.`PARENT_Id` AS `parent`,
  54. (
  55. SELECT COUNT(1) FROM `categories` AS `c2` WHERE (`c2`.`CAT_Id` = `c`.`PARENT_Id`)
  56. ) AS `isset`
  57.  
  58. FROM
  59. `categories` AS `c`
  60. ';
  61.  
  62. if( !$result = db::__connect()->query( $query ) ) {
  63. throw new Exception('Blad bazy danych');
  64. }
  65.  
  66.  
  67. $category = array();
  68. while( $row = $result->fetch_assoc() ) {
  69. $category[ $row['id'] ] = array(
  70. 'parent' => $row['parent'],
  71. 'isset' => (bool)$row['isset']
  72. );
  73.  
  74. self::$categoryIp[$row['id']] = $row['id'];
  75. }
  76.  
  77.  
  78. foreach( self::$categoryIp as $key => $value ) {
  79. if( $category[$key]['isset'] ) {
  80. self::$categoryIp[$key] = parent::categoriesIP( $category, $category[$key]['parent'] );
  81. self::$categoryIp[$key].= $key;
  82. }
  83. }
  84.  
  85. $query = '
  86. UPDATE
  87. `categories` AS `c`
  88. SET
  89. `c`.`CAT_Ip` = '%s'
  90. WHERE
  91. `c`.`CAT_Id` = %d
  92. ';
  93.  
  94. foreach( self::$categoryIp as $key => $value ) {
  95. if( !db::__connect()->query( sprintf($query, self::$categoryIp[$key], $key) ) ) {
  96. throw new Exception('Blad bazy danych');
  97. }
  98. }
  99.  
  100. }
  101. catch( Exception $e ) {
  102. err($e);
  103. }
  104. }
  105.  
  106.  
  107. }
  108. ?>


elementy:
a) db::__connect() - jest znany zapewne większości osób z forum - to gotowiec z php.net - dotyczący połączenia z mysql'em z wzorcem singleton,
cool.gif funkcja err - wyświetla ewentualne błędy na ekranie (może zapisywać loga do pliku/bazy

Zapytanie Update zostawiem bez komentarza - nie zależy mi w tym przypadku na szybkości działania :-)

Jak mówiłem może sie komuś przyda, jest to tylko część kodu i może nie działać...winksmiley.jpg
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.