: Home / IT / dev zone / Wydajność bazy danych, a wydajność plików tekstowych

Warning: file_get_contents(http://graph.facebook.com/http://youthcoders.net/it/dev-zone/1035-wydajnosc-bazy-danych-a-wydajnosc-plikow-tekstowych.html): failed to open stream: HTTP request failed! HTTP/1.0 403 Forbidden in /var/www/old-yc/plugins/content/addthis.php on line 38
Wydajność bazy danych, a wydajność plików tekstowych
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 :)

 

Komentarze 

 
0 #7 Tomasz Stasiak 2011-04-26 12:33
spoja widziałem, skrypty w php pod konsolę także miałem okazję pisać, ale jak na razie brak werwy do zastanowienia się nad rozwiązaniem zadań
Cytować
 
 
0 #6 Patryk yarpo Jar 2011-04-26 09:38
Uwaga do tagów. Rozdzielaj tagi przecinkami :)
Cytować
 
 
0 #5 Patryk yarpo Jar 2011-04-26 09:37
Bardzo podoba mi się :)

Może cała seria takich testów? :)

Widzę, ze spodobała Ci się opcja optymalizacji kodu, aby działał jak najszybciej :)

Może więc zainteresuje Cię serwis:
http://pl.spoj.pl/problems/latwe/

Aby w PHP wczytywać dane z wejścia [odpowiednik cin z c++] należy:

Cytat:
<?php
echo "Are you sure you want to do this? Type 'yes' to continue: ";
$handle = fopen ("php://stdin","r");
$line = fgets($handle);
if(trim($line) != 'yes'){
echo "ABORTING!\ n";
exit;
}
echo "\n";
echo "Thank you, continuing...\n";
?>


źródło: http://php.net/manual/en/features.commandline.php

Miłej zabawy :)
Cytować
 
 
+1 #4 Tomasz Stasiak 2011-04-25 22:38
W pełni się zgadzam, choć warto także zauważyć że czasami (podobno się zdarza...) nie mamy na serwerze dostępu do bazy danych, dlatego trzeba sobie z tym jakoś radzić. Niestety, ale czasami, jak to powiadają "jak się nie ma co się lubi, to się lubi co się ma" :)
BTW dodane nowe testy do artykułu
Cytować
 
 
0 #3 Patryk yarpo Jar 2011-04-25 19:42
Warto oczywiście pamiętać o możliwości korzystania z pliku, i czasem może być to słuszne rozwiązanie.

Jednak mając na uwadze możliwość rozwoju naszego systemu często lepiej jest od razu dać bd. Lub odpowiednio obudować plik txt (np. w klasę implementującą jakiś interfejs) aby późniejsza zmiana pozwalała na wprowadzenie klasy gdzie dane trzymane byłyby w bazie, a interfejs pozostałby taki sam :)

Pamiętaj, że czas pracy programisty jest często ważniejszy niż czas pracy komputera.
Cytować
 
 
+1 #2 Tomasz Stasiak 2011-04-25 18:18
Cytat:
jak widać odczyt zawsze był dużo szybszy. O rząd. Jest to druzgocąca porażka plików.

przy małej ilości danych odczyt z pliku jest szybszy (no dobra, w granicach błędu pomiarowego, ale jest ;]), natomiast przy wzroście ilości danych...
Cytat:
Zapis następuje raz: przy rejestracji. Odczyt wielokrotnie - przy każdej autentykacji (np. logowaniu).

No, w sumie nie raz, bo to samo jest przy wszelkich zmianach dotyczących ustawień konta (choć masz rację, że występuje wiele razy mniej niż odczyt).
Cytować
 
 
0 #1 Patryk yarpo Jar 2011-04-25 18:05
jak widać odczyt zawsze był dużo szybszy. O rząd. Jest to druzgocąca porażka plików.

Zapis następuje raz: przy rejestracji. Odczyt wielokrotnie - przy każdej autentykacji [np. logowaniu].

Warto było jednak dodać:

1. czas połączenia, jako kolejny test :)
2. flock na pliki. Bez tego [miałem już takie przypadki] pliki potrafią się np. opróżnić, czego tu byśmy raczej nie chcieli :)
http://php.net/manual/en/function.flock.php

bardzo ciekawy artykuł! :)
Cytować
 

Dodaj komentarz


Kod antysapmowy
Odśwież