JustPaste.it

Cachowanie zapytań SQL w PHP

Artykuł ten pokazuje, jak przyspieszyć skrypty wykorzystujące bazy danych poprzez cachowanie wyników zapytań do późniejszego użycia. Demonstracja na bazie MySQL.

Bardzo dużą część czasu wykonywania skryptu PHP + baza danych zabiera właśnie wysyłanie zapytań SQL, stąd też każdy programista powinien zadbać o to, by wykonywać ich jak najmniej. W tym artykule chcę właśnie to zaprezentować.

Omawiana tu technika zwie się cachowaniem wyników zapytań. Polega ona na tym, że po pierwszym pobraniu jakiś danych z bazy, pakujemy je ładnie do pliku. W następnych uruchomieniach skryptu nie wysyłamy już zapytania, a dane zasysamy właśnie z utworzonego wcześniej pliku. Wbrew pozorom nie jest to trudne do wykonania; wymaga tylko odrobiny czasu na zaplanowanie tego tak, by skrypt odpowiednio działał. Całkiem dobry mechanizm cache można napisać po jednej godzince pracy nad kodem, a zyski są znaczne.

Jeśli nadal nie jesteś przekonany o skuteczności cachowania, może zaprezentuję pomiary prędkości, jakie sporządziłem na tę okazję. Normalny czas wykonania zapytania typu "SELECT * FROM tabela" wynosił ok. 0.00075 sekundy, natomiast pobranie ok. 20 rekordów zajmowało 0.0005 sekundy. Wyniki przy zastosowaniu cachowania danych przeszły moje najśmielsze oczekiwania. Czas wykonania funkcji wysyłającej zapytanie był tak niski, że musiałem sztucznie go zawyżyć, by uzyskać informację czytelną dla przeciętnego zjadacza chleba. Po ręcznym odjęciu zawyżenia okazało się, że czas ten wyniósł... 0.00008 sekundy, a więc był ok. 10 razy niższy! Pobieranie wyników również było szybsze, lecz już nie tak spektakularnie - średnia wyniosła około 0.0003 sekundy. Wiem, że różnice rzędu tysięcznych części sekundy mogą wydawać Ci się śmieszne, ale weź pod uwagę, że normalny serwer jest znacznie bardziej obciążony. Przecież na sekundę może obsługiwać nawet 10 wywołań różnych skryptów, z czego każdy żąda dostępu do bazy. Wtedy nawet czasy rzędu jednej dziesięciotysięcznej sekundy są niezwykle cenne!

Projekt

Zacznijmy od zaplanowania pracy. Rzeczą najważniejszą jest wykorzystanie ręcznie napisanego sterownika do bazy danych (tłumaczenie dla programistów zanglicyzowanych: database layer), bo bez niego to nawet nie masz co marzyć o stworzeniu cachowania, chyba że lubisz do każdego wywołania mysql_query() doklepywać ręcznie dodatkowych 20 linijek kodu :). Przy okazji jest to jedna z sytuacji, kiedy użycie takiego sterownika przydaje się nawet wtedy, gdy nie masz zamiaru wykorzystywać przenośności między różnymi bazami danych.

Jeśli chcesz się dowiedzieć więcej o takim sterowniku, możesz zajrzeć do artykułu sickboy'a zatytułowanego "Prosty sterownik MySQL w PHP".

Jako, że będziemy cachować tylko niektóre zapytania, musimy mieć możliwość włączania tej możliwości na życzenie. Dlatego też domyślnie zapytania będą wykonywane bez tego. Dopiero po wywołaniu metody sql_cache(), do której podamy nazwę, pod jaką chcemy zapisać nasze dane, mechanizm zostanie uaktywniony. Po pobraniu wyników będziemy go z powrotem wyłączać (by następne zapytanie omyłkowo nie wykorzystało tego samego uchwytu i nie zwaliło wszystkiego), wywołując metodę sql_cache() bez parametrów. Dodatkowo musimy mieć możliwość czyszczenia cache, co bardzo przyda się nam przy zmianie danych w bazie. Zaraz po dodaniu/zmianie/usunięciu czegoś, skasujemy plik ze starymi informacjami, co spowoduje, że pierwszy gość, który zażąda do nich dostępu, pobierze wyniki bezpośrednio z bazy. Zostaną one od nowa scachowane. Takim oto sposobem zmiana informacji będzie od razu widoczna w serwisie.

