MySQL DATE() dla pola DATETIME

Jul 17

MySQL DATE() dla pola DATETIME

Oblicza MySQL nie są do końca znane przy tworzeniu aplikacji, a problemy optymalizacyjne stają się nie lada problemem przy funkcjonowaniu wersji produkcyjnej projektu. Nie sposób przewidzieć wszystkich możliwości użycia pól, założenia zarówno wspólnych, jak i pojedynczych indeksów posiadających zakładaną przez nas moc i zajętą pamięć na dysku.

Ostatnimi czasy budowałem dość skomplikowany projekt, jeżeli chodzi o złożoność zapytań i wykonywanych przez nie operacje matematyczne. Pomimo tego, że aplikacja była doskonale przemyślana, a struktury bazy danych perfekcyjnie jej podporządkowane, gdzieś tkwił problem, bowiem jedno z zapytań generowało pozornie prosty (wizualnie) rezultat, baza reagowała na zapytanie dopiero po 2.5 sekundy dla 30k+ rekordów. Patrząc na strukturę kluczy i zapytania, zwłaszcza, że pola, na których operowałem były różnego rodzaju liczbami i datami zacząłem się poważnie martwić i rozkładać zapytanie na czynniki pierwsze, kończąc na warunkach. Wyobraźcie sobie moje zdziwienie, gdy doszedłem do tego, że całe obciążenie (ponad 2.3 sekundy) generował warunek:

WHERE DATE(ticket_date) >= " ... "

Gdzie ticket_date to pole typu DATETIME. Od razu doszedłem do wniosku, że w parze idzie złe przygotowanie danych przez PHP, a angażowana jest w to wszystko baza, na której forsuje się użycie funkcji DATE(). Przynajmniej dla 30k+ rekordów zindeksowanego pola. Prosty zabieg zamiany jednej linijki kodu na drugą przyniósł porządane efekty.

$aTerms[] = 'DATE(ticket_date) >= "' . $sDate . '"'
$aTerms[] = 'ticket_date >= "' . date('Y-m-d H:i:s', strtotime($sDate)) . '"'

Budując aplikację zwracam szczególną uwagę na strukturę bazy, indeksowanie pól, rysuję diagramy przewidujące wykorzystanie danych pod różne zapytania, ale… tak banalny błąd przy przeanalizowanej aplikacji rozłożył mnie na łopatki. Z drugiej strony, zapomniałem o jednej bardzo ważnej rzeczy: maksymalnym odciążeniu bazy danych przy preparowaniu argumentów warunków, skoro warunki te mogą być w odpowiedni i przede wszystkim szybki sposób spreparowane na poziomie modelu (abstrakcyjnie rzecz ujmując, pozbywam się pojęcia PHP), który przygotują zapytanie tylko do wykonania operacji na surowych danych, bez konieczności ich ewentualnego przeliczania. Oczywiście nie zawsze taki efekt da się uzyskać, ale należy to maksymalnie optymalizować.

Jedno jest wiadome: przeliczanie DATE() dla rekordów w warunku jest nieoptymalne dla pola DATETIME.

Read More

jQuery Animate i Easing

May 08

jQuery Animate i Easing

Nie można kwestionować faktu, że jQuery.animate() jest jednym z najbardziej potężnych narzędzi jQuery. Służy on do animowania atrybutów CSS (czyli zmiany ich wartości w czasie od obecnego stanu A do definiowanego stanu B). Najprostszą implementacją jQuery Animate jest podanie zbioru atrybutów CSS, które mają ulec zmianie oraz czasu, w jakim ta zmiana ma nastąpić. Nie będę się zagłębiał w najprostsze przykłady użycia, są one dostępne w oficjalnej dokumentacji jQuery.

Należy pamiętać, że dzięki jQuery jesteśmy w stanie nie tylko płynnie zmieniać kolory, wielkość czcionki, obramowanie, ale także pozycje elementów, nadając stronie dynamicznego kształtu. Domyślnym sposobem animowania (easing) jest płynne przechodzenie. Istnieje natomiast sposób na zmianę adaptera animowania. Robert Penner – autor pluginu jQuery Easing dostarczył nam niewiarygodnie efektowne i proste w implementacji narzędzie. Na oficjalnej stronie pluginu można znaleźć wiele przykładów animacji, które dostarcza nam dodatek. Efekty widoczne są zwłaszcza przy animowaniu pozycji i wymiarów obiektu, ale następują także w przypadku zmiany koloru – czyli są aplikowane do zmiany stanu każdego z atrybutów CSS.

