JustPaste.it

Optymalizacja skryptów PHP i zapytań SQL

Jak przyspieszyć skrypty PHP i zapytania do baz danych? Jak skonfigurować narzędzie Xdebug? Jak zmierzyć wydajność skryptu za pomocą Apache Benchmark?

Jak przyspieszyć skrypty PHP i zapytania do baz danych? Jak skonfigurować narzędzie Xdebug? Jak zmierzyć wydajność skryptu za pomocą Apache Benchmark?

 

PHP jest językiem interpretowanym po stronie serwera. Do przeglądarki jest wysyłany jedynie wynik jego działania. W typowym serwisie internetowym serwer po otrzymaniu żądania pobiera niezbędne rekordy (np. treść strony i informacje o użytkowniku) z bazy danych, a następnie przetwarza je i wysyła do klienta. Przy dobrze zoptymalizowanym kodzie może on obsłużyć znacznie więcej internautów w określonym czasie.

Zacznij od... Xdebug

Podczas pisania aplikacji PHP i jej optymalizacji pomocne jest rozszerzenie Xdebug, ponieważ:

  • wyświetla czas wykonywania się funkcji oraz użycie pamięci w przypadku wystąpienia błędu

  • oferuje funkcje wyświetlające np. całkowity czas interpretacji kodu, szczytowe użycie pamięci...

  • generuje pliki ze szczegółowymi informacjami na temat każdego wywołania funkcji i metod, które można odczytać za pomocą specjalnego programu

Konfigurujemy Xdebug

  1. Pobierz rozszerzenie z oficjalnej strony xdebug.org w wersji dla używanego systemu operacyjnego – dla Linuksa lub Windowsa.

  2. Umieść plik .dll lub .so w folderze ext znajdującym się w katalogu instalacji PHP.

  3. W pliku php.ini dodaj linijkę:

    zend_extension_ts('PATH');

  4. W miejsce PATH wpisz ścieżkę bezwzględną do pliku .dll lub .so. Dostępne są też opcje konfiguracyjne, opisane na stronie Xdebug.

  5. Zapisz plik php.ini i zrestartuj Apache.

Aby sprawdzić, czy rozszerzenie zostało zainstalowane prawidłowo, utwórz jakikolwiek skrypt i wywołaj funkcję phpinfo(). Jeżeli nie ma żadnej wzmianki o nim, sprawdź, czy podałeś poprawną ścieżkę (BEZWZGLĘDNĄ!) oraz czy wersja Xdebug jest zgodny z zainstalowaną wersją PHP.

Czas i użycie pamięci

Możesz użyć przedstawionych poniżej funkcji:

//Użycie pamięci 
echo xdebug_memory_usage(); 

//Szczytowe użycie pamięci 
echo xdebug_peak_memory_usage();

//Czas w sekundach 
echo xdebug_time_index();

Instrukcje czy funkcje?

Użycie instrukcji lub wyświetlenie wartości zmiennej / stałej jest kilka razy szybsze od wywołania funkcji. Na przykład wersję PHP można uzyskać na 2 sposoby.

//Użycie funkcji 
echo phpversion(); 

//Wyświetlenie stałej 
echo PHP_VERSION; 

//Wersja rozszerzenia Xdebug
echo phpversion('Xdebug');

Od wersji PHP 5.1 uniksowy znacznik czasu (liczba sekund, która upłynęła od 11 stycznia 1970) dla momentu przyjęcia żądania znajduje się w tablicy $_SERVER. W większości sytuacji można nim zastąpić funkcję time().

echo $_SERVER['REQUEST_TIME'];

Apostrof czy cudzysłów?

Jeżeli w łańcuchu znaków nie używasz kombinacji: \n, \r, \t, \v, umieść go w apostrofach. W przypadku użycia cudzysłowów PHP sprawdza, czy w tekście znajdują się zmienne oraz ww. symbole, co pochłania więcej czasu.

//Szybciej - bez analizy 
$x = 'Witaj' . $user;

//Wolniej - z analizą 
$x = "Witaj $user";

Złączenia ciągów tekstowych

Staraj się zmniejszyć ilość złączeń ciągów tekstowych (w pierwszym przykładzie - za pomocą kropki). Im jest ich więcej, tym bardziej wzrasta użycie pamięci przez skrypt, gdyż jest to dość kosztowna operacja.

