W związku z dyskusją jaka się wywołała między mną, a yarpo w komentarzach pod moim artykułem dotyczącym logowania w PHP, skorzystałem z propozycji i zrobiłem kilka testów wydajnościowych porównujących szybkość działania bazy danych opartej na plikach i takiej opartej na MySQL. Stanowiska były następujące: ja twierdziłem, że pliki są szybsze, yarpo, że niekoniecznie.
Testy zostały przeprowadzone na małej (275 userów) i dużej (27057 userów) porcji danych, z wykorzystaniem bazy MySQL oraz plików tekstowych; przy nich była brana pod uwagę szybkość dodawania oraz szybkość wyszukiwania. Parametry środowiska testowego (serwera):
{codecitation}
Procesory: 2 x Intel(R) Core(TM) i7 CPU X 980 @ 3.33GHz
RAM: 1362160 kB/24738796 kB
Apache: 2.2.17
MySQL: 5.1.56
Silnik bazy danych: MRG_MYISAM
{/codecitation}
Kody, które służyły do testów:
Baza na plikach:
{codecitation class='brush: php'}
include 'profiler.php';
/**
* Pliki tekstowe
*/
/// Tworzenie bazy użyszkodników
$file = fopen("db.php", "a");
$time1 = new Profiler();
fwrite($file, "
for($i = 0; $i < 275; ++$i)
{
$user = HTMLSpecialChars(chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)));
$db[] = array('user' => $user, 'passwd' => hash('sha256', $user));
} // End for
fwrite($file, '$db = '.var_export($db, true).';');
echo 'Wpisywanie trwało: ' . $time1->stop()."\n";
fclose($file);
/// Szukanie
include 'db.php';
$time2 = new Profiler();
for($i=0, $x = count($db); $i < $x AND $db[$i]['user'] != 'k+]?N]Z+8' AND $db[$i]['passwd'] != '2895c383bd3c5ba1a6672d0759f88c0f692fd90bd9298e8ce572eac8cf042373'; ++$i);
echo 'Szukanie trwało: ' . $time2->stop();
{/codecitation}
Baza danych:
{codecitation class='brush: php'}
include 'profiler.php';
/**
* Baza danych
*/
$conn = mysql_connect("localhost", "adm", "aaaa");
mysql_select_db("tomekby_test");
/// Tworzenie bazy użytkowników
$count1 = mysql_fetch_row(mysql_query("SELECT COUNT(*) FROM `users`;"));
$time1 = new Profiler();
for($i = 0; $i < 300; ++$i)
{
$user = HTMLSpecialChars(chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)).chr(rand(32, 126)));
$q = "INSERT INTO `users` (`id`, `user`, `passwd`) VALUES (NULL, '".$user."', '".hash('sha256', $user)."');";
mysql_query($q);
} // End for
echo 'Wpisywanie trwało: ' . $time1->stop();
/// Liczenie dodanych rekordów
$count2 = mysql_fetch_row(mysql_query("SELECT COUNT(*) FROM `users`;"));
echo "\nDodane: ".($count2[0] - $count1[0]);
/// Szukanie
$time2 = new Profiler();
mysql_query("SELECT * FROM `users` WHERE `user` = 'k+]?N]Z+8' AND `passwd` = '2895c383bd3c5ba1a6672d0759f88c0f692fd90bd9298e8ce572eac8cf042373';");
echo "\n".'Szukanie trwało: ' . $time2->stop();
mysql_close($conn);
{/codecitation}
Profiler: (tu podziękowania dla yarpo za klasę, bo nie chciało mi się samemu pisać :p)
{codecitation class='brush: php'}
// autor Patryk yarpo Jar, klasa Profiler pozwala na mierzenie czasu wykonania skryptu
class Profiler {
private $start = false;
private $stop = false;
private $duration = false;
public function __construct() {
$this->start = $this->getCurrentTime();
}
private function getCurrentTime() {
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
private function measureDuration() {
if (false != $this->stop or false != $this->start) {
$this->duration = $this->stop - $this->start;
return $this->duration;
}
return false;
}
public function start() {
$this->stop = false; // skoro zaczelismy, to popredni stop jest niewazny
$this->duration = false; // nie wiemy kiedy konie, wiec skad mamy znac dlugosc?
$this->start = $this->getCurrentTime(); // aktualny czas
return $this->start;
}
public function stop() {
$this->stop = $this->getCurrentTime();
return $this->measureDuration();
}
public function getStartTime() {
return $this->start;
}
public function getStopTime() {
return $this->stop;
}
public function getTime() {
// jeszcze nie obliczono trwania
if (false === $this->duration) {
return $this->stop();
}
return $this->duration;
}
}
{/codecitation}
Jak widać do obu baz byli dodawani użytkownicy o losowo generowanych nazwach i hasłach, a później wyszukiwany jakiś użytkownik, który niekoniecznie musiał istnieć.
Oba kody spokojnie można by jeszcze zoptymalizować, ale więcej mówi raczej typowy przypadek. W żadnym z testów nie uwzględniłem czasu otwarcia połączenia/wybrania bazy czy otwarcia/include'a pliku, aby pokazać sam czas operacji na danych (BTW mam nieodparte wrażenie, że otwarcie połączenia i wybranie bazy jest bardziej czasochłonne od otwarcia pliku...). Baza danych znajdowała sie na tym samym serwerze, co skrypt (co widać po wpisaniu localhost w hoście bazy).
Teoretycznie można by się pokusić o trochę bardziej realistyczny przypadek, czyli otwieranie/zamykanie za każdym razem pliku/połaczenia z bazą, aby zasymulować operacje wykonywane przez różnych użytkowników (bo od kiedy jeden uzytkownik wykonuje ponad 50 tysięcy operacji na bazie danych w jednym czasie? :>), ale nie jestem pewien, jak serwer by zareagował na takie obciążenie i wolę go nie przeciążać za bardzo.
Wyniki z użyciem plików:
{codecitation}
Test na 27057 rekordach
Wielkość pliku: 3,59 MiB
Wpisywanie trwało: 0.197447061539
Szukanie trwało: 0.0108730792999
Test na 275 rekordach
Wielkość pliku: 36,91 KiB
Wpisywanie trwało: 0.00179982185364
Szukanie trwało: 0.000112056732178
{/codecitation}
Wyniki z użyciem bazy danych:
{codecitation}
Dodane: 27057
Wielkość tabeli: 3,12 MiB
Wpisywanie trwało: 1.76564502716
Szukanie trwało: 0.00290703773499
Dodane: 275
Wielkość tabeli: 34,00 KiB
Wpisywanie trwało: 0.0166158676147
Szukanie trwało: 0.00014591217041
{/codecitation}
Jak widać, im więcej rekordów tym większą wydajność (w stosunku do plików) oferuje baza danych. Na niewielkiej porcji danych pliki tekstowe są niewiele szybsze od bazy przy wyszukiwaniu, natomiast o wiele szybsze przy zapisie. Prawdę mówiąc zdziwiła mnie tak duża różnica prędkości zapisu między bazą danych a plikami. Spodziewałem sie, że te drugie będą szybsze, ale nie prawie 10 razy! Niestety pliki mają także swe wady - przy dużej ilości danych może nam po prostu zabraknąć pamięci, gdyż cała tablica z pliku tekstowego musi zostać wczytana do pamięci, a przy kilku(dziesięciu|set) tysiącach użytkowników te dane mogą sporo zajmować (przede wszystkim przy bardziej rozbudowanych strukturach, nie tylko uzytkownik/hasło). Dodatkową ciekawostką może być fakt, iż tabela bazy danych MySQL waży mniej niż plik tekstowy.
Mam teraz wrażenie, że jakimś cudem udało mi się obalić dowód przez oczywistość :P
W razie wszelkich sugestii śmiało pisać - postaram się wybronić ze swego stanowiska.
ps. początkowo to miał być zwykły komentarz, ale się "trochę" za bardzo rozrósł...
ps2. prawie zapomniałem: paczka z kodem
//edit: Nowe testy:
Tym razem porównanie między szybkością otwarcia połączenia, wybrania bazy i zamknięcia połączenia oraz między czasem otwarcia, zablokowania i zamknięcia pliku. Tym razem nie będę się aż tak rozpisywal, bo już sił brakuje - może później ;]
Kod użyty do testowania:
{codecitation class='brush: php'}
include 'profiler.php';
/// Łączenie/odłączanie bazy danych
$time1 = new Profiler(); // Czas startu
for($i = 0; $i < 100000; ++$i)
{
$conn = mysql_connect('localhost', '123', '123456');
mysql_select_db('test');
mysql_close($conn);
} // End for
echo 'Operacje trwały: ' . $time1->stop()."\n"; // Czas końca
/// Otwieranie/zamykanie/blokowanie pliku
$time2 = new Profiler(); // Czas startu
for($i = 0; $i < 100000; ++$i)
{
$file = fopen('file', 'a');
if (flock($file, LOCK_EX))
flock($file, LOCK_UN);
fclose($file);
} // End for
echo 'Operacje trwały: ' . $time2->stop(); // Czas końca
{/codecitation}
Tym razem oba testy zostały umieszczone w jednym pliku. Jak widać, w obu przypadkach zostało wykonane 100.000 operacji. Wyniki (output skryptu) - najpierw baza danych, później pliki:
{codecitation}
Operacje trwały: 10.1349132061
Operacje trwały: 0.940469026566
{/codecitation}
Trzeba przyznać, że wyniki testu są druzgoczące - o ile przy zapisie/odczycie z bazy/pliku różnice były co najwyżej rzędu wielkości, to w tym wypadku różnica to ponad dwa rzędy wielkości (0,9s dla pliku w porównaniu do 10,1s dla bazy). Chociaż, patrząc z drugiej strony - nikt normalny chyba nie otwiera połączenia z bazą danych 100k razy za jednym zamachem? :>
jeszcze tradycyjna paczka z kodem :) Read more
|