Главная > Сравнение скорости PDO, ADOdb, MDB2, Zend_Db, ADOdb Lite и DbSimple
На этой странице показано одно сообщение с комментариями к нему, самые новые комментарии снизу.

122 месяца назад

Постоянная ссылка Vanav

Файлы

Сравнение скорости PDO, ADOdb, MDB2, Zend_Db, ADOdb Lite и DbSimple

В этом тестировании я решил выяснить, какова производительность различных современных классов абстракции от базы данных. В тестировании приняли участие:

  • PDO (часть PHP 5);
  • ADOdb 5 5.04a (25.03.2008);
  • MDB2 (часть PEAR, наследник и объединение PEAR DB и Metabase) 2.5.0b1 (15.03.2008);
  • Zend_Db 1.5.2 (15.05.2008);
  • ADOdb Lite 1.4.2 (12.01.2007);
  • DbSimple v2 R226 SVN (17.09.2007).

Версии — последние на текущий момент.

Субъективность и объективность

Я раньше в своих проектах использовал собственный класс доступа к БД, потом начал использовать DbSimple, потом перешёл на PHP 5 и стал больше склоняться к решениям без поддержки PHP 4 (например, Zend Framework). DbSimple и ADOdb Lite уже больше года не обновлялись, это достаточно плохо с моей точки зрения. На большие и неиспользуемые объёмы кода смотрю подозрительно, удобство интерфейса уважаю. Всегда могу выдавить из тюбика зубной пасты ещё чуть-чуть. К счастью, это всё никакого отношения к тестированию не имеет.

Методология

Нагрузка на сервер создавалась при помощи httperf от Hewlett-Packard. Тесты запускались несколько раз, после чего отбрасывался худший результат, а остальные усреднялись. Сервер настроен на высокую производительность, программная часть следующая:

  • FreeBSD 7.0-RELEASE-p1;
  • Apache 2.2.8;
  • PHP 5.2.6 with Suhosin-Patch 0.9.6.2;
  • nginx 0.6.31;
  • MySQL 5.0.51a;
  • httperf 0.9.0;
  • APC 3.0.19.

Логи отключены в Apache и nginx, вывода скриптов нет. Текст .htaccess:

php_value include_path "/home/test.com/data/library"
php_value display_errors off
php_value error_reporting 2047
php_value max_execution_time 200

httperf создавал постоянную последовательную нагрузку без пауз в один поток, запускался следующим образом:

httperf --server=test.com --uri=/DBBenchmark/adodb.php --num-conns=1000

Тест 1. Синтетический

В этом тесте каждый класс исполнял 10 000 раз цикл из 8 разных запросов, скрипт запускался 1 раз.

Что должен показать: в первую очередь, это та методология, которую обычно применяют при тестировании производительности классов. В основном нагружается лексический парсер запросов и кеш, если он есть у класса. Тест показать ничего не должен, на то она и синтетика.

Страницы в секунду — здесь это расчётный параметр для сравнения, он получается по формуле: 10000 / 8 / (время исполнения тестового скрипты), и отображает, сколько циклов по 8 запросов выполняет класс в секунду.

Установите Adobe Flash player, чтобы увидеть график.

Видно, что у MDB2 достаточно медленный парсер.

В виде таблицы:

КлассВремя, сСтраниц/с
PDO10,1123,8
ADOdb15,481,0
mdb242,429,5
Zend_Db20,660,8
ADOdb Lite17,272,5
DbSimple32,838,1

Тест 2. Реальный

В этом тесте каждый класс исполнял 2 раза цикл из 8 разных запросов. Скрипт запускался последовательно 1000 раз.

Что должен показать: как класс реагирует на настоящую нагрузку сайта. При этом будет сильно сказываться скорость загрузки самого класса и время подключения к базе.

Проводилось два разных измерения: сначала цикл с 8 запросами исполнялся 2 раза на страницу (всего 16 запросов), потом 1 раз (всего 8 запросов). Официальным результатом теста считается первый вариант с 2 проходами цикла. Он показывает также, насколько эффективен кеш подготовленных (prepared) запросов в классе, и в реальном многомодульном приложении не всегда удаётся избежать повторных запросов. Тест с одним проходом цикла интересен для сравнения, позже я опишу результаты.