Aby wszystko to było możliwe, sterownik musi przechowywać gdzieś informację o tym, w jakim trybie aktualnie pracuje. Trybów będzie trzy. Pierwszy z nich (numer 0) - cachowanie wyłączone, normalny tryb pracy. Drugi (1) oznacza czytanie z pliku cache bez wykonywania danego zapytania. Trzeci i ostatni (z numerkiem 2) będzie informował sterownik, iż zapytanie ma wykonać, ale jego wyniki ma zachować także w odpowiednim pliku. Dla nas oznacza to po prostu utworzenie pliku cache.

Kod

Na początek chciałbym dodać pewną uwagę odnośnie kodu do niektórych "czytelników" moich artykułów: nawet nie próbuj bezmyślnie kopiować poniższego kodu, nie patrząc na opisy do poszczególnych jego fragmentów. Na pytania "dlaczego to nie działa" spowodowane właśnie przez głupotę (bo inaczej tego nazwać nie można) po prostu nie będę odpowiadał. Najpierw zajmiemy się początkiem pliku sterownika, czyli deklaracją klasy + połączeniem się z bazą:

<?php
 
define('CACHE_DIR', './sql_cache/');
 
class sql{
var $connection;
var $result;
var $rows;
 
var $queries = 0;
 
var $cache_state =0;
var $cache_file;
var $cache_buffer;
var $cache_ptr;
 
function sql_connect($host, $user, $pass, $db){
$this -> connection = mysql_connect($host, $user, $pass);
mysql_select_db($db);
}
 
function sql_close(){
mysql_close($this -> connection);
}

Stała CACHE_DIR przechowuje ścieżkę do katalogu, w którym będą składowane pliki cache. Ponadto, w deklaracji klasy znajdują się cztery pola wykorzystywane przez nasz mechanizm. $cache_state przechowuje numer aktualnego stanu (0, 1, lub 2). $cache_file trzyma nazwę pliku, w którym cache ma być zachowane. $cache_buffer to bufor, w którym będziemy gromadzić dane przy generowaniu cache, bądź z którego będziemy je czytać. Natomiast $cache_ptr przyda się właśnie przy pobieraniu danych, przechowując numer ostatnio pobranego pola w buforze.

Teraz podstawa mechanizmu pozwalająca nam go włączyć, bądź wyłączyć:

      function sql_cache($handle = 0){
if(is_string($handle)){
if(file_exists(CACHE_DIR.'xxx_'.$handle.'.666')){
$this -> cache_state = 1;
$this -> cache_ptr = 0;
$this -> cache_buffer = unserialize(file_get_contents(CACHE_DIR.'xxx_'.$handle.'.666'));
}else{
$this -> cache_state = 2;
$this -> cache_buffer = array();
$this -> cache_file = CACHE_DIR.'xxx_'.$handle.'.666';
}
}else{
if($this -> cache_state == 2){
file_put_contents($this -> cache_file, serialize($this -> cache_buffer));
}
$this -> cache_state = 0;
}
}

Jeśli podaliśmy uchwyt (is_string($handle)), PHP musi zdecydować, czy należy pobrać dane z cache, czy też takowe wygenerować. Określa to na podstawie istnienia pliku danego uchwytu. Jeżeli istnieje - to OK (tryb 1), w przeciwnym wypadku będziemy go generować (tryb 2). Niepodanie uchwytu wyzwoli mechanizm wyłączania cache. Gdy ten był włączony w trybie zapisu, musimy dodatkowo zachować nasze dane w pliku.

Scachowane dane zachowane są w pliku w postaci zserializowanej tablicy. Dlaczego tak? Przecież mogłem generować od razu odpowiedni kod PHP, który wystarczyłoby tylko dołączyć poprzez require()! Otóż w tym przypadku nie jest to najwydajniejsze wyjście. Zmierzyłem czas odczytu dla obu metod - serializacji i plików PHP. Wynika z nich, iż "odserializowywanie" danych jest dwa razy szybsze, niż dołączanie kodu PHP z nimi używając require()!

Tu chciałbym zamienić słówko z posiadaczami przedpotopowego PHP 4 :). Otóż z jakiś powodów nie ma w nim jeszcze funkcji file_put_contents() pozwalającej w prosty sposób zapisać dane do pliku za jednym zamachem. Jako, że takowa jest wykorzystywana w naszym skrypcie (pisałem go na PHP 5), musisz ją samemu "zrobić". Podaję tu gotowy kod, który należy umieścić PRZED deklaracją aktualnie pisanej klasy:

   function file_put_contents($plik, $dane){
$f = fopen($plik, 'w');
fwrite($f, $dane);
fclose($f);
}

