Optymalizacja przez odwrócenie relacji has_many - czy ma sens?

Witam,
Moje pytanie jest z pogranicza tematyki RoR i baz danych.
Mam dwa modele, które jako wstęp do rozumowania możnaby zapisać tak:

[code]class Listing < ActiveRecord::Base
has_many :items
end

class Item < ActiveRecord::Base
belongs_to :listing
end[/code]
A więc chodzi o to, w liście może być kilka itemów. Tyle, że:

  1. Niektóre itemy mogą się pojawiać na wielu listingach (tak, wiem, od tego jest habtm)
  2. Niektóre itemy mogą się pojawiać na tym samym listingu wielokrotnie
  3. Itemy w listingu muszą być w stałym porządku
  4. Zależy mi na szybkim dostępie listing->itemy (b. wiele zapytań)
  5. Nie potrzebuję w ogóle dostępu do listingu z poziomu itemu.

Jednym słowem, najprościej i optymalnie z punktu widzenia wydajności by było, gdyby to listing zawierał odwołania do itemów, a nie odwrotnie.
Można to prosto zrealizować tworząc w listingu wirtualny atrybut items na zewnątrz zachowujący się podobnie jak generowany w przypadku asocjacji, a przechowywany np. w stringu tworzonym metodą Array.join i rozbijanym Array.split.

I pewnie tak bym zrobił, ale chciałbym, żeby ktoś bardziej doświadczony utwierdził mnie w moim rozumowaniu lub obalił mój pomysł. A więc, czy z praktycznego punktu widzenia ma to sens?

Z góry dzięki!

O.

Ma to sens z punktu widzenia agresywnej optymalizacji przez denormalizację.
Chociaż niespecjalnie rozumiem “problem wielu zapytań”. Albo inaczej, to Ty nie rozumiesz i boisz się na zapas nieistniejącego problemu
– od tego są złączenia (SQL JOIN) i/lub wyciąganie wielu rekordów jednym zapytaniem. A wyciągania wielu rekordów jednym zapytaniem nie unikniesz (w Twoim rozwiązaniu oznaczać będzie ono WHERE ID IN (1,2,3)), co najwyżej możesz pozbyć się złączeń.

Zasadniczo habtm lub (najlepiej) has_many :through powinno rozwiązać Twoje problemy bezboleśnie, więc po co przepłacać. :wink:

Znaczy, pozbywanie się złączeń ma sens na etapie, na którym jest to konieczne (optymalizacja przez denormalizację właśnie, jako że JOIN przy bardzo dużych bazach jest kosztowną operacją). Tobie do niego daleko.

Ja bym to zoptymalizował jak już rzeczywiście będzie taka potrzeba. To co teraz chcesz zrobić to przedwczesna optymalizacja.

Skoro item może pojawić się w listingu wielokrotnie oraz być w wielu listingach to potrzebujesz relacji has_many :through:

[code=ruby]class Item

end

class Listing
has_many :items, :through => :item_listings, :order => “position”
has_many :item_listings
end

class ItemListing

id int

item_id int

listing_id int

position int

belongs_to :item
belongs_to :listing
end[/code]
Odnośnie wydajności to w czym jest problem ? Pamiętaj tylko żeby dodać indeksy i nie ładować za dużo obiektów AR jeśli ich nie potrzebujesz.

Dzięki za wyczerpujące odpowiedzi.
Być może to moje zboczenie zawodowe (na codzień pracuję w firmie zajmującej się naprawdę ogromnymi bazami) i rzeczywiście nie ma co optymalizować zanim to wszystko nie ruszy…
Rzeczywiście, relacji has_many :through jakoś nie brałem pod uwagę, ale z drugiej strony, czy wnosi ona coś ponad has_and_belongs_to_many? Złożoność w obu przypadkach wydaje mi się identyczna (czyt. tragiczna dla teoretyka), tyle, że mogę pilnować porządku itemów… Jest jeszcze jakaś różnica?

Korzystając z has_many :through możesz dodać dodatkowe pola w ItemListing, takie jak na przykład created_at. W habtm nie masz takiej możliwości.