$user = 'Grzegorz Brzęczyszczykiewicz'; //Login
$lang = 'Zalogowany jako'; //Tekst w pliku językowym 
echo $lang . ': ' . $user;

W tym przypadku zmienna $lang może już zawierać dwukropek:

$lang = 'Zalogowany jako: '; 
echo $lang . $user;

Jeszcze lepszym rozwiązaniem jest użycie instrukcji printf().

$lang = 'Zalogowany jako: %s'; //Na miejscu %s (string) pojawi się login
printf($lang, $user);

Aby zwrócić ciąg tekstowy zamiast wyświetlać go na ekran użyj sprintf().

Unikaj operatora @

Operator @ postawiony przed nazwą funkcji zapobiega wyświetlaniu błędów przez nią wygenerowanych. Jednak jego użycie znacznie spowalnia skrypt. Zaleca się przemyśleć kod tak, aby pominąć operator @, a nasze rozwiązanie było jeszcze szybsze.

Nie wynajduj koła na nowo

Język PHP jest wciąż rozwijany. Warto dowiedzieć się, co nowego oferują nowe wersje. Być może już istnieje potrzebna Ci funkcja, którą chcesz napisać od podstaw?

Funkcje wbudowane w PHP oraz w rozszerzenia są zawsze szybsze od tych, które znajdują się w interpretowanym kodzie, gdyż zostały one napisane w języku C.

Nowe funkcje w PHP5

  • file_put_contents() – zapisuje treść do pliku

  • scandir() – zwraca tablicę z listą plików i folderów w danym katalogu posortowaną alfabetycznie

  • error_get_last()

Nowe klasy w PHP5

  • SimpleXML i DOM (obiektowy model dokumentu) – do operacji na języku XML

  • DateTime i DateTimeZone

  • PDO – uniwersalny interfejs do obsługi baz danych

  • MySQLi – rozszerzenie obsługujące nowe możliwości MySQL (od wersji 4.1), np. podpinanie danych, transakcje...

  • Tidy – obsługuje narzędzie Tidy do analizy, czyszczenia, i naprawy dokumentów HTML

  • Klasy Standard PHP Library

  • Exception – wyjątki, które mogą zostać rzucone instrukcją throw

Zbędne zmienne

Nie twórz zmiennych, których użyjesz tylko 1 raz. PHP przechowuje je w tablicy. Jeśli jest ich zbyt wiele, znalezienie szukanej zmiennej może potrwać długo. Ze względu na ilość danych użycie pamięci wzrośnie. Zasada dotyczy również stałych i funkcji. Są wyjątki od reguły. O tym w dalszej części artykułu.

Oszczędź procesor

Rozważ następujący przykład:

setcookie('OK', 1, 60*60*24);

Powyższy kod ustawia cookie na 1 dzień. Tylko po co PHP ma za każdym razem liczyć wartość wyrażenia 60*60*24? Szybszy kod:

// Ustaw cookie na 1 dzień
setcookie('OK', 1, 86400);

Przeanalizuj teraz poniższą pętlę FOR:

// $x jest tablicą
for($i=0; $i<count($x); ++$i)
{
    echo $x[$i];
}

PHP liczy przy każdej iteracji ilość elementów w tablicy. Jeśli w pętli nie ulega ona zmianie, policz ją tylko raz. Szybciej wykona się poniższy kod:

$ile = count($x);
for($i=0; $i<$ile; ++$i)
{
    echo $x[$i];
}

Zmienną $ile można utworzyć w pierwszym wyrażeniu pętli FOR.

for($i=0, $ile=count($x) ...)

Jeżeli ilość elementów jest stała (np. 20), wprowadź ją od razu to drugiego wyrażenia:

for($i=0, $i<20; ++$i)

Programowanie obiektowe

Zalążki programowania obiektowego pojawiły się już w PHP 3. Nowy model obiektowy, oferujący funkcjonalność istniejącą w wielu innych językach, został wprowadzony dopiero w PHP5.

W PHP 4 obiekty są jednocześnie zmiennymi. Podczas przekazywania ich do funkcji, zwracania lub przypisywania, są one kopiowane. Aby temu zapobiec, należy przekazać obiekt przez referencje.

