Порівняння швидкодії БД (PostgreSQL, MySQL, SQLite)

Опубліковано: 2009-09-03   08:00:55

PostgreSQLДумаю, найбільш популярною на сьогодняшній день серед розробників є БД MySQL. Досить простий у використанні, розповсюджений серед хостерів, більшість CMS орієнтовані саме на нього, а починаючи з 5ої версії в цій системі з'явилось багато можливостей, притаманних корпоративним БД. І список можливостей продовжує рости. Розробники та прихильники MySQL досить часто стверджують, що він дуже швидкий, гнучкий та здатний до масштабування.

Однак, не дивлячись на те з MySQL все так добре та чудово, самеPostgreSQL використовується для збереження данних в більшості великих соціальних мереж, саме нею скористалось Yahoo! для побудови найбільшої в світі БД. Цікаво, що прихильники PostgreSQL з гордістю кажуть, що їх улюбленець значно швидший за MySQL, ну а його прихильники в свою чергу доволі часто кажуть протилежне.

А тут ще SQLite, що займає доволі потужні позиції в desktop-орієнтованому софті, а ще дехто каже про чудові можливості Firebird. І це тільки найпопулярніші з opensource баз данних.

Втомившись читати купу різних порівнянь, вирішив провести для себе самого ряд простих тестів та нашвидкоруч оцінити як поводять себе деякі БД

"Методика" тестування

Мій спосіб перевірки швидкодії не претендує ні на звання "серйозної методики". Мене просто цікавило, скільки запитів того чи іншого виляду витримає та чи інша БД. Причому для тесту було обрано найпримітивніші запити на запис та читання. Це синтетичний тест, погляд лише з одного боку. Однак чим є БД за своєю суттю? Інструментом для збереження данних. А які операції є основними? Читання та запис. Тому не дивлячись на свою синтетичність, леяку інформацію про БД він надає.

Оскільки в мене завжди на робочому місті встановлено MySQL та PostgreSQL, а для роботи з SQLite практично ніякого додаткого інструментарію непотрібно, то для тесту було обрано саме ці БД. Причому ніякої додаткової конфігурації ні для MySQL ні для PostgreSQL не проводилось. Хоча б тому що я не вважаю себе знавцем тонкого налаштування серверів БД і мої маніпуляції могли б зробити тільки гірше.

Для уніфікації тестів для кожної з БД я написав ось такий абстрактний клас, що є прототипом для тесту БД:

/**
* Abstract class for test running
*/
abstract class AbstractDatabaseTest {
protected $connection;
/**
* Creates test suite, connect to database and create table
*/
public function __construct($host, $login, $password, $database) {
$this->connect($host, $login, $password, $database);
$this->createTable();
}
/**
* Close drop table
*/
public function __destruct() {
$this->query('DROP TABLE test');
$this->disconnect();
}
/**
* Abstract function for query executing
*/
public abstract function query($query);
/**
* Abstract function fot table creation
*/
public abstract function createTable();
/**
* Abstract method for creating database connection
*/
public abstract function connect($host, $login, $password, $database);
/**
* Abstract method for closing connection
*/
public abstract function disconnect();
/**
* Method for running test
*/
public function runTest($query, $times) {
for ($i = 0; $i < $times; $i++)
$this->query(preg_replace("/[\?]/", $i, $query));
}
/**
* Create single query for all requests
*/
public function runSingleTest($query, $times, $exec = true) {
$q = '';
for ($i = 0; $i < $times; $i++)
$q .= preg_replace("/[\?]/", $i, $query).";\n";
if ($exec)
$this->query($q, $i);
else
return $q;
}
/**
* Run test as transaction
*/
public function runTransactionTest($query, $times) {
$q = 'BEGIN;'."\n";
$q .= $this->runSingleTest($query, $times, false);
$q .= 'COMMIT;';
}
}