Wracamy do naszej właściwej klasy. Teraz prościutka metoda czyszczenia cache:

      function sql_cache_remove($handle){
if(file_exists(CACHE_DIR.'xxx_'.$handle.'.666')){
unlink(CACHE_DIR.'xxx_'.$handle.'.666');
}
}

Wiadomo - jeżeli plik dla danego uchwytu istnieje, to go wywal.

Teraz przechodzimy do najważniejszych metod sterownika - sql_query(), sql_fetch_array(), oraz sql_fetch_row(). To dzięki nim będziemy wykonywali operacje na bazie danych:

      function sql_query($query){
if($this -> cache_state != 1){
$this -> result = mysql_query($query);
$this -> queries++;

if(mysql_errno() != 0){
die('Error: '.mysql_error().'<br/>');
}
return 1;
}
}

W tej metodzie zapytanie jest wysyłane jedynie wtedy, gdy cachowanie jest wyłączone (tryb 0), lub jesteśmy w trakcie generowania pliku cache (tryb 2). Jeżeli czytamy dane z pliku, nic się nie dzieje i to jest recepta na tak niski czas wykonywania tej metody.

Metody pobierania danych są już troszkę bardziej skomplikowane, gdyż muszą inaczej obsługiwać każdy z trybów pracy. Wrzuciłem obie naraz, gdyż różnią się one tylko tym, że w jednej wywoływana jest funkcja mysql_fetch_assoc(), a w drugiej mysql_fetch_row().

      function sql_fetch_array(){
if($this -> cache_state == 1){
if(!isset($this -> cache_buffer[$this -> cache_ptr])){
return 0;
}
$this -> rows = $this -> cache_buffer[$this -> cache_ptr];
$this -> cache_ptr++;
return 1;
}else{
if($this -> rows = mysql_fetch_assoc($this -> result)){
if($this -> cache_state == 2){
// Dodaj do cache
$this -> cache_buffer[] = $this -> rows;
}
return 1;
}
}
return 0;
}
 
function sql_fetch_row(){
if($this -> cache_state == 1){
// czy koniec bufora?
if(!isset($this -> cache_buffer[$this -> cache_ptr])){
return 0;
}
// odczytaj z bufora
$this -> rows = $this -> cache_buffer[$this -> cache_ptr];
$this -> cache_ptr++;
return 1;
}else{
if($this -> rows = mysql_fetch_row($this -> result)){
if($this -> cache_state == 2){
// Jeśli tworzymy cache, musimy rekord dodatkowo zapisac w buforze
$this -> cache_buffer[] = $this -> rows;
}
return 1;
}
}
return 0;
}
 
} // koniec klasy
?>

Stan 1 obsługuje pierwsza część metody. Wykorzystujemy tam czytanie z naszego bufora cache - $this -> cache_buffer, a do identyfikacji, rekordu do pobrania używamy $this -> cache_ptr. Oczywiście na początku musimy sprawdzić, czy przypadkiem nie osiągnęliśmy już końca bufora. Gdyby tego nie było, albo znajdowało się to w innym miejscu, otrzymalibyśmy albo pętlę nieskończoną, albo błędy przy pobieraniu.

Czytanie z bazy danych jest już bardziej znajome - po prostu przypisanie tablicy wygenerowanej przez funkcję mysql_fetch_xxx(). Dodatkowo doszło tu sprawdzenie pozwalające nam zapisać tę tablicę w buforze, w przypadku generowania cache. I to właściwie tyle, jeśli chodzi o kod.