Sytuacja uległa zmianie w PHP 5. Zmienna zawiera jedynie identyfikator obiektu, który jest kopiowany podczas ww. operacji.

__set, __get, __autoload

Metody magiczne __set oraz __get() są uruchamiane, gdy PHP nie znajdzie własności (zmiennej), do której odwołujesz się. Nie definiuj ich, jeśli ważna jest wydajność skryptu.

Następujące przykłady przedstawią przekazywanie danych do szablonu na 2 sposoby:

//Z użyciem metody __set() 
class Template 

  protected $data; 

  function __set($name,$val)
  { 
    $this->name = $val;
  }
}

$tpl = new Template;
$tpl -> title = 'Tytuł';

//Z użyciem własnej metody 
class Template
{
  protected $data;

  function set($name, $val)
  {
    $this->data[$name] = $val;
  }
}

$tpl = new Template;
$tpl->set('title', 'Tytuł');

Własności czy metody?

Wywołanie metody jest 2 razy wolniejsze od wywołania zwykłej funkcji. Natomiast czas dostępu do własności obiektu jest niewiele większy niż do globalnych zmiennych (poza klasą lub funkcją).

Programiści często definiują metody służące tylko do zmiany lub odczytu wartości własności obiektu, nadając większą rangę skalowalności aplikacji i hermetyzacji niż wydajności. Zastanów się jednak, czy celem takiej metody jest wcześniejsza obróbka danych, czy tylko zwrócenie lub przypisanie wartości do zmiennej obiektu.

$mail -> title = 'Tytuł';
$mail -> setTitle('Tytuł');

Przestrzegaj typów danych

Dużym ułatwieniem podczas tworzenia skryptów PHP jest to, że PHP automatycznie konwertuje typy danych. Jeżeli funkcja bądź instrukcja językowa wymaga przekazania liczby całkowitej integer, a otrzyma ciąg znaków (np. '70' zamiast 70), interpreter zamieni go na liczbę całkowitą i na odwrót.

Aby uniknąć zbędnych rzutowań typów danych, staraj się tak zaprojektować aplikację, aby zminimalizować ich ilość.

Aby porównać 2 wartości lub zmienne z uwzględnieniem typów danych, użyj operatora ===.

if($a === $b) { }

Operacje dyskowe

Operacje dyskowe (np. dołączanie plików, skanowanie katalogów), czyli wszystkie czynności wymagające odczytu lub zapisu danych na HDD, są czasochłonne. Nie dołączaj plików bez potrzeby. Oto kilka porad:

  • Nie dołączaj kilka razy tego samego skryptu.

  • Jeśli dla każdego żądania skanujesz zawartość katalogu, rozważ stworzenie cache (gotowej listy).

  • Ładuj biblioteki do pamięci tylko wtedy, gdy są potrzebne

  • Nie dziel kodu aplikacji na zbyt wiele plików, który musi być zawsze dostępny.

Include + include_path

Domyślnie instrukcja include sprawdza, czy plik, który chcemy dołączyć dołączyć, nie znajduje się w którymś z katalogów wymienionych w dyrektywie include_path. Zaletą takiego rozwiązania jest to, że nie musimy podawać ścieżki do pliku, jeśli jest w include_path. Aby PHP szukał pliku tylko w bieżącym lub określonym folderze, poprzedź ścieżkę symbolem ./

//Najpierw w include_path
include('plik.php');

//Tylko w bieżącym folderze
include('./plik.php');

//Tylko w wyższym folderze
include('../sth.php');

Powyższe uwagi dotyczą także instrukcji require, która różni się tym, że w razie niepowodzenia wyświetla błąd krytyczny i przerywa dalszą interpretację skryptu.

Instrukcja switch

Gdy chcesz wykonać określone akcje w zależności od wartości zmiennej, zamiast pisać kilka razy if / elseif / else, użyj instrukcji switch:

$x = 5; 
switch($x) 
{
   case 1:
      /* Akcja 1 */ 
      break; 
   case 2:
   case 3:
      /* Akcja 2 */
      break; 
   default: 
      /* Domyślna akcja */
}

Zakończ każdy bloku case instrukcją break; oprócz default.

Testy pod obciążeniem