Dziś postaram się pokazać efekty, jakie można uzyskać za pomocą jQuery Animate rozszerzonego o jQuery Interface oraz Easing.

Pierwszym krokiem jest wygenerowanie własnej biblioteki jQuery Interface. Dzięki generatorowi, jesteśmy w stanie ściągnąć tylko te części Interface, które są nam de facto potrzebne, zmniejszając jednocześnie ilość kodu. Klikamy ‘deselect all components’, a w sekcji Effects wybieramy efekty, których będziemy używać. Mnie w tej chwili interesuje Bounce i Slide. W paczce otrzymamy wersję deweloperską (z wcięciami) oraz minified, gotową do publikacji na serwerze.

Do wykorzystania efektu slideowania a’la iPhone (elastyczne odbicie od krawędzi ściany ekranu) sprowadza się drobny kawałek kodu, w którym istotnym jest parametr easing:

$('#example').animate({ left: 500 }, { duration: 1000, easing: 'easeOutElastic' })

Na pewno komuś się przyda.

Read More

Chmura tagów Tagcloud w PHP

Mar 07

Chmura tagów Tagcloud w PHP

Ostatnio byłem zobligowany napisać nową klasę tagów do mojego projektu Sypacz.pl, która de facto zachowała stare API, lecz rozszerzyła swoje funkcjonalności, więc w kodzie projektu nie było wielu zmian. Wena spowodowała to, że zacząłem pisać kod od zera.

Cały problem polega na tym, aby napisać na tyle elastyczną klasę tagów, która przyjmie nam zestaw danych, a następnie zaprezentować ją w formie chmury, czym zaopiekuje się arkusz stylów CSS:

Nazwa tagu => Ilość występowań

Parę osób mnie pytało, jak wyciągnąć takie informacje z bazy danych:

SELECT tag_name, COUNT(tag_name) AS tag_times FROM tags GROUP BY tag_name ORDER BY tag_times LIMIT 1, 50

Wykorzystałem obiekt Vframe_Attribute, aby ustandaryzować komponent względem pozostałych w moim frameworku. Jeżeli ktoś nie chce używać obiektu Attribute, może w prosty sposób przekształcić klasę tagów, otrzymując ten sam efekt, deklarując tylko atrybut chroniony protected $_aAttributes = array();. Temat chmury tagów wydaje mi się na tyle trywialny, że nie ma się co nad nim zbyt wiele rozwodzić, zamieszczę tylko klasę i opiszę krótko w przykładach jej możliwości.

  • Vframe_Tagcloud – klasa tagów,
  • Vframe_Attribute – pomocnicza klasa atrybutów dla stosu $_aAttributes, dziedziczenie można usunąć i zadeklarować atrybut samemu.

Aby stworzyć nowy obiekt tagów, po prostu wywołujemy konstruktor:

$oCloud = new Vframe_Tagcloud();

Konstruktor nie przyjmuje żadnych argumentów, wiec możemy od razu przejść do podawania obiektowi tagów. W tym miejscu warto nadmienić, że każdy znak jest rozróżniany (ze względów elastycznych), więc jeżeli chcesz, aby Nazwatagu oraz nazwatagu były rozpoznawane jako jeden klucz, wypadałoby użyć funkcji strtolower lub mb_strtolower (dla Multibyte Strings):

foreach($aDataTags as $iKey => $aRow)
  $oCloud->add(strtolower($aRow['tag_name']), $aRow['tag_times']);

Najistotniejszą częścią klasy jest sposób renderowania chmury, które może działać w dwóch trybach:

  • Tryb prosty zwraca nam nazwę tagu oraz jego wagę po przeliczeniu w formie liczby.
  • Tryb zaawansowany zwraca nam nazwę tagu oraz tablicę z danymi:
    • level – waga tagu po przeliczeniu,
    • count – ilość występowań, taka jaką podaliśmy,
    • count_percentage – informacja, w jakiej procentowej części ilości występowań znajduje się tag, przyjmując za 100% tag, który występuje najczęściej.

