Wydajność bazy danych, a wydajność plików tekstowych Drukuj
Ocena użytkowników: / 6
SłabyŚwietny 
Wpisany przez Tomasz Stasiak   
poniedziałek, 25 kwietnia 2011 15:58

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):


	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

	

Kody, które służyły do testów:

Baza na plikach:


	<?php

	        include 'profiler.php';

	        /**

	         * Pliki tekstowe

	         */

	        /// Tworzenie bazy użyszkodników

	        $file = fopen("db.php", "a");

	        $time1 = new Profiler();

	        fwrite($file, "<?php");

	         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();

	

Baza danych:


	<?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);

	

Profiler: (tu podziękowania dla yarpo za klasę, bo nie chciało mi się samemu pisać :p)

<?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;

	    }

	}

	

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:


	 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

	

Wyniki z użyciem bazy danych:


	 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

	

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:

<?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

	

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:


	Operacje trwały: 10.1349132061

	Operacje trwały: 0.940469026566  

	
 

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 :)