Статические классы вместо справочных таблиц. Тестирование.

19th Август 2011 | Категории: PHP | Метки: , ,

Пару лет назад я написал один интересный «велосипед» для уменьшения нагрузки на MySQL. «Велосипед» использовал статический класс вместо справочной таблицы. Суть сего действия заключалась в уменьшении числа запросов к БД и перекладывании функции определения значения по ключу и ключа по значению на PHP. Тогда все это дело было реализовано через switchcase и заполнялось в ручную в обычном блокноте.

Идея подобного метода уменьшения нагрузки на базу данных родилась неслучайно. У меня был шаред хостинг за 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

  1 10 100 1000 10000
mysql 332.47 285.84 128.21 18.51 1.92
switch 4599.85 3970.44 1763.33 262.33 27.19
if 4444.77 3641.47 1609.50 247.76 25.92
array 4715.70 3992.24 1649.82 237.58 26.13

Все три способа на статических классах показывают высокий результат. MySQL — намного медленней.

Получение несуществующего элемента из 10

  1 10 100 1000 10000
mysql 348.77 279.02 121.18 21.23 1.89
switch 4190.46 3566.82 1440.27 208.10 20.82
if 4347.47 3561.58 1416.74 213.98 21.88
array 4792.69 4053.50 1758.00 253.28 28.51

При попытке получить несуществующее значение методы ведут себя по-разному. Swith и If замедляются, тогда как методы на массивах и MySQL обрабатывают ситуацию быстрее. Запросу не важно ищем мы существующий элемент или несуществующий — он отрабатывает одинаково быстро. Ускорение в целом происходит из-за того, что после SQL запроса mysql_num_rows возвращает 0, а значит ему не надо обрабатывать данные. С алгоритмом на массивах всё тоже просто: перед тем как получить элемент мы запрашиваем

isset($array[$key])

нет элемента — нет последующего извлечения элемента из массива. Конструкция Switch и конструкция If должны перебрать все свои элементы, прежде чем поймут, что запрашиваемого элемента нет. Отсюда и замедление работы.

Выборка из 50 значений

Получение существующего элемента из 50

  1 10 100 1000 10000
mysql 320.90 237.28 109.86 13.25 1.86
switch 2476.14 1968.56 788.66 115.63 12.51
if 2209.25 1866.09 654.81 87.84 8.98
array 3667.60 2749.54 1511.92 236.40 25.72

Выборка из 100 значений

Получение существующего элемента из 100

  1 10 100 1000 10000
mysql 322.70 285.04 116.82 14.11 1.71
switch 1504.31 1306.07 565.83 71.80 8.05
if 1279.00 1112.69 428.12 63.84 6.49
array 2523.87 2438.19 1313.52 231.05 25.76

Выборка из 1000 значений

Получение существующего элемента из 1000

  1 10 100 1000 10000
mysql 312.16 279.07 108.13 13.69 1.69
switch 193.02 163.70 67.53 10.51 1.09
if 175.84 143.34 63.44 8.77 0.94
array 474.04 488.98 410.51 167.79 24.16

Добрались до самого интересного. С ростом числа элементов очень сильно проседают алгоритмы на Switch и If. При большом числе итераций цикла и метод на массивах ведет себя в 10-15 раз быстрее, чем MySQL.

Получение несуществующего элемента из 1000

  1 10 100 1000 10000
mysql 356.69 303.68 127.82 16.59 1.83
switch 183.01 144.13 40.32 4.24 0.47
if 169.43 135.54 40.36 4.96 0.52
array 448.38 428.31 417.21 168.15 27.27

В «минус» этих алгоритмов можно записать и колоссальное замедление работы при попытке получить несуществующий элемент:

И большее потребление памяти 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;
	}
 
}
?>

класс генератора с конфигом

Subscribe without commenting


  1. Alex
    28th Март 2012 в 19:42

    Интересное решение. Спасибо. Пригодилось.

  2. Конечно, данные словаря (справочника) очень удобно хранить не в БД, а в массиве PHP. Но вовсе не обязательно внутри класса. Тогда эти справочные (или настроечные) данные легко можно менять через интерфейс админки (и не надо каждый раз генерировать новые классы).