Różnicę widać najlepiej jak weźmiesz pod uwagę takie sytuacje gdzie sama relacja nie wystarczy. Czasem możesz chcieć dopisać do niej jakieś atrybuty. Dobrym przykładem jest osobny model obsługujący np; subskrypcje, gromadzone ulubione, czy wszelkiego rodzaju członkostwa …

Rzeczywiście, możliwość dodania dodatkowych pól (np. streszczenia, thumbnaila, automatycznie wygenerowanych słów kluczowych) pojawiła mi się od razu. Dzięki jeszcze raz za podpowiedzi!
Jedyne, co zmieniłbym w przykładzie to nazwa tego dodatkowego modelu. Jakoś bardziej logiczny wydaje mi się ListingItem, bo przecież należy do Listingu i określa (ma) Item.

[quote=ohaleck]Dzięki za wyczerpujące odpowiedzi.
Być może to moje zboczenie zawodowe (na codzień pracuję w firmie zajmującej się naprawdę ogromnymi bazami) i rzeczywiście nie ma co optymalizować zanim to wszystko nie ruszy…[/quote]
Tak z ciekawości: z jak dużymi bazami masz do czynienia w pracy?

Dlaczego uważasz złożoność tego zapytania (rozumiem, że chodzi o coś w stylu “select items.* from items left join listing_items on (listing_items.item_id = items.id) where listing_items.listing_id = X order by position”) za “tragiczną dla teoretyka”?

Radarek: bo złączenia to bardzo kosztowne obliczeniowo operacje. Nie tak jak N+1 zapytań, ale generalnie jest to coś, czego eliminacja (przez denormalizację) jest praktycznie drugim, po położeniu indeksów, krokiem w mocnej optymalizacji bazy. Przynajmniej w świecie webdevelopmentu, gdzie każdy ułamek sekundy w zwróceniu danych się liczy.

Stąd zresztą popularność ruchu noSQL: skoro dla wysokiej wydajności wywalamy relacyjność, to po cholerę ją mieć in the first place?

[quote=radarek][quote=ohaleck]Dzięki za wyczerpujące odpowiedzi.
Być może to moje zboczenie zawodowe (na codzień pracuję w firmie zajmującej się naprawdę ogromnymi bazami) i rzeczywiście nie ma co optymalizować zanim to wszystko nie ruszy…[/quote]
Tak z ciekawości: z jak dużymi bazami masz do czynienia w pracy?

Dlaczego uważasz złożoność tego zapytania (rozumiem, że chodzi o coś w stylu “select items.* from items left join listing_items on (listing_items.item_id = items.id) where listing_items.listing_id = X order by position”) za “tragiczną dla teoretyka”?[/quote]
No właśnie… też mnie ciekawi jak duże te bazy… a jakby ktoś znał definicję ‘naprawdę ogromnej bazy’ to poproszę.

Natomiast co do tego zapytania to uważam to zapytanie za pomyłkę z dwóch powodów (biorąc pod uwagę optymalizację szybkości działania):

  • samo SELECT * to przeważnie pomyłka (chyba, że to się robi celowo, ale przeważnie nie potrzeba mieć wszystkich kolumn, często niepotrzebnie to zwiększa czas ściągania danych, a niektóre bazy potrafią to zoptymalizować jeśli wiedzą jakie kolumny są ściągane)
  • nie ma żadnego limit na końcu (wiem, że to pewnie przez pomyłkę, ale ogólnie to zapytanie bez limit na końcu może mieć problem)

Natomiast samo to zapytanie to nie jest żaden problem. Robię takie na co dzień i śmiga aż miło.

[quote=Tomash]Radarek: bo złączenia to bardzo kosztowne obliczeniowo operacje. Nie tak jak N+1 zapytań, ale generalnie jest to coś, czego eliminacja (przez denormalizację) jest praktycznie drugim, po położeniu indeksów, krokiem w mocnej optymalizacji bazy. Przynajmniej w świecie webdevelopmentu, gdzie każdy ułamek sekundy w zwróceniu danych się liczy.