Aby w prosty sposób wyrenderować chmurę tagów, używamy poniższego przykładu:

$aDataTagsRender = $oCloud->render();

Najczęściej używa się trybu prostego. Oba tryby są dalej rozbudowane, bowiem mamy możliwość zdefiniowania zakresu i dokładności wag tagów. Domyślnie wagi tagów zawierają się pomiędzy 1, a 10. Możemy na przykład przyjąć, że najmniejszą wagą jest liczba 3, największą 5, a precyzja wag tagów to 2 miejsca po przecinku:

$aDataTagsRender = $oCloud->render(3, 5, 2);

W celu uniknięcia precyzji po przecinku (chcemy otrzymać liczby całkowite), ustawimy precyzję na 0.

Aby wywołać tryb zaawansowany, musimy podać 4 argument dla metody render() i ustawić go na true.

$aDataTagsRender = $oCloud->render(1, 5, 0, true);

Warto w tym miejscu nadmienić, że tagi mogą nie być posortowane alfabetycznie (co ma miejsce podczas tworzenia chmury tagów). Wystarczy wywołać funkcję ksort (key sort).

Finalny przykład używania klasy tagów, celem wywołania klas CSS level_X, gdzie X to liczba całkowita z zakresu od 1 do 10, resztę robi CSS (kolorowanie, nakładanie rozmiaru):

if(count($aDataTags)) {
 
$oCloud = new Vframe_Tagcloud();
 
foreach($aDataTags as $iKey => $aRow)
  $oCloud->add(strtolower($aRow['tag_name']), $aRow['tag_times']);
 
$aDataTags = $oCloud->render(1, 10, 0);
ksort($aDataTags);
 
foreach($aDataTags as $sTag => $iTag)
  echo '<a href="' . $this->route('tag', $sTag) . '" class="level_' . $iTag . '">' . $sTag . '</a>';
 
}
Read More

MySQL: remove duplicate entries/rows

Jul 05

MySQL: remove duplicate entries/rows

Usuwając coś permanentnie z bazy danych musimy być bardzo ostrożni, bowiem przywrócenie danych jest bardzo trudne, czasem niemożliwe. Podstawową strukturę bazy danych powinno się budować na samym początku tworzenia aplikacji, z biegiem czasu rozbudowywać ją, ale unikać przebudowywania. Niestety są przypadki, gdzie trzeba przebudować jedną rzecz, co powoduje zmianę w wielu warstwach nie tyle aplikacji, co strukturze bazodanowej.

Dziś postaram się opisać, jakie kroki trzeba wykonać, aby bezpiecznie usunąć zdublowane rekordy z bazy danych nie tracąc żadnych danych:

  1. Tworzymy dwie kopie bazy danych lub tabel, na których będziemy pracowali. Najlepiej, aby pracować na drugiej kopii, nigdy na oryginale, a potem wdrożyć zmiany z drugiej kopii na oryginał. Przezorny zawsze ubezpieczony.
  2. Analiza danych w tabeli. Musimy dokładnie wiedzieć jakie są relacje między tabelami, kiedy występują JOIN’y itp. Jeżeli rekordy są zdublowane, a posiadają ustalony ID, do których odwołuje się inny rekord z sąsiedniej tabeli, trzeba będzie w niej zmienić ID rekord zdublowanego na ID “substytuta”, bądź takiego rekordu, który nie spowoduje zmian w serwisie.
  3. Wykonanie operacji usunięcia zdublowanych rekordów.

Po wykonaniu kroku pierwszego zabieramy się za kolejny. Jest to najważniejszy moment naszych operacji. Aby ułatwić zrozumienie problemu, podam przykład z życia. Aplikacja posiadała poważny błąd, który umożliwiał zdublowanie użytkowników, ściślej: można było zdublować username. Za każdym razem, gdy użytkownik się logował i pisał komentarze, był ich właścicielem, ale comment_author posiadały różne ID tego samego użytkownika. Zaraz po skopiowaniu bazy danych spróbowałem przepisać ID autorów komentarzy na pierwszy rekord identyfikujący użytkownika, jaki istnieje w tabeli użytkowników. Skonstruowałem zapytanie:

UPDATE cms_comments
JOIN cms_members AS user_original ON(user_original.user_id = comment_author)
SET comment_author = (
  SELECT user_first.user_id FROM cms_members AS user_first
  WHERE user_first.user_name = user_original.user_name
  ORDER BY user_first.user_id ASC LIMIT 0, 1)

Usunięcie zdublowanych użytkowników było już tylko formalnością. Teraz się okaże, dlaczego zależało mi na wyciągnięciu dokładnie pierwszego rekordu reprezentującego “unikalnego” użytkownika: poniższe zapytanie (ALTER IGNORE TABLE ADD UNIQUE) usunie wszystkie kolejne rekordy oznaczone jako duplicated:

ALTER IGNORE TABLE cms_members ADD UNIQUE INDEX(user_name);

Krótki komentarz z manuala do ALTER TABLE:

IGNORE is a MySQL extension to standard SQL. It controls how ALTER TABLE works if there are duplicates on unique keys in the new table or if warnings occur when strict mode is enabled. If IGNORE is not specified, the copy is aborted and rolled back if duplicate-key errors occur. If IGNORE is specified, only the first row is used of rows with duplicates on a unique key, The other conflicting rows are deleted. Incorrect values are truncated to the closest matching acceptable value.

Pisząc ostatnie posty związane z bazami danych, mam nadzieję, że komuś się przydadzą.

Read More

MySQL: how to convert NULL to 0 number/int

Jul 02

MySQL: how to convert NULL to 0 number/int

Im więcej nietypowych rzeczy programuję, tym więcej nietypowych problemów musze pokonać. Co powiecie na sumę 2 liczb, z których jedna jest wartością NULL powstałą w wyniku działania SUM() lub pochodnych, gdzie nie odnaleziono żadnego rekordu.

Badamy:

SELECT 1+2+3
>> 6

SELECT 1+2+NULL
>> NULL

SELECT COALESCE( NULL, 0 )
>> 0

Zatem analogicznie do powyższego przykładu:

UPDATE users SET user_points = user_points + COALESCE((SELECT SUM( ... ) WHERE ...), 0)

Punkty użytkownika już zawsze będą się sumowały poprawnie :-)

Read More

MySQL UPDATE JOIN

Jun 30

MySQL UPDATE JOIN

Ostatnimi czasy potrzebowałem danych z sąsiedniej tabeli przy UPDATE jedngo z pól w bazie danych. Danych do przetworzenia było sporo, więc zwracałem uwagę na wydajność zapytania. Aby zebrać potrzebne informacje, można użyć jednego ze sposobów:

  1. Zebrać potrzebne dane za pomocą SELECT‘a, co sprawiłoby, że zajęta zostanie niepotrzebna pamięć w środowisku PHP podczas przypisania rezultatu do zmiennej.
  2. Wykonać SET z podzapytaniem, ale potrzebnych mi było kilka kolumn z sąsiedniej tabeli, podzapytanie może zwrócić tylko jedną określoną wartość.
  3. Wykonać JOIN przy update, czego niestety wówczas nie potrafiłem zrobić.

Kartkując manual nie natrafiłem się w standardowej dokumentacji na nic konkretnego, aż nie spojrzałem na bardzo przydatne komentarze użytkowników. Okazało się, że przy UPDATE można wykonywać dowolne JOIN‘y, schemat jest następujący:

UPDATE TABLE JOIN another_table SET ...

W tym momencie mamy do dyspozycji wszystkie pola z dołączonej tabeli. Bardzo przydatne.

Przykład z życia.

Miałem za zadanie odznaczyć typy bukmacherskie na trafione, nietrafione, odwołane z przyczyn odwołania całego meczu piłkarskiego oraz te, które jeszcze nie mogą zostać oznaczone jako trafione lub nie, gdyż mecz się jeszcze nie odbył.

UPDATE typer_tickets_items
LEFT JOIN typer_events ON(event_id = item_event)
SET item_status = (
CASE
WHEN event_status IS NULL THEN NULL # mecz nie zostal rozegrany
WHEN event_status = -1 THEN -1 # mecz anulowany
WHEN event_status = item_bet THEN 1 # typ trafiony
ELSE 0 END # typ nietrafiony
) WHERE item_status IS NULL

Mam nadzieję, że komuś się przyda…

Read More