Ещё один проход теста был сделан с включённым opcode кешированием от APC. По некоторым сторонним тестам APC даёт очень хороший результат из всех opcode кешей (xCache, eAccelerator, Zend Platform). Тестирование разных opcode кешей выходит за рамки данной статьи, поэтому я использовал только APC. Здесь результаты сильно варьировались от запуска к запуску, пока оптимизировался кеш, методология тестирования игнорирует самые плохие результаты и усредняет хорошие, поэтому можно получить достаточно устойчивую картину.

Без APC

Будет тестироваться запуск с двумя и одним циклом.

Что ожидается: в отличии от синтетического теста, здесь достаточно большую долю времени будет компилироваться код класса. Разница в выполнении двух и одного цикла нам покажет, насколько эффективен механизм кеширования скомпилированных запросов. Если разница будет небольшая, значит механизм эффективен, если большая — то неэффективен или отсутствует. Также по разнице можно примерно оценить, какая доля времени приходится на компиляцию собственно кода класса.

Результаты без APC:

Установите Adobe Flash player, чтобы увидеть график.

Итак, наиболее эффективные механизмы кеширования у Zend_Db и ADOdb, чуть менее у MDB2. PDO, ADOdb Lite и DbSimple показывают достаточно ощутимую разницу.

Самое интересное, ради чего и создавался этот тест, выяснить, какая из библиотек эффективнее. Как я и говорил, будем сравнивать результаты с двумя циклами. Итак, из библиотек с полным DBAL лидирует Zend_Db, второе место у ADOdb и замыкает MDB2. Среди более простых библиотек лидер ADOdb Lite, следом DbSimple. PDO, как единственный класс, написанный на Си, вне конкуренции.

C APC

Я выделил 128 Мб на кеш и поставил большой TTL, чтобы код всех классов мог поместиться одновременно и не выбрасываться до окончания тестов.

Что ожидается: наиболее тяжёлые библиотеки должны показать наибольший прирост скорости, потому что их код будет теперь доступен в скомпилированном виде.

Это несколько идеализированный тест, потому что я позволил библиотекам разместиться в кеше целиком и ничем их не ограничивал. Но все в равных условиях, тем не менее.

Установите Adobe Flash player, чтобы увидеть график.

Здесь меня ожидал сюрприз: не все библиотеки одинаково работают с APC, и некоторые результаты не так просто объяснить.

Во-первых, произошли перестановки среди лидеров: ADOdb поднимается на первое место среди полноценных DBAL, Zend_Db и MDB2 идут следом. Интересно, что кеширование позволило Zend_Db догнать DbSimple. В случае PDO, кешировать было практически нечего, кроме самих тестовых скриптов и файла с запросами. Мне очень понравилось как ADOdb работает с APC.

Замечено интересного

  1. При попытке тестирования стандартным ab, я получал Segmentation fault от Apache 2. Выяснилось, что так ab реагирует на связку nginx + apache и ломает apache. Причём в комплекте Apache 1 ab рабочий. Но доверие к нему было подорвано, и я использовал httperf.
  2. Очень сильно на скорость влияет include_path в PHP. Надо папку с библиотеками указывать первой, затем всё остальное. Это потому, что пути include внутри библиотек вида «Zend/Db/Table.php» и они начинают поиск по списку include_path.
  3. При тестировании сам процеcc httperf создаёт достаточно большую нагрузку на CPU. Отчасти поэтому я сделал синтетический тест, где httperf нагрузки практически не создавал. Все классы находились в одинаковых условиях, поэтому большой проблемы это не вызвало, но иметь в виду нужно.
  4. При тестировании с APC и первом запуске теста заметно значительное среднеквадратичное отклонение скорости коннекта, смотрим:
КлассВремя соединения, мс
PDOmin 0.7 avg 3.9 max 201.8 median 3.5 stddev 6.3
ADOdbmin 4.7, avg 7.2, max 198.9, median 6.5, stddev 10.5
mdb2min 8.9, avg 11.5, max 125.9, median 10.5, stddev 4.6
Zend_Dbmin 8.9, avg 10.0, max 101.8, median 9.5, stddev 3.5
ADOdb Litemin 1.3, avg 6.0, max 58.9, median 5.5, stddev 2.1
DbSimplemin 6.0, avg 9.8, max 134.3, median 9.5, stddev 7.3

Это косвенно показывает, как заполняется кеш opcode в APC. После окончания тестов использование APC этими классами было следующим:

Cached Files: 583 ( 45.0 MBytes)
Hits: 230198
Misses: 586
Request Rate (hits, misses): 138.94 cache requests/second
Hit Rate: 138.59 cache requests/second
Miss Rate: 0.35 cache requests/second
Insert Rate: 0.35 cache requests/second
Cache full count: 0

Тексты тестов

pdo.php:

PHP
<?php
require_once 'sql.php';
$db = new PDO("mysql:dbname=blog;host=127.0.0.1", "blog", "password");
for ($i = 0; $i < $count; $i++)
{
    foreach($select as $s) {
        $rs = $db->query($s)->fetchAll(PDO::FETCH_ASSOC);
        foreach($rs as $r) {
        }
    }
    foreach($update as $s) {
        $z = $db->query($s);
    }
}

adodb.php:

PHP
<?php
require_once 'adodb5/adodb.inc.php';
require_once 'sql.php';
$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
$db = NewADOConnection("mysql://blog:password@127.0.0.1/blog"); 
for ($i = 0; $i < $count; $i++)
{
    foreach($select as $s) {
        $rs = $db->Execute($s);
        foreach($rs as $r) {
        }
    }
    foreach($update as $s) {
        $z = $db->Execute($s);
    }
}

mdb2.php:

PHP
<?php
require_once 'PEAR/MDB2.php';
require_once 'sql.php';
$db =& MDB2::connect(array(
    'phptype'  => 'mysql',
    'hostspec' => '127.0.0.1',
    'username' => 'blog',
    'password' => 'password',
    'database' => 'blog'
));
$db->setFetchMode(MDB2_FETCHMODE_ASSOC);
for ($i = 0; $i < $count; $i++)
{
    foreach($select as $s) {
        $rs = $db->query($s)->fetchAll();
        foreach($rs as $r) {
        }
    }
    foreach($update as $s) {
        $z = $db->query($s);
    }
}

zend_db.php:

PHP
<?php
require_once 'Zend/Db/Adapter/Pdo/Mysql.php';
require_once 'sql.php';
$db = new Zend_Db_Adapter_Pdo_Mysql(array(
    'host'     => '127.0.0.1',
    'username' => 'blog',
    'password' => 'password',
    'dbname'   => 'blog'
));
for ($i = 0; $i < $count; $i++)
{
    foreach($select as $s) {
        $rs = $db->fetchAll($s);
        foreach($rs as $r) {
        }
    }
    foreach($update as $s) {
        $stmt = $db->query($s);
        $z = $stmt->rowCount();
    }
}

adodb_lite.php:

PHP
<?php
require_once 'adodb_lite/adodb.inc.php';
require_once 'sql.php';
$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
$db = NewADOConnection("mysql://blog:password@127.0.0.1/blog"); 
 
for ($i = 0; $i < $count; $i++)
{
    foreach($select as $s) {
        $rs = $db->Execute($s)->GetAll();
        foreach($rs as $r) {
        }
    }
    foreach($update as $s) {
        $z = $db->Execute($s);
    }
}

dbsimple.php:

PHP
<?php
require_once 'DbSimple/Generic.php';
require_once 'sql.php';
$db = DbSimple_Generic::connect('mysql://blog:password@127.0.0.1/blog');
for ($i = 0; $i < $count; $i++)
{
    foreach($select as $s) {
        $rs = $db->query($s);
        foreach($rs as $r) {
        }
    }
    foreach($update as $s) {
        $z = $db->query($s);
    }
}

sql.php:

PHP
<?php
$select = array();
$select[] = "SELECT uni_files.id, name, disk_filename, icon, size FROM uni_files INNER JOIN uni_filetypes ON uni_files.filetype_id=uni_filetypes.id WHERE post_id='167' AND blog_id='3'";
$select[] = "SELECT p.id, nick, p.poster_id, creation_date, post_title, post_text, CEILING((rgt - lft - 1) / 2) AS comment_count FROM uni_posts p LEFT OUTER JOIN uni_users ON p.poster_id = uni_users.id LEFT OUTER JOIN uni_posts_keywords pk ON p.id = pk.post_id AND pk.keyword_id='34' WHERE parent_post_id IS NULL AND NOT(keyword_id IS NOT NULL AND p.poster_id!='3') AND blog_id='3' ORDER BY creation_date DESC LIMIT 0, 4";
$select[] = "SELECT EXTRACT(DAY FROM creation_date) AS day, COUNT(*) AS day_count FROM uni_posts WHERE parent_post_id IS NULL AND blog_id='3' AND EXTRACT(YEAR FROM creation_date)=2008 AND EXTRACT(MONTH FROM creation_date)=6 GROUP BY day";
$select[] = "SELECT uni_posts.id, nick, poster_id, creation_date, post_title, post_text, post_text_wiki, active_text FROM uni_posts LEFT OUTER JOIN uni_users ON uni_posts.poster_id = uni_users.id WHERE uni_posts.id='123' AND blog_id='3'";
$select[] = "SELECT uni_posts.id, rgt, nick, creation_date, post_title, post_text FROM uni_posts LEFT OUTER JOIN uni_users ON uni_posts.poster_id = uni_users.id LEFT OUTER JOIN uni_posts_keywords pk ON uni_posts.id = pk.post_id AND pk.keyword_id='34' WHERE NOT(keyword_id IS NOT NULL AND poster_id!='3') AND uni_posts.id='123' AND blog_id='3'";
$select[] = "SELECT p1.id, nick, p1.creation_date, p1.modification_date, p1.post_title, p1.post_text, p1.parent_post_id, p2.post_title AS parent_post_title, p3.post_title AS answer_parent_post_title, p3.id AS answer_parent_post_id FROM uni_posts p1 LEFT OUTER JOIN uni_users ON p1.poster_id=uni_users.id LEFT JOIN uni_posts p2 ON p1.parent_post_id=p2.id LEFT JOIN uni_posts p3 ON p2.root_post_id=p3.id LEFT OUTER JOIN uni_posts_keywords pk ON p1.id = pk.post_id AND pk.keyword_id='34' WHERE NOT(keyword_id IS NOT NULL AND p1.poster_id!='3') AND p1.blog_id='3' ORDER BY modification_date DESC LIMIT 0, 10";
$select[] = "select productid,productname,unitsinstock,unitprice from products";
 
$update = array();
$update[] = "UPDATE uni_users SET login_date=NOW() WHERE id='1'";
 
$count = 2;

Итоги

Сравним скорость без APC и с APC. В обоих случаях выполнялось 2 цикла.

Установите Adobe Flash player, чтобы увидеть график.

Табличка для дигиталов:

Класс2 цикла1 цикл2 цикла + APC1 цикл + APC
PDO262,6355,0272,9370,1
ADOdb47,749,0155,9205,65
mdb239,047,988134,8
Zend_Db57,062,5101,8127,75
ADOdb Lite124,2153,4166,9231,9
DbSimple80,8101,1102,3164,6

Одной из целей этого тестирования было опробовать новый класс Zend_Db и посмотреть его в сравнении. Тесты показали, что Zend_Db достойный конкурент, и этот класс вполне можно использовать в любых комбинациях. Одним из сюрпризов тестирования оказалась большая скорость ADOdb Lite при немаленьком объёме кода. Если ему простить некоторые интерфейсные огрехи, то можно рекомендовать там, где не нужен полный DBAL. В некотором роде аутсайдером оказался MDB2, он работает медленнее остальных. Отчасти это можно объяснить тем, что он построен на основе абстракции от диалектов SQL Metabase.

Субъективные итоги

Я ожидал большего от Zend_Db, но в то же время по результатам теста я не буду отказываться от этого класса, он показал себя вполне достойно. Я приятно удивлён скоростью ADOdb, и шокирован скоростью ADOdb Lite, почему-то слово ADOdb у меня ассоциировалось с чем-то большим и неповоротливым. Да, у него есть недостатки в интерфейсе, но скорость на уровне, это радует. DbSimple по непонятной причине оказался медленнее ADOdb Lite, хотя в нём всего 2 файла. Видимо разработчик не ставил целью скорость.

Для себя я сделал вывод: в новых проектах я буду использовать Zend_Db и APOdb Lite (если устроит его интерфейс). Когда понадобится что-то совсем простое, то либо свой класс, либо DbSimple. MDB2 стоит использовать, когда на самом деле нужна независимость от диалектов SQL, что на практике достаточно редко.

Ссылки

  1. В одном из запросов использовалась БД из давнего теста ADBL.
  2. Более популярно про Zend_Db с примерами.
  3. В Zend_Db есть ещё небольшой нюанс, который замедляет исполнение SELECT запросов в MySQL. После его исправления надо будет повторить тесты.

Комментарии