Stąd zresztą popularność ruchu noSQL: skoro dla wysokiej wydajności wywalamy relacyjność, to po cholerę ją mieć in the first place?[/quote]
Czy ja wiem… zależy ile masz danych i jak masz zoptymalizowaną bazę. Ogólnie wszystko jest kosztowne ale bez przesady, masz gdzieś jakieś obliczenia pokazujące tę złożoność i że ona tak boli? Ruch noSQL raczej idzie w stronę shardingu czy rozproszenia baz, wtedy brak joinów jest raczej skutkiem tego podejścia a nie przyczyną.

Jeśli Ohaleck zajmuje się wielkimi bazami, to prawdopodobnie Oracle. A w Oracle SQL nie istnieje klauzula LIMIT.
(w każdym razie jeszcze 3-4 lata temu, kiedy pracowałem jako programista Oracle PL/SQL, nie istniała)

Nie mam pod ręką wyników żadnego benchmarka, natomiast guglnięcie “sql joins expensive” zwróci kilka wartych poczytania stron.

Swoją drogą można stworzyć bazę łączącą sharding ze złączeniami/relacjami – MongoDB (moja nowa fascynacja :wink: ) – ale to temat na inną rozmowę.

To zapytanie radarka było podane jako przykład, więc chyba bez sensu jest wytykać co w nim jest nie tak. Ale jeżeli chodzi o ten powyższy zarzut, to z reguły różnica jest bardzo mała. A dla tych, którym to jest potrzebne, ciekawostka: http://github.com/methodmissing/scrooge - plugin, który sam stara się wybrać, które pola będą potrzebne (poprzez analizę wcześniejszych zapytań). Nie używałem u siebie, ale może ktoś się skusi.

Pisanie “złączenia to bardzo kosztowne obliczeniowo operacje” bez podania konkretnego przykładu jest bezsensu. Podobnie z googlowaniem fraz “sql joins expensive”. Za frazą “ruby is slow” też znajdę mnóstwo wyników, co nie znaczy, że ruby jest zawsze i wszędzie wolny. Podobnie rzecz ma się z joinami. W tym konkretnym przypadku join, przy odpowiednio założonych indeksach nie powinien być kosztowny (ostatecznie to zależy od średniej ilości itemów przypadających na listing). Oczywiście należy przeanalizować co zwraca polecenie “EXPLAIN …” i dopiero można dyskutować o złożoności zapytania.

Chwilę mnie nie było. Radarek et al.: Bazy są duże, w setkach milionów rekordów, choć, biorąc pod uwagę, że to nie jest SQL (thx Tomash za zwrócenie uwagi, że świat się na SQLu nie kończy :slight_smile: to nawet ciężko to dokładnie przeliczyć w ten sposób.
Nie mam dostępu do większości baz produkcyjnych, ale jedną, która jest używana do demonstracji, jest baza e-maili Enronu (http://www.cs.cmu.edu/~enron/). Około 500tys maili, w których można szukać dowolnych słów z zachowaniem kontektstu i odpowiedź przychodzi w ułamkach sekund). Generalnie czas zapytania w tej bazie jest w przybliżeniu liniowo zależny od liczby ostatecznie zwracanych wyników (a więc odwrotnie niż w SQLach, gdzie w uproszczeniu zależy od iloczynu liczby zbiorów zwracanych przez poszczególne składniki zapytania).
Nie mogę za dużo o tym pisać, ale jak coś, to mogę podesłać jeszcze jakieś info.

Jeśli Ohaleck zajmuje się wielkimi bazami, to prawdopodobnie Oracle. A w Oracle SQL nie istnieje klauzula LIMIT.
(w każdym razie jeszcze 3-4 lata temu, kiedy pracowałem jako programista Oracle PL/SQL, nie istniała)[/quote]

  1. Prawdopodobnie, co nie oznacza, że to Oracle.
  2. Nie wiem jakiego Oracla używałeś, ale w moim od zawsze używało się WHERE ROWNUM < 100 i działa to tak samo i raczej było to od zawsze. Przecież nie chodzi o samo słowo ‘limit’ ale o limitowanie wielkości recordsetu.
    Jesteś pewien, że pracowałeś jako programista PL/SQL?

Nie mam pod ręką wyników żadnego benchmarka, natomiast guglnięcie “sql joins expensive” zwróci kilka wartych poczytania stron.[/quote]
No tak… pooglądałem te linki, większość pisana jest przez ludzi nie mających pojęcia o tym jak sprawdzić plan zapytania itp. Normalnie jak używam joinów na tabelach, które mają tak do 2mld rekordów, to jakoś dziwnie szybko mi to działa i nie mam żadnych problemów z wydajnością. Wydajność siada jak zapytanie nie używa indeksów, no ale nie można winić bazy, że ktoś jej nie umie używać.

[quote=ohaleck]Chwilę mnie nie było. Radarek et al.: Bazy są duże, w setkach milionów rekordów, choć, biorąc pod uwagę, że to nie jest SQL (thx Tomash za zwrócenie uwagi, że świat się na SQLu nie kończy :slight_smile: to nawet ciężko to dokładnie przeliczyć w ten sposób.
Nie mam dostępu do większości baz produkcyjnych, ale jedną, która jest używana do demonstracji, jest baza e-maili Enronu (http://www.cs.cmu.edu/~enron/). Około 500tys maili, w których można szukać dowolnych słów z zachowaniem kontektstu i odpowiedź przychodzi w ułamkach sekund). Generalnie czas zapytania w tej bazie jest w przybliżeniu liniowo zależny od liczby ostatecznie zwracanych wyników (a więc odwrotnie niż w SQLach, gdzie w uproszczeniu zależy od iloczynu liczby zbiorów zwracanych przez poszczególne składniki zapytania).
Nie mogę za dużo o tym pisać, ale jak coś, to mogę podesłać jeszcze jakieś info.[/quote]
Czy ja wiem… takie bazy mające po 300mln rekordów miałem przyjemność adminować i wcale nie były one duże. Join tabelki mającej 20mln rekordów z inną, która miała 2tys rekordów z sortowaniem wyniku to w sumie drobiazg zajmujący max. 500ms. Wszystko to kwestia odpowiednich indeksów.

Obecnie się bawię bazami nieco większymi, tabelki tak do 2 mld rekordów i też nie jest to jakiś wielki problem żeby mieć wynik zapytania w czasie w granicach sekundy, kwestia optymalizacji samej bazy.

Tak naprawdę nie demonizujcie joinów. Oczywiście na sqlu świat się nie kończy, ale spokojnie można bazy sqlowe podkręcić korzystając z ich mechanizmów i używać joinów. W bazach nie-sqlowych jest o tyle gorzej, że często trzeba samemu opracować algorytmy obrabiania danych, co grozi tym, że jak programista dupa, to baza nie działa (mimo, że ten sam programista poradzi sobie z prostym selectem w bazie sqlowej, bo pewnie użyje orma, a to większość programistów zwalnia z myślenia).

SimonG: spróbuj zrobić join na dwóch tabelkach po 20mln. Nie będzie już tak ładnie.

Relacyjne bazy danych opierają się na indeksach, które pomagają w joinach. Wszystko ładnie, jeśli przewidziałeś strukturę. Wierzcie mi lub nie, ale nie zawsze to się sprawdza. Jest coś takiego, jak faceted search albo faceted navigation. Generalnie działa to tak, jak wprowadzanie nazw ulic w GPSach. Jeśli po “K” nie występuje “Q” to nie możesz go wprowadzić. I w tym momencie bazy relacyjne wymiękają kompletnie, bo o ile nie ma problemu, żeby szybko wyszukać wszystkie ulice na “K”, to wyszukanie wszystkich liter możliwych na drugiej pozycji po “K” zajmie już trochę więcej. A jest to najprostszy przykład.
Chyba nie ma sensu debatować nad wyższością świąt tych nad tamtymi bo każde rozwiązanie ma wady i zalety. Jak zauważył SimonG, bazy SQLowe są w jakimś tam stopniu idiotoodporne i przez to uniwersalne. Zrobił się off-topic, a chodziło tylko o optymalizację zapytania :slight_smile: Nie mniej jednak cieszę się, że wywołałem taką dyskusję. Fajny debiut na forum :slight_smile:

To było dawno, nieprawda i jako jednomiesięczne praktyki studenckie. Także nawet mnie nie zdziwiło, że wylazła mi noobowska słoma z butów :wink: