czerwca 07

Podczas rozwoju dużego portalu webowego każdy prędzej czy później stanie przed problemem wydajności. Przeważnie najbardziej wąskim gardłem jest baza danych. Łatwo jest rozproszy obrazki na więcej serwerów, nawet content html czy pliki php. Najgorzej jest ze skalowalnością bazy. Nie dość że jest to problem od strony technicznej to również jest to najbardziej kosztowne. Po pewnej ilości użytkowników okazuje się że cachowanie, czy generowanie statyczne wszystkiego co było możliwe nie jest już wystarczające a na dodatek wielu użytkowników wykorzystuje opcję przeglądu czy search które pochłaniają najwięcej zasobów bazy danych. W takich przypadkach z pomocą może przyjść nam sphinx. Oczywiście jak w każdym przypadku jego zastosowanie niesie ze sobą wiele plusów i minusów a zadaniem tego artykułu będzie pokazanie jak sobie z tym wszystkim poradzić i jak optymalnie wykorzystać możliwości sphinx’a.

Sphinx to darmowy silnik SQL full-text search na licencji GPL 2. Jest to program pozwalający na zaindeksowanie wybranej przez nas części bazy danych i przeszukiwanie go w bardzo wydajny sposób . W chwili obecnej pozwala na podłączenie sie do MySQL’a i PostgreSQL’a. Posiada natywny support do PHP, Pythona, Javy, Perl’a i Ruby.

Continue reading »

kwietnia 07

Natchnieniem do napisania tego artykułu były dla mnie doświadczenie ostatnich kilku lat mojej pracy i to że mówienie NIE przynosiło mi często więcej pożytku od mówienia tak. Ostatnio znalazłem też w sieci artykuł 10 Absolute “Nos!” for Freelancers który pokazuje że nie tylko ja miałem podobne problemy, a kluczem do ich rozwiązania jest asertywność.

Czy możesz przedstawić nam projekt/atrape a my się zastanowimy

NIE!

Każda szanująca się firma przed podpisaniem umowy z klientem ustala ile kosztować będzie każda część pracy. Przygotowanie projektu, atrapy czy analiza też są płatne. Jeżeli masz poświęcać swój czas to nigdy za darmo. Przeważnie podczas tworzenia aplikacji faza analizy i projektu to koszt na poziomie 15-20% płatny od razu po wykonaniu. Klient zawsze może zabrać owoce pracy analitycznej i pójść do innego developera. W takich przypadkach zaproponuj że projekt/analiza może być wykonany za odpowiednią opłatą razem z przeniesieniem praw autorskich do projektu na klienta. Zaproponuj też podpisanie odpowiedniej umowy bo w przeciwnym razie napracujesz się za darmo a w najlepszym przypadku postawisz się na z góry przegranej pozycji podczas negocjacji ceny całego projektu ponieważ ty już włożysz swoja pracę za która będziesz chciał odzyskać pieniądze a zleceniodawca wykorzysta twoją naiwność i słabe położenie w negocjacjach.

Continue reading »

kwietnia 05

Subselect to fajne zapytanie, ale co w sytuacji, kiedy musimy przeszukać miliony rekordów i to setki razy w ciągu minuty. Subselect okaże się mało wydajny, a przy dużej ilości zapytań liczy się każda oszczędność. W portalach społecznościowych często użytkownicy poszukują innych profili na postawie wieku lub wzrostu i tutaj nie ma żadnych problemów, gdyż łatwo przechować je w jednej tabeli. Gorzej jest natomiast z danymi typu znajomość języków obcych czy zainteresowania, gdzie pola te mogą mieć wiele wartości. Niektórzy stosują harcerskie rozwiązania w stylu umieszczania wielu danych do jednego varchar’a i użycie LIKE w zapytaniu. Mnie chyba nie przyszłoby coś takiego do głowy ;) Innym pomysłem byłoby zastosowanie pól typu array, lecz odradzane jest to nawet na stronie PostgreSQL:

Tip: Arrays are not sets; searching for specific array elements may be a sign of database misdesign. Consider using a separate table with a row for each item that would be an array element. This will be easier to search, and is likely to scale up better to large numbers of elements.

Najlepszym rozwiązaniem byłoby zastosowanie kolejnej tabeli z danymi, oddzielnej dla języka i zainteresowań w relacji wiele do wielu z naszą tabelą profili. To dobry sposób zachowujący wszystkie zasady budowania schematów baz danych, niestety trochę powolny. Zapytanie wykonujące subselect na milionach rekordów to wydajnościowa masakra dla bazy danych. W czasach programowania w DOS’ie, kiedy każdy bit i cykl procesora był niezwykle ważny, dane przechowywano jako bity wewnątrz wartości integer, aby zużyć mniejszą ilość pamięci. Taki sam trick można zastosować w bazach danych, by zaoszczędzić miejsce i zasoby podczas przeszukiwania.

Aby pokazać na przykładzie różnice w wydajności zastosowałem poniższy schemat.

schemat bazy