Zbadaj też wydajność skryptu pod większym obciążeniem serwera. Służy do tego program Apache Benchmark rozprowadzany wraz z Apache 2. Z jego pomocą dowiesz się, ile żądań serwer jest w stanie obsłużyć podczas 1 sekundy na wykorzystanym sprzęcie oraz ile trwa złożenie 1 strony.

W systemie Windows znajduje się on w katalogu bin w folderze, w którym Apache został zainstalowany, np. C:\Apache2\bin\ab.exe.

  1. Otwórz wiersz poleceń, wpisując cmd w oknie Uruchom. Skrót klawiszowy: WINDOWS+R.

  2. Przejdź do katalogu z programem i uruchom go z opcją -h, aby wyświetlił pomoc i sposób użycia. Przykład:

    cd C:\Apache2\bin
    ab -h

  3. Narzędzie pokaże listę opcji, których można użyć przy badaniu wydajności skryptu. Aby wykonać 500 żądań przy 10 jednocześnie konkurujących ze sobą, wpisz:

    ab -n 500 -c 10 http://localhost/strona.php

Aby zwiększyć precyzję, zakończ zbędne procesy w systemie, które wykorzystują moc procesora. Test możesz powtórzyć kilka razy. Nie ustawiaj zbyt małej ilości powtórzeń, gdyż niektóre liczby zostaną obliczone niedokładnie.

Zapytania do bazy danych

Zazwyczaj najwięcej czasu zajmują niezoptymalizowane lub skomplikowane zapytania do bazy danych. Staraj się zmniejszyć ich ilość. W dalszej części artykułu dowiesz się, jak zoptymalizować zapytania SQL.

Indeksy

Utwórz indeksy na polach, do których odwołujesz się w zapytaniu, np. po słowie WHERE lub ON. W strukturze tabeli najważniejszy jest klucz podstawowy, np. na polu ID. Załóżmy, że chcesz wyświetlić artykuł o ID=5. Wysyłasz następujące zapytanie:

SELECT * FROM `articles` WHERE ID = 5;

Silnik bazy danych nie musi przemierzać wszystkich rekordów. Indeksy zawierają pozycję początków wierszy, a więc dotarcie do celu jest bardzo szybkie.

Pozostałe typy kluczy to:

  • zwykłe – KEY

  • unikalne – UNIQUE KEY – każdy rekord musi mieć inną wartość w tym indeksie niż pozostałe

  • obce – FOREIGN KEY – gdy wartość klucza w 1 tabeli zostanie zmieniona, ulegnie ona zmianie także w tabelach, które łączy ze sobą (w MySQL dostępny tylko w silniku InnoDB)

  • pełnotekstowe – FULLTEXT (MySQL: tylko w silniku MyISAM)

Łączenie tabel

W zapytaniach z użyciem JOIN kolumny, wg których łączysz tabele (po słowie ON), powinny zawierać indeks. W ten sposób silnik znajdzie istotne rekordy szybciej.

Wyrażenie WHERE

Spójrz na poniższy przykład:

SELECT * FROM `tabela` WHERE `num`+10 > 100

Zakładamy, że 100 jest liczbą wejściową z PHP. Zoptymalizujmy teraz to zapytanie. Silnik SQL powinien od razu policzyć wartość wyrażenia 100-90, jednak można to zrobić w PHP, np. ($num – 10). Do serwera SQL wysyłamy zaś:

SELECT * FROM `tabela` WHERE `num` > 90

Wyrażenie EXPLAIN

Za pomocą słowa EXPLAIN dowiesz się więcej na temat analizy zapytania przez bazę danych, np. czy klucze są wykorzystywane. W MySQL dostępne jest też słowo EXTENDED, wyświetlające szczegółowe informacje.

EXPLAIN EXTENDED SELECT * FROM `articles` WHERE author = 1 AND access = 1;

Predefiniowane zapytania

Aby przekazać informacje do bazy danych bezpiecznie i szybko, nie narażając się na groźny atak SQL Injection, wykorzystaj nowe możliwości serwerów bazodanowych. W omawianym przypadku są to predefiniowane zapytania (ang. `prepared statements`).