Użycie

Teraz omówię, jak używać podanego powyżej sterownika. Wbrew pozorom jest to bardzo proste. Oto przykład kodu bez cachowania:

<?php
require('./sterownix.php');
 
$sql = new sql;
$sql -> sql_connect('localhost', 'root', '', 'moja_kurde_baza');
 
$sql -> sql_query('SELECT * FROM config');
while($sql -> sql_fetch_row()){
echo $sql -> rows[0].' - '.$sql -> rows[1].'<br/>';
}
 
$sql -> sql_close();
?>

Jak widać, użycie sterownika bez wykorzystania cachowania jest banalnie proste. Podobnie jest także z użyciem cachowania:

<?php
 
require('./sterownix.php');
 
$sql = new sql;

$sql -> sql_connect('localhost', 'zyx', 'doopah', 'fws');
 
$sql -> sql_cache('uchwyt');
$sql -> sql_query('SELECT * FROM config');
 
while($sql -> sql_fetch_row()){
echo $sql -> rows[0].' - '.$sql -> rows[1].'<br/>';
}
$sql -> sql_cache();
 
$sql -> sql_close();
?>

Aby scachować jakieś zapytanie, musimy przed nim wywołać metodę sql_cache() z podaną nazwą uchwytu, pod jakim chcemy te dane zapisać. Po pobraniu wyników cache jest wyłączany, by nie powodować problemów z resztą zapytań. Jeśli chcesz cachować dwa zapytania następujące po sobie, także nie możesz zapomnieć o uprzednim wyłączeniu mechanizmu:

$sql -> sql_cache('zapytanie1');
...
$sql -> sql_cache();
$sql -> sql_cache('zapytanie2');
...
$sql -> sql_cache();

Jak dobrze wykorzystać cache?

Posiadanie cache to jedna strona medalu - właściwe użycie to druga. Przede wszystkim nigdy nie powinieneś używać go przy zapytaniach, w których musisz pobrać konkretną informację, np. dane użytkownika, który chce się zalogować. Takie coś mija się z celem i może spowodować mnóstwo błędów. Według mnie przydaje się to bardzo przy wyświetlaniu listy dostępnych zasobów strony, np. newsów, listy artykułów, czy też porad. Listy takie ogląda mnóstwo ludzi i dlatego należałoby przyspieszyć ich generowanie m.in właśnie cachując je! Z kolei pojedynczy zasób, np. artykuł także do scachowania raczej się nadaje, gdyż zajmować będzie niepotrzebnie miejsce na twoim koncie. Musiałbyś także rozwiązać problem z uchwytami.

Możesz także użyć mechanizmu do przyspieszenia inicjacji silnika strony. W moim skrypcie w ten sposób oszczędziłem sobie każdorazowe zmuszanie bazy do pobrania konfiguracji strony, a także menu. Obie te rzeczy mogę przecież bez żadnych moralnych itp. szkód trzymać na dysku i w ten sposób przyspieszyć pracę silnika.

Chciałbym poruszyć jeszcze sprawę edycji/dodawania/usuwania danych z cachowanej tabeli. Otóż w takim przypadku zaraz po wysłaniu do bazy odpowiedniego zapytania musisz usunąć plik cache, przez co pierwszy użytkownik, który zechce sobie coś scachowanego obejrzeć, automatycznie wygeneruje nowy plik z już zmienionymi danymi. Możesz to zrobić, używając metody sql_cache_remove($uchwyt); dostępnej w sterowniku:

$sql -> sql_query('INSERT INTO news VALUES ....');
$sql -> sql_cache_remove('news');

W ten oto sposób rozwiążesz problem z aktualizacją zasobów serwisu.

Zakończenie

Tyle miałem do powiedzenia, jeśli chodzi o cachowanie wyników zapytań. Mam nadzieję, że przedstawione tu rozwiązania przyspieszyły twoje skrypty. Jeśli masz jakieś pytania, skorzystaj z forum dyskusyjnego Webcity. Do następnego artykułu!

Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com

 

Źródło: http://webcity.pl/webcity/cachowanie_zapytan_sql_w_php

Licencja: Creative Commons - użycie niekomercyjne - bez utworów zależnych