Статические классы вместо справочных таблиц. Тестирование.
Пару лет назад я написал один интересный «велосипед» для уменьшения нагрузки на MySQL. «Велосипед» использовал статический класс вместо справочной таблицы. Суть сего действия заключалась в уменьшении числа запросов к БД и перекладывании функции определения значения по ключу и ключа по значению на PHP. Тогда все это дело было реализовано через switch
—case
и заполнялось в ручную в обычном блокноте.
Идея подобного метода уменьшения нагрузки на базу данных родилась неслучайно. У меня был шаред хостинг за 4$/месяц на котором крутилась пара сайтов с общей посещаемостью до 1500 человек в сутки. Загрузка MySQL составляла 80-90 % условных единиц, тогда как процессор/память — редко отъедали хотя бы половину выделенных ресурсов. Так и возникла мысль: а почему бы часть операций не переложить на диск/процессор. Со временем две справочные таблицы были переведены на статические классы. Помимо уменьшения нагрузки при выводе данных пользователям, класс использовался в системе парсинга и позволял избежать до 30000 лишних запросов в час.
В целом меня все устраивало, но зерно сомнения все-таки зрело во мне. Во-первых, данные в классах менялись очень редко, во-вторых, они правились вручную, в-третьих, а вдруг все сделано не оптимально, и есть более изящные варианты? Именно поэтому я и решил провести тестирование и написать автогенератор статичных классов.
У нас есть 4 варианта (может кто-то предложит еще?):
- Стандартное подключение к БД, выполнение SQL запроса
Switch - Case
конструкцияIf - Else
конструкция- Массивы
Для тестирования были подготовлены 4 БД, имеющие одинаковую структуру — id (PK INT) и text (VARCHAR), и заполненные на 10, 50, 100, 1000 записей. С помощью класса генератора для каждой таблицы мы создадим свои статические классы. Для каждого варианта запустим циклы с определением значения id по text и наоборот, в количестве — 1, 10, 100, 1000, 10000 раз.
Данные по времени и размер потребляемой памяти будут занесены в итоговые таблицы.
Так же будет произведено тестирование выборки несуществующих значений, то есть, если у нас в таблице id от 1 до 10, мы будем пытаться получить значение 11-15 элемента. А затем посмотрим, как влияют подобные манипуляции на скорость выполнения запросов.
И последнее отступление. Результатом измерения будет число запросов в секунду (сколько раз за 1 секунду цикл сработает в заданных условиях). Чем больше значение — тем лучше.
Выборка из 10 значений
Получение существующего элемента из 10
|
Все три способа на статических классах показывают высокий результат. MySQL — намного медленней.
Получение несуществующего элемента из 10
|
При попытке получить несуществующее значение методы ведут себя по-разному. Swith
и If
замедляются, тогда как методы на массивах и MySQL обрабатывают ситуацию быстрее. Запросу не важно ищем мы существующий элемент или несуществующий — он отрабатывает одинаково быстро. Ускорение в целом происходит из-за того, что после SQL запроса mysql_num_rows
возвращает 0, а значит ему не надо обрабатывать данные. С алгоритмом на массивах всё тоже просто: перед тем как получить элемент мы запрашиваем
isset($array[$key]) |
нет элемента — нет последующего извлечения элемента из массива. Конструкция Switch
и конструкция If
должны перебрать все свои элементы, прежде чем поймут, что запрашиваемого элемента нет. Отсюда и замедление работы.
Выборка из 50 значений
Получение существующего элемента из 50
|
Выборка из 100 значений
Получение существующего элемента из 100
|
Выборка из 1000 значений
Получение существующего элемента из 1000
|
Добрались до самого интересного. С ростом числа элементов очень сильно проседают алгоритмы на Switch
и If
. При большом числе итераций цикла и метод на массивах ведет себя в 10-15 раз быстрее, чем MySQL.
Получение несуществующего элемента из 1000
|
В «минус» этих алгоритмов можно записать и колоссальное замедление работы при попытке получить несуществующий элемент:
И большее потребление памяти php:
И напоследок 2 графика со сводными данными.
1 итерация цикла:
То есть, когда у вас в скрипте в одном месте необходимо получить одно значение.
10000 итераций цикла:
То есть когда у вас в скрипте в одном месте необходимо перебрать множество значений.
Выводы:
- Можно смело использовать метод на массивах для замены справочных таблиц с числом элементов более 1000 (по предварительным тестам даже на 10000 элементах массивы быстрее MySQL). Если число элементов более 1000 — то стоит задуматься. Особенно если таблица часто изменяется, ведь при изменении таблицы мы должны перегенерировать статический класс.
- «Плюсом» данного подхода будет и версионность справочной таблицы для программистов, использующих SVN, GIT или другие подобные системы. Ведь после перегенрации класса мы его можем закоммитить, и в будущем — просмотреть все изменения в нем. Многим очень не хватает версионности БД.
- При использовании ORM и Lazy Load очень легко изменить ваш код на использование статических классов. Всего лишь в место запроса к БД написать примерно так
a_test::get_by_id($id)
или для любителей CamelCase
aTest::getById($id)
Примеры сгенерированных статических классов
<?php class a_stat { static $array_id = array( '1' => 'c4ca4238a0b92382', '2' => 'c81e728d9d4c2f63', '3' => 'eccbc87e4b5ce2fe', '4' => 'a87ff679a2f3e71d', '5' => 'e4da3b7fbbce2345' ); static function get_by_id($key) { $out = (isset(self::$array_id[$key])) ? self::$array_id[$key] : NULL; return $out; } static $array_text = array( 'a87ff679a2f3e71d' => '4', 'c4ca4238a0b92382' => '1', 'c81e728d9d4c2f63' => '2', 'e4da3b7fbbce2345' => '5', 'eccbc87e4b5ce2fe' => '3' ); static function get_by_text($key) { $out = (isset(self::$array_text[$key])) ? self::$array_text[$key] : NULL; return $out; } } ?> <?php class i_test { static function get_by_id($key) { if (1==2) $out = NULL; else if ("1"==$key) $out = "c4ca4238a0b92382"; else if ("2"==$key) $out = "c81e728d9d4c2f63"; else if ("3"==$key) $out = "eccbc87e4b5ce2fe"; else if ("4"==$key) $out = "a87ff679a2f3e71d"; else if ("5"==$key) $out = "e4da3b7fbbce2345"; else $out = NULL; return $out; } static function get_by_text($key) { if (1==2) $out = NULL; else if ("a87ff679a2f3e71d"==$key) $out = "4"; else if ("c4ca4238a0b92382"==$key) $out = "1"; else if ("c81e728d9d4c2f63"==$key) $out = "2"; else if ("e4da3b7fbbce2345"==$key) $out = "5"; else if ("eccbc87e4b5ce2fe"==$key) $out = "3"; else $out = NULL; return $out; } } ?> <?php class s_test { static function get_by_id($key) { switch ($key) { case '1' : $out = 'c4ca4238a0b92382'; break; case '2' : $out = 'c81e728d9d4c2f63'; break; case '3' : $out = 'eccbc87e4b5ce2fe'; break; case '4' : $out = 'a87ff679a2f3e71d'; break; case '5' : $out = 'e4da3b7fbbce2345'; break; default : $out = NULL; break; }; return $out; } static function get_by_text($key) { switch ($key) { case 'a87ff679a2f3e71d' : $out = '4'; break; case 'c4ca4238a0b92382' : $out = '1'; break; case 'c81e728d9d4c2f63' : $out = '2'; break; case 'e4da3b7fbbce2345' : $out = '5'; break; case 'eccbc87e4b5ce2fe' : $out = '3'; break; default : $out = NULL; break; }; return $out; } } ?> |
Интересное решение. Спасибо. Пригодилось.
Конечно, данные словаря (справочника) очень удобно хранить не в БД, а в массиве PHP. Но вовсе не обязательно внутри класса. Тогда эти справочные (или настроечные) данные легко можно менять через интерфейс админки (и не надо каждый раз генерировать новые классы).