W przykładzie demonstrującym zasadę ich działania wykorzystamy rozszerzenie PDO stworzone do obsługi wielu baz danych. Zakładamy, że $db jest obiektem klasy PDO.

  1. Przygotuj szkielet zapytania za pomocą metody prepare().

    $q = $db->prepare('UPDATE items SET title = :title, txt = :txt WHERE ID = :id');

  2. Wprowadź dane do zapytania. Możesz to zrobić na 2 sposoby – za pomocą metod bindValue() lub bindParam() (z użyciem referencji) bądź przekazując je od razu do metody execute().

    $q->bindValue('title', $_POST['title']);
    $q->bindValue('txt', $_POST['text']);
    $q->bindValue('id', $id, 1);
    $q->execute();

    //Sposób 2
    $data = array(
    'title' => $_POST['title'],
    'txt'   => $_POST['text'],
    'id'    => (int)$id
    );
    $q->execute($data);

Jeśli za jednym razem dodajesz do bazy danych wiele pozycji, wystarczy podpiąć nowe dane do istniejącego już zapytania. W ten sposób można osiągnąć lepszą wydajność i bezpieczeństwo.

W przypadku, gdy dane pochodzące z zewnątrz nie powinny zawierać kodu HTML, w szczególności wysłane przez internautów, należy użyć funkcji htmlspecialchars().

$data = array(
'title' => htmlspecialchars($_POST['title']),
);

Obejmij transakcją zbiór zapytań, które modyfikują dane

Porada dotyczy szczególnie bazy danych SQLite oraz tabel InnoDB w MySQL. Gdy wysyłasz kilka zapytań pod rząd, które wprowadzają jakiekolwiek zmiany, np. INSERT, UPDATE, DELETE, dla każdego z nich jest tworzona osobna transakcja. Operacja może trwać długo. Aby przyspieszyć modyfikację rekordów, obejmij cały zbiór zapytań transakcją. Baza danych od razu obliczy i stworzy odpowiednią ilość miejsca dla nowych wierszy w systemie plików lub usunie wszelkie pozostałości.

Dlaczego transakcje są kluczowym elementem komunikacji z bazą danych? Rozważ następujący przypadek w sklepie internetowym. Internauta kupuje produkt, lecz informacja o dokonaniu płatności nie może zostać zapisana z nieznanej przyczyny. Inny przypadek. Podczas tworzenia nowej ankiety występuje błąd przy zapisie odpowiedzi do bazy danych. Jak temu zapobiec? Jednym z najlepszych i najbezpieczniejszych rozwiązań są transakcje w połączeniu z instrukcją try {} catch(PDOException $e) {}.

Przykład modyfikacji danych objętej transakcją:

try
{
    //Rozpocznij transakcję
    $db->beginTransaction();

    //Zmodyfikuj dane
    $db->query('INSERT INTO tabela ...');
    $db->query('UPDATE tabela SET ...');

    //Zatwierdź zmiany
    $db->commit();
}
catch(PDOException $e)
{
    /* Nieudana operacja */
}

Jeżeli oczekujesz, że inne komponenty mogą rzucić wyjątek, przechwyć podstawową klasę Exception zamiast PDOException lub złap PDOException w pierwszej kolejności. Wolno umieścić dowolną ilość instrukcji catch.

UWAGA! Tabele MyISAM w MySQL nie obsługują transakcji! Nie ma znaczenia, czy zapytania zostaną objęte transakcją. Nie bój się więc używać transakcji przy tworzeniu aplikacji z obsługą wielu baz danych.

Wyniki testu szybkości wstawiania 500 rekordów w SQLite:
+ bez transakcji: 64 sekundy
+ z transakcją: 0.15 sekundy

GROUP BY zamiast wielu zapytań

Rozważ algorytm, który liczy ilość artykułów w każdej kategorii. Można to zrobić na 2 sposoby.

SELECT COUNT(*) FROM articles WHERE cat = 5

Powyższe zapytanie trzeba wysłać dla każdej kategorii, co może chwilę potrwać. Aby przyspieszyć operację, użyj instrukcji grupującej GROUP BY. Wystarczy wysłać wtedy tylko 1 zapytanie. Baza danych zwróci ilość artykułów dla poszczególnych wartości w polu `cat`. Przykład:

SELECT COUNT(*) FROM articles GROUP BY cat

Aby dołączyć dodatkowy warunek, umieść instrukcję WHERE przed GROUP BY:

SELECT COUNT(*) AS ile FROM articles WHERE on = 1 GROUP BY cat

Licencja: Creative Commons - użycie niekomercyjne