Цей класс декларує конструктор та деструктор, що в свою чергу звертаються до абстрактний функцій connect() та disconnect(), які мають бути перевантажені в нащадках з метою отримання підключення до серверу БД та його безпечного закриття. Також предокларовані методи для виконання тестів (runTest(), runSingleTest(), tunTransactionTest()), які будуть звертатись до низькорівневих функцій роботи з підключенням через абстрактний метод query(). Ну і залишається ще одна функція, що створює тестову таблицю.

Тоді для тесту наприклад PostgreSQL достатньо описати такий клас:

/**
* Class for working with PostgreSQL database
*/
class PostgreSQLTest extends AbstractDatabaseTest {
/**
* Creates database object
*/
public function connect($host, $login, $password, $database) {
$this->connection = pg_connect('host='.$host.' dbname='.$database.' user='.$login.' password='.$password)
or die('Could not connect to database: '.pg_last_error($this->connection));
}
/**
* Close connection
*/
public function disconnect() {
pg_close($this->connection);
}
/**
* Create database table
*/
public function createTable() {
pg_query($this->connection, 'CREATE TABLE test
(
id serial,
"name" character varying(255),
o integer,
CONSTRAINT id_k PRIMARY KEY (id)
)');
}
/**
* Execute query
* @param query query for execution
*/
public function query($query) {
pg_query($this->connection, $query)
or die('Could not execute query: '.pg_last_error($this->connection)."\n".$query."\n");
}
}

Як можна побачити, для тестування створюється таблиця, що містить три поля: id (ключ), name (текстове поле), o (цілочислене поле). Таким чином можна протестувати вибірки за різним типом порівнянь. Аналогічні класи я створив для SQLite та два для MySQL (InnoDB та MyISAM).

Тепер сам тест:

</?php
**
>runSingleTest($query, $times);
} elseif ($testType == 'trans') {
$msg = ' in transaction';
$test->runTransactionTest($query, $times);
} else {
$test->runTest($query, $times);
}
$end = microtime(true);
$summary += ($end-$begin);
}
// Put time
$summary = $summary/$iters; // Try to get average time
echo $type, ' (', $times, ' times'.$msg.') takes ', "\t\t", $summary, " sec.\n";
return array($type => $summary);
}
/**
* Static method for test running
*/
public static function run($className, $host, $user, $pass, $database, $count = 1000) {
$results = array();
// Usual test
$test = new $className($host, $user, $pass, $database);
$results[] = self::singleTest($test, 'INSERT INTO test (name, o) VALUES (\'record?\', ?)', $count, 'INSERT');
$results[] = self::singleTest($test, 'SELECT * FROM test WHERE id = ?', $count, 'SELECT ID');
$results[] = self::singleTest($test, 'SELECT * FROM test WHERE o = ?', $count, 'SELECT INT');
$results[] = self::singleTest($test, 'SELECT * FROM test WHERE name = \'record?\'', $count, 'SELECT TEXT');
$results[] = self::singleTest($test, 'SELECT * FROM test WHERE name like \'%?\'', $count, 'SELECT LIKE');
$results[] = self::singleTest($test, 'DELETE FROM test WHERE id = ?', $count, 'DELETE');
unset($test);
// Prepare for output
$prepared = array();
foreach ($results as $res)
foreach ($res as $key => $value)
$prepared[$key] = $value;
return $prepared;
}
}
< * Class for running tests
*/
class Test {
/**
* Test for running multiply-query, signle-query and transtactional test on demand
* @param test test connection
* @param query query fo execution
* @param times how many requests will be transmitted
* @param type additional message about request type
*/
public static function singleTest(AbstractDatabaseTest $test, $query, $times, $type, $testType = 'usual') {
// Select iterations count
/* TODO: uncomment it later
if ($times = 1000)
$iters = 50;
elseif ($items = 5000)
$iters = 10;
else */
$iters = 1;
// Evaluate time
$summary = 0;
for ($i = 0; $i $iters; $i++) {
$begin = microtime(true);
$msg = '';
if ($testType == 'single') {
$msg = ' in single query';
$test-/code>
Як видно з коду спочатку виконуються вставки в БД, потім вибірки за ключем, цілочисельним полем, повним порівнянням текстового поля та шаблонним порівнянням через <strong>like</strong>.
Для збірки всього до купи, читання конфігураційного файлу та збереження результатів у вигляді html було написано ось такий код:
<code class="code-block">
// Get configuration file
$config = (isset($args[1]))? $args[1]: 'config.conf';
if (!file_exists($config))
die('Error:'."\t".'file '.$config.' does not exists'."\n");
// Read configuration
$configuration = array();
if (!($file = fopen($config, 'r')))
die('Error:'."\t".'can open config file for reading: '.$config."\n");
while ($parsed = fgetcsv($file, 1000, '=')) {
// Check format
if (count($parsed) != 2) {
var_dump($parsed);
die('Error:'."\t".'wrong configuration file format '."\n");
}
// Parse line
if (preg_match("/,/", $parsed[1]))
$vals = explode(",", $parsed[1]);
else
$vals = array($parsed[1]);
// Add parameters
if (!isset($configuration[$parsed[0]]))
$configuration[$parsed[0]] = array();
foreach ($vals as $val)
$configuration[$parsed[0]][] = $val;
}
fclose($file);
// Load classes
include 'lib/abstract.php';
include 'lib/test.php';
foreach ($configuration['libs'] as $lib) {
include 'lib/'.$lib.'.php';
}
// Output file
$result = (isset($args[2]))? $args[2]: 'results.html';
$output = fopen($result, "w");
// Counts
fwrite($output, '
<html>
<head>
<title>Database test</title>
</head>
<body>
<h1>Database test</h1>
'
);
$types = array('INSERT', 'SELECT ID', 'SELECT INT', 'SELECT TEXT', 'SELECT LIKE', 'DELETE');
foreach ($configuration['count'] as $count) {
fwrite($output, '<h2>'.$count.' items</h2>
<table>
<tr>
<th> </th>
');
$results = array();
foreach ($configuration['database'] as $db) {
fwrite($output, "\t\t\t<th>$db</th>\n");
$results[$db] = Test::run($db, $configuration['host'][0], $configuration['user'][0],
$configuration['pass'][0], $configuration['db'][0], $count);
}
fwrite($output, "\t\t</tr>\n");
foreach ($types as $type) {
fwrite($output, "\t\t<tr>\n\t\t\t<td>$type</td>\n");
foreach ($configuration['database'] as $db)
fwrite($output, "\t\t\t<td>".$results[$db][$type]."</td>\n");
fwrite($output, "\t\t</tr>\n");
}
fwrite($output, "\t</table>\n");
}
fwrite($output, '</body>
</html>');

Настав час погратись.

Результати тестів

Спочатку я провів тестування на старенькій машинці з Amd Sempron 2500+ 1 Gb DDR з файловою системою raiserfs під управлінням Ubuntu 9.04 для 1000, 5000 та 10000 елементів. Отримав такі результати:

1000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 8.46631503105 0.182038068771 0.922454833984 1.3453848362
SELECT ID 0.140038013458 0.401347875595 0.375024080276 1.090269804
SELECT INT 0.825850963593 1.01993608475 1.38959908485 1.50171399117
SELECT TEXT 0.847486019135 1.13318300247 1.46317386627 1.63713502884
SELECT LIKE 1.00699400902 1.10880994797 1.43363499641 1.72174596786
DELETE 8.6017370224 0.277512073517 1.48435211182 1.46303582191

5000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 48.9740638733 0.897409915924 4.29658889771 6.77726101875
SELECT ID 0.521908998489 1.68041110039 1.66038298607 5.13580799103
SELECT INT 18.8576068878 17.7570590973 26.9309771061 16.0441360474
SELECT TEXT 18.7875778675 19.5907330513 28.7952878475 19.5119550228
SELECT LIKE 23.2714881897 19.961373806 28.3384401798 20.7058980465
DELETE 44.6153440475 1.21550679207 7.36333107948 7.01828098297

10000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 96.4258060455 1.78123998642 8.63258886337 13.5855839252
SELECT ID 0.983808994293 3.49806404114 3.36594605446 10.2117769718
SELECT INT 78.3077938557 65.505810976 95.4381818771 53.9780220985
SELECT TEXT 72.8628439903 103.212995052 71.3834159374 69.0468411446
SELECT LIKE 89.72281003 107.325515985 72.6874220371 71.0193040371
DELETE 88.7086949348 2.29328393936 15.0134780407 13.882789135

Цього мені виявилось замало, і я вирішив спробувати провести для більшої кількості елементів. Хоча б ще для 50 та 100 тис. Оскільки тести на такій кількості елементів займають доволі багато часу, то їх я провів на іншій машині: Intel Atom, 1 Gb DDR2, ext3 і тією самою Ubuntu 9.04 з ядром 2.6.30. І отримав ось такі результати:

1000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 3.42623877525 0.485679149628 1.01362395287 1.63590288162
SELECT ID 0.519760131836 1.09079289436 0.573807001114 1.17772603035
SELECT INT 2.56960511208 2.94936800003 2.23329114914 1.9480099678
SELECT TEXT 2.33323192596 3.13713407516 2.38640213013 2.154296875
SELECT LIKE 2.18005108833 2.80790996552 2.43208599091 2.29287910461
DELETE 3.46106314659 0.81547498703 0.937149047852 1.56592297554

5000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 16.860270977 3.05509400368 4.44511294365 8.22729110718
SELECT ID 1.90315794945 3.12027382851 2.6133749485 6.00253605843
SELECT INT 35.5371229649 30.0901350975 44.3228580952 25.8845059872
SELECT TEXT 32.2936480045 33.9316368103 48.9920039177 29.3765230179
SELECT LIKE 41.3553678989 35.6514999866 49.3500881195 33.2532720566
DELETE 17.458812952 2.04428100586 5.16926288605 8.14243984222

10000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 35.5141940117 5.62814283371 8.56925797462 16.4258570671
SELECT ID 3.68663096428 7.88118100166 5.21180105209 11.7483720779
SELECT INT 130.809201956 116.798990965 169.440524817 90.0897068977
SELECT TEXT 125.267574072 128.374459982 188.359474897 105.278270006
SELECT LIKE 160.688622952 135.743841887 188.13747716 117.56459713
DELETE 34.9218218327 3.94423508644 10.6728608608 16.3871839046

50000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 186.580451012 27.5768818855 42.6071000099 78.3421108723
SELECT ID 17.9766161442 38.9252171516 25.6919820309 55.8121678829
SELECT INT 3373.11729312 2714.17641687 4097.4976809 1971.63789201
SELECT TEXT 3319.83222294 3077.92565894 4539.87472916 2321.9553771
SELECT LIKE 4336.35408092 3266.41011596 4621.69911408 2826.56725407
DELETE 167.727796078 19.2260329723 47.8846509457 79.9886329174

100000 items

  SQLiteTest MySQLTestIsam MySQLTestInno PostgreSQLTest
INSERT 380.183732986 57.4253020287 87.9900531769 154.548943996
SELECT ID 37.0985400677 65.9570541382 54.8159809113 114.572535038
SELECT INT 13562.0152919 11314.5157359 19920.4457819 8018.85069585
SELECT TEXT 15053.0083721 12835.214098 21606.1742871 9508.44170213
SELECT LIKE 19123.6498909 13622.2726769 18889.7097459 11153.777102
DELETE 353.978772879 40.8035359383 96.7261219025 192.977801085

Ну і хто ж кращий?

Перше, на чому зверну увагу: SQLite дуже швидко вибирає елементи за первинним ключем. Причому навіть при великій кількості елементів в БД. Також при обсязі записів до 5 тис. елементів його швидкодія для простих вибірок йде на рівні або навіть випереджає інши БД. Однак швидкість запису та видалення елементів є для нього не простою задачею, внаслідок того, що вся БД знаходиться в одному єдиному файлі. Однак враження від цієї БД в мене залишилось дуже гарне і я при необхідності зберігати дані в програмах, що мають працювати локально, з великим задоволенням скористаюсь цією БД. Дарма я не звернува уваги на цю БД раніше, колись би зберіг обі дуже багато часу.

Тепер про MySQL та PostgreSQL. Другий виявився повільнішим на вставках та видаленні і що не дуже приємно при виборі за первинним ключем. Повільні вставки мене не лякають, а от швидкість виборок за ключем мене дійсно не радує.

Що означає на практиці повільніша швидкість вставок? Приведу приклад в рамках абстрактного сайту: один перегляд сторінки вимагає вибрати список коментарів, список статей, список тегів, якісь налаштування, посилання, коментарі. Наприклад, CMS Joomla 1.5 виконує десь 13 запитів при перегляді головнох сторінки (жахливо, чи не так). Нехай, відвідуваність у проекту буде така як і у мого блога, тобто хочаб 10 уніків щодня, які роблять хоча б 20 переглядів. Тобто близько 250-260 запитів на вибірку до БД. А скільки коментарів залишать ці читачі? Добре, якщо 10% читачів будуть окментувати. Тобто за день середньостатистично буде 1 запит на вставку коментаря, ну і якби я писав кожного дня то зі збереженнями тексту в процесі написання вийшло б десь 10 запитів на запис. Тобто в такому ідеальному варіанті дуже активного автора та користувачів добре якщо кількість записів у бд досягне 5% від загального числа запитів. Коли запис йде швидко, то це добре, однак значно критичніше для реальних задач швидко вибирати дані.

І тут PostgreSQL тримається досить добре: на 1000 елементів він йде поряд з MySQL InndoDB, на 5000 наздоганяє MySQL MyIsam, ну а на 10 тис. починає показувати свою високу здатність витримувати навантаження. Швикдість виконання запитів вища ща відповідну у двигунця MyISAM відсотків на 20, не кажучи про InnoDB, який повільно плентається інколи відстаючи в два рази. Хочу хвернути увагу на також на те, що робота з різними типами данних відбувається в PostgreSQL по різному. За цілими числами вибірка відбувається швидше (за різницею швидкодії між MySQL та PostgreSQL).

В чому різниця між MyISAM та InnoDB. У другого можливості ширші. Тільки він дозволяє вводити в БД додаткову логіку, таку як вторинні ключі, що є на мою думку важливим для побудови серйозних проектів. Фактично InnoDB і мав би бути конкурентом для PostgreSQL. Тобто за результатами тесту MySQL в якості корпоративної БД програє за всіма параметрами PostgreSQL: функціонал обмежений, швидкість роботи нижча.

Що ж обрати?

Думали буду кричати: "Обирайте PostgreSQL"? Не буду. Оскільки вибір цієї БД є виправданим коли кількість записів в БД сягає хоча б 10 тис на таблицю. Для якогось персонального сайту, сайту-візитки, простого не навантаженого проекту цілком вистачить MySQL, оскільки там кількість записів у БД на таблицю навряд сягне хоча б однієї тисячі записів. Та й для багатьох навантажених проетів важливими є ще багато факторів, окрім швидкодії БД (наприклад, як саме було спроектовано БД, як написано код, чи кешуються якось дані). Значно важливішим при виборі PostgreSQL є необхідність включати якусь логіку чи складні типи збереження данних на рівень БД. Тут і за швидкодією і за можливостями кращим вибором буде саме PostgreSQL.

Головне при виборі БД керуватись здоровим глуздом. Ще хочу додати два посилання на інші тести, що проводили порівняння PostgreSQL та MySQL. Першим буде порівняння навантаження що витримують MySQL та PostgreSQL в рамках блогохостингу, в якому досить детально описується і методика тестування і пояснюються результати. Ну а тест за кількістю конкуруючих запитів від Tweacers.net є досить відомим, і хоча й досить застарілим, однак добре показує як справляється з високими навантаженнями різни БД.

Дякую за увагу.

Коментарі: 0
 

Коментарі:

Додати коментар

user

email

url

text

Повідомляти про новікоментарі