Optimalizace počtu dotazů do MySQL

Možná už jste se někdy setkali s tím, že máte menu, které je hierarchicky členěno. Prostě obyčejný rozklikávací pavouk. Bývá to nejčastěji u e-shopů, ale můžeme se s tím setkat i jinde.

Já jsem při vývoji e-shopu narazil na jeden problém:

Generování menu a zjišťování podkategorií otevřené kategorie jsem prováděl pomocí rekurzivních funkcí, které neukládaly žádné mezihodnoty a při každém průchodu použili alespoň jeden dotaz do databáze. Podíval jsem se pak na celkový počet dotazů při jednom reloadu a mírně jsem strnul. Téměř 30 dotazů do DB, přičemž jsem pouze vykreslil menu katalogu a zobrazil příslušné produkty a fotky k nim. To mi přišlo až příliš. Přece jen, když má hosting omezení 20 000 dotazů za hodinu, tak při slušné návštěvnosti by mohly záčít být problémy.

Zaměřil jsem se na funkci, která vrací v poli idčka všech podkategoríí a pokusil jsem se ji zoptimalizovat.

Původní funkce vypadá takto:

function subcategories($id){
  $query = new Query("SELECT `id_category` FROM `".table_name('category')."` WHERE `parent` = '".sql_in($id,1)."'");
  if($query->num()){
    while($sub = $query->fetch()){
      $arr[] = $sub['id_category'];
      $subsub = subcategories($sub['id_category']);
      if(is_array($subsub)){
        $arr = array_merge($arr,$subsub);
      }
    }
    return $arr;
  }else return false;
}
$query->num() vrací počet řádků

$query->fetch() vrací mysql_fetch_array

U trochu rozlehlejšího stromu kategorií začíná dotazů do DB nemile přibývat...

Zkusil jsem tedy funkci napsat jinak a obě metody poté porovnal.

druhý postup

U druhého postupu jsem dbal na co nejmenší počet dotazů do DB - tedy pouze jeden. Zbytek už musí zařídit PHP. Vypadá to takto:

$query = new Query("SELECT id_category,parent FROM `".table_name('category')."`");
while($cat = $query->fetch()){
  $categories[] = array('id'=>$cat['id_category'],'parent'=>$cat['parent']);
}

function subcategories2($id){
  global $categories;
  for($i=0;$i<count($categories);$i++){ 
    if($categories[$i]['parent']==$id){
      $arr[] = $categories[$i]['id'];
      $subsub = subcategories2($categories[$i]['id']);
      if(is_array($subsub)){
        $arr = array_merge($arr,$subsub);
      }
    }
  }
  return $arr;
}

V cyklu jsem musel místo foreach použít for, protože ukazatel na aktuální prvek v poli(u foreach) je nejspíš globální proměnná - po průchodu celého pole ve vnitřní funkci a skoku nahoru do funkce rodičovské cyklus foreach už nepokračoval.

Porovnání

prvni:0.00181913375854
druhy:0.00172901153564

prvni:0.00146102905273
druhy:0.0017831325531

prvni:0.00127506256104
druhy:0.00156593322754

prvni:0.00109505653381
druhy:0.0014660358429

prvni:0.00424790382385
druhy:0.00165700912476

prvni:0.00142979621887
druhy:0.00140404701233

prvni:0.00146007537842
druhy:0.00148797035217

prvni:0.00097393989563
druhy:0.000872850418091

prvni:0.00133800506592
druhy:0.00151610374451

prvni:0.000968217849731
druhy:0.000833034515381

Jak vidíte, obě funkce jsou rychlostně velmi podobné. Zkusil jsem tedy ještě 1000x opakování obou funkcí:

prvni:14.92943573
druhy:8.41595482826

Tady už je rozdíl celkem znatelný. Optimalizovaná funkce tedy nejen že neplýtvá dotazy do DB, jejichž počet může být omezen, ale je dokonce i rychlejší!

Díky této jediné zoptimalizované funkci se mi počet dotazů snížil na polovinu...