mardi, 9 janvier 2007

Optimisation du nombre de requêtes SQL dans les collections d'objets et relation n-m

L'orientation objet de PHP n'est plus contestable, mais les problèmes d'optimisation persistent.

Par exemple le malheureusement célèbre n+1 pattern, qui fait que pour afficher une liste de n objets, le + 1 étant la requête qui sélectionne tous les ids des objets à instancier, n + 1 requêtes SQL seront effectuées.

De même dans des relations n-m, la majorité des implémentations sélectionnent les n objets (en n + 1 requêtes donc), puis pour chacun on sélectionne les m objets de la relation. On obtient donc n * m + 1 requêtes.

Le but de cet article est de présenter deux techniques qui, combinées, réduisent les n * m + 1 requêtes en n + m + 1.

Les deux techniques que je vais illustrer ici se nomment object caching et grouped fetching. Elles se combinent très bien, ce qui permet d'optimiser drastiquement les performances d'un script PHP, du points de vue I/O (moins de requêtes), vitesse d'exécution et même mémoire utilisée (les objets ne sont pas dupliqués).

Object caching :

Le principe de l'object caching est de rendre le constructeur de l'objet privé (au pire protégé) et de l'instancier au moyen d'une factory. Puis on ajoute à la classe un tableau statique dans lequel les références des objets instanciés seront placés. La factory va donc regarder dans le tableau de références si l'objet existe, et si ça n'est pas le cas elle va le créer, l'ajouter au tableau et le retourner.


class A {
protected static $objects_cache = array();

public static function factory($id, $class = __CLASS__)
{
if (array_key_exists($id, $class::$objects_cache)) {
return $class::$objects_cache[$id];
} else {
$o = new $class($id);
$class::$objects_cache[$id] = $o;
return $o;
}
}
}


Cette solution pourrait encore être améliorée si les objets pouvaient être partagés entre toutes les instances de PHP. Ce n'est pas le cas à cause de l'architecture share nothing de PHP, et c'est un des points forts des serveurs d'applications.
Un autre désavantage de ce concept, si on a un script qui tourne suffisamment longtemps pour que le grabage collector se lance, est que tous les objets créés sont toujours référencés, même ceux qui pourraient être des candidats potentiels à la finalisation. Ce problème est résolu dans d'autres langages, en Java par exemple grâce aux références faibles (WeakReference).

Grouped fetching

Le principe du grouped fetching, dérivé d'une solution proposée par notre ami Colder, est de charger les données des objets de manière asynchrone et en bloque. Quand un objet est instancié, il est marqué comme non chargé, et sa référence est placée dans un tableau global de la classe. Puis lors d'un accès à un champ non chargé, la classe va sélectionner dans la base de données tous les objets instanciés mais pas encore chargés. Plus on retarde les accès aux attributs d'un objet, plus on va paralléliser les requêtes SQL.


class A {
private static $_to_load = array();
private $_is_loaded = false;
public function __construct($id)
{
$this->id = $id;
self::$_to_load[$id] = $this;
}

public function __get($attribut)
{
if (!$this->_is_loaded) {
self::_groupedLoad();
}
return $this[$attribut];
}

private function _setLoaded()
{
$this->is_loaded = true;
}

private static function _groupedLoad()
{
$ids = implode(', ', self::$_to_load);
$query = 'SELECT * FROM ' . self::$_table . ' WHERE id IN ( ' . $ids . ' ) ';
$res = db_query($query);
while ($row = $res->getNext()) {
self::_to_load[$row['id']]->_initByArray($row);
self::_to_load[$row['id']]->_setLoaded;
}
}
}


L'overhead de ce concept est très faible (un tableau de références supplémentaire), et le nombre d'accès à la base de données sont grandement réduit. Mais le problème de la finalisation des objets se repose aussi.

3 commentaires:

KillerWhile a dit…

En fait grâce à une légère modification de l'implémentation, on arriverait même à gérer des relations n-m en 4 requêtes, pour autant que l'accès aux champs autres que la clé primaire se fasse après l'initialisation de tous les objets. Mais cette sur-optimisation donnera lieu à un autre article, et éventuellement même une petite framework :)

KillerWhile a dit…

Petite précision relevée par Sam : le concept que je désigne par object caching se rapproche énormément du pattern de "multiton" (http://en.wikipedia.org/wiki/Multiton_pattern, proche parent disputé du "singleton")

Anonyme a dit…

Keep up the good work.