Moje dane testowe:

  • 11 rekordów w tabelach usr_profile_languages i usr_profile_linterests ( po jedenaście grup)
  • ponad milion rekordów w tabeli usr_table
  • ponad trzy miliony rekordów w tabelach łączących profil ze znajomościa języków - usr_profile_languages_list, usr_profile_linterests_list (średnio każdy posiada trzy zainteresowania i zna trzy języki)
  • indeksy na wieku, kraju, językach, zainteresowaniach

Test z wykorzystaniem subquery:

Aby sprawdzić możliwości bazy rozpocząłem od prostego zapytania:

  • select count(*) from usr_table u WHERE age > 56;

Otrzymaliśmy 200 000 rekordów w 1,5 sekundy

  • select count(*) from usr_table u WHERE age < 42;

Zapytanie ma ponad 600 000 rekordów i trwało 1,6 sekundy.

Teraz zapytanie o ludzi poniżej 50 lat znających jeden wybrany język z wykorzystaniem podzapytania.

  • select count(*) from usr_table u WHERE age < 50 AND (SELECT count(*) FROM usr_profile_languages_list l WHERE l.usr_profile_languages_id=4 AND l.id=u.id)=1;

Zapytanie zabrało od 14s do 50s. Różnica wynika z cachowania zapytania w bazie.

Następnie zapytanie o profile znające dwa języki powyżej 58 roku życia (większość rekordów została wycięta przez index wieku)

  • select count(*) from usr_table u WHERE age >58 AND (SELECT count(*) FROM usr_profile_languages_list l WHERE (l.usr_profile_languages_id=4 OR l.usr_profile_languages_id=6) AND l.id=u.id)=2;

Zapytanie wyszukało około 10000 rekordów w ciągu 17 do 40 sekund.

Podobnie jest z ludźmi powyżej 58 lat znającymi dwa z trzech wybranych języków.

  • select count(*) from usr_table u WHERE age >58 AND (SELECT count(*) FROM usr_profile_languages_list l WHERE (l.usr_profile_languages_id=4 OR l.usr_profile_languages_id=6 OR l.usr_profile_languages_id=7) AND l.id=u.id)>=2;

18000 rekordów w 14 do 20 sekund.

Test na bitach:

Teraz pola bitowe: tworzymy dane typu integer wewnątrz tabeli użytkowników usr_table i wypełniamy je trzema losowymi zainteresowaniami z jedenastu dostępnych.

alter table usr_table add column languagebit integer;

UPDATE usr_table set languagebit=2^(ROUND(RANDOM()*10+1));

UPDATE usr_table set languagebit=languagebit+2^(ROUND(RANDOM()*10+1));

UPDATE usr_table set languagebit=languagebit+2^(ROUND(RANDOM()*10+1));

Prymitywna skuteczność :)

Profile poniżej 50 lat z jednym zainteresowaniem wyszukano w 1,8s .

  • select now() ;select count(*) from usr_table u WHERE age <50 AND languagebit & 32 = 32

Użyłem 32 ponieważ 2^5 daje 32, a poszukiwaną grupą była grupa numer 5.

Ludzie powyżej 58 lat z dwoma zainteresowaniami zostali wyszukani w 1,6s

  • select count(*) from usr_table u WHERE age >58 AND languagebit & 2 = 2 AND languagebit & 1024 = 1024;

Jak to wszystko działa?

insert danych

wprowadzenie rekordu dla grupy 2 i 3:

dodaj 2^2 + 2^3 = 12 i wprowadzamy do languagebit.

update danych

dodanie kogoś do grupy 5 (2^5=32)

  • UPDATE usr_table set languagebit=(languagebit|32) WHERE id=1;

usunięcie osoby z grupy 5

  • UPDATE usr_table set languagebit=(languagebit-32) WHERE id=1;

zapytania select:

Wyszukiwanie odpowiedniej grupy.

jeżeli interesuje nas grupa 3. wykonujemy operacje 2^3 = 8

  • select count(*) from usr_table u WHERE languagebit & 8 = 8;

dla rekordu w grupie 4 i 5 , mamy 2^4=16 i 2^5=32

  • select count(*) from usr_table u WHERE languagebit & (16|32) = 48;

poszukiwanie rekordu zawierającego jedna z dwóch grup 3 lub 6, 2^3=8 i 2^6=64

  • select count(*) from usr_table u WHERE languagebit & (8|64) > 0;

Wszystkie testy przeprowadzone były na PostgreSQL 8.0.6 zainstalowanym na FreeBSD 5.5 RELEASE

Konkluzje

Plusy rozwiązania

+ Duży wzrost prędkości zapytania - rozwiązanie z podzapytaniem jest przynajmniej dziesięć razy wolniejsze.

+ Ilość miejsca potrzebna do przechowania tej samej ilości danych jest znacznie mniejsza (brak indeksów, jeden integer zamiast kilku rekordów w tabeli, tylko jedna tabela z danymi)

Minusy rozwiązania

- Brak możliwości sprawdzenia integralności danych, można dodać bit który nie istnieje.

- więcej programowania wymagane jest po stronie aplikacji