Optimisation du nombre de requetes SQL pour afficher une page par GouBs
Quelques explications sommaires
dans la version wikini actuelle, à chaque mot wiki correspond une requete SQL. Sur mon site perso, qui utilise un menu, une moyenne de 20 requetes correspondent à ces mots (sur un total d'environ 30). Les requetes associées à chaque mot Wiki permettent de savoir si la page associée à ce mot existe. L'idée est donc de remplacer ces 20 requetes par une seule !
Pour cela j'ai rajouté une fonction dans la class Wiki qui répertorie l'ensemble des mots Wiki présents dans la page (Header + body). L'ensemble de ces mots permet ensuite de construire une requete SQL du type "Select tag from pages where latest = 'Y' and ( tag = wikiMot1 or tag =wikiMot2 or ... )". Le résultat de la requete est utilisé pour mettre à jour un tableau Wiki du type "tab_mot_reference[wikiMot1]=true" pour chaque tag de page retourné.
La modification suivante intervient dans la fonction Link qui retourne un formatage de type
page existante ou
page à créer en fonction de la valeur du tableau précédent (
if tab_mot_reference[wikiMot] then formatage page existante else formatage page à créer)
Pour que le tableau soit créé il faut bien sur appeler la fonction. J'ai mis cet appel dans la class Wiki au niveau de la fonction Method (appel du Handler). la derniere chose à faire est de déclarer le tableau dans l'entete de la class.
Seule la class Wiki est impactée par cette modification (dans le fichier wakka.php)
Remarques sur temps de traitement
Le principe adopté permet de réduire le nombre de requetes SQL mais on peut se poser la question de savoir si le temps de traitement rajouté pour remplir le tableau et celui correspondant à la requete SQL plus complexe ne sont pas pénalisant par rapport aux gains correspondant aux suppressions de requete.
Les premiers essais fait sur mon site perso montre que le temps global de traitement a baissé
d'environ 15% et le pourcentage de ce temps consommé par les
requetes SQL est passé d'environ 33% à
21%. Ces résultats ne sont biensur valables pour l'instant que dans le cadre de la configuration de mon site : environ 70 pages et une table page contenant environ 750 pages. Il serait interressant que d'autres puissent faire des essais sur un site + conséquent.
Pourquoi pas un
tableau de résultats apres modif sous la forme ci-dessous mis à jour par chacun :
Nom |
Tps total |
% tps SQL |
Nb Pages |
Nb enreg table page |
GouBs |
-15% |
-12% |
70 |
750 |
?? |
? |
? |
? |
? |
Pistes d'amélioration
- La fonction qui rempli le tableau de mots Wiki doit faire appel à la lecture du header et du body pour connaitre ces mots. Un deuxième appel à ces fonctions sont fait plus tard pour afficher le header et le body. Il est peut être possible d'optimiser cela.
Voir démo sur :
Mon Site
--
GouBs
code de la modification
<?php
/* ---------------- déclaration du tableau ---------------------- */
class Wiki
{
var $tab_mots_wiki_definis = array(); /* ajout Goubs pour optimisation Rqt SQL */
/* ----------------- Ajout le la fonction --------------------------*/
/* Modification Goubs du 22/5/2004
cette fonction permet de mettre dans une table la liste des mots wiki défini
(c'est a dire ayant un enregistrement associé dans la table des pages
*/
function Ctrl_Mot_Wiki_Defini() {
// recherche de la liste des mots Wiki dans le Header et le body
$patterns = "#".
"\b[A-Z][a-z_]+[A-Z,0-9][A-Za-z0-9_]*\b|". // mots wiki
"(?<=missingpage\">)[A-z0-9]+". // mots wiki forcés apparaisent pécédés de : missingpage">
"#";
$message = $this->GetMessage(); //sauvegarde le message éventuel (sinon il est perdu !)
$contenu = $this->Header().$this->Format($this->page["body"]);
$this->SetMessage($message); // restitue le message
$nb_mot = preg_match_all ($patterns, $contenu,$tab_mots_wiki);
if ($nb_mot > 0){
// élimine les doublons du tableau
foreach ($tab_mots_wiki[0] as $value)
$tableau_mots[]=$value;
$tab_mots = array_unique($tableau_mots);
$this->nb_mots_wiki = count($tab_mots); /* pour test seulement */
// construction d'une Rqt SQL à partir de ces mots
$Rqt_sql = "Select tag from ".$this->config["table_prefix"].
"pages where latest = 'Y' and tag in(";
foreach($tab_mots as $key=>$value)
$Rqt_sql .= "'".$value."',";
$Rqt_sql .= "'')";
$Mots_definis = $this->LoadAll($Rqt_sql);
// mise à jour d'un tableau avec les mots Wiki défini par une page
foreach ($Mots_definis as $Mot_defini)
$this->tab_mots_wiki_definis[$Mot_defini['tag']] = true;
}
}
/* fin de la modification Goubs */
/* ---------------- dans la fonction Link ------------------------------------*/
// it's a Wiki link!
if ($_SESSION["linktracking"] && $track) $this->TrackLinkTo($tag);
// modif Goubs pour optimiser le nombre de requete SQL
//return ($this->LoadPage($tag) ? "<a href=\"".$this->href($method, $tag)."\">".str_replace('_',' ',$text)."</a>" : "<span class=\"missingpage\">".str_replace('_',' ',$text)."</span><a href=\"".$this->href("edit", $tag)."\">?</a>");
return (isset($this->tab_mots_wiki_definis[$tag]) ? "<a href=\"".$this->href($method, $tag)."\">".str_replace '_',' ',$text)."</a>" : "<span class=\"missingpage\">".str_replace('_',' ',$text)."</span><a href=\"".$this->href("edit", $tag)."\">?</a>");
/* ---------------- Appel de la fonction -----------------------------------*/
function Method($method) {
if (!$handler = $this->page["handler"]) $handler = "page";
$methodLocation = $handler."/".$method.".php";
/* ******* ICI *********** */
$this->Ctrl_Mot_Wiki_Defini(); // ajout Goubs pour optimisation Rqt SQL
return $this->IncludeBuffered($methodLocation, "<i>Méthode inconnue \"$methodLocation\"</i>", "", $this->config["handler_path"]);
}
?>
Merci
GarfieldFr. J'ai fait la modif comme tu l'indiquais en commentaire sur cette page (
utiliser une clause WHERE du style : WHERE latest='Y' and tag in ('Page1','Page2?' ...) ). Je n'ai pas réussi à mesurer un gain de performance (pas d'outil de mesure assez précis) mais la solution est plus éléguante. --
GoubS
Les outils en ligne de commande de
MySQL permettent de voir le temp d'execution des requêtes ... regarde la doc de
MySQL pour plus de précisions. --
GarfieldFr
De toute façon à mon sens il y a forcément un gain de performances, sinon les listes ne serviraient à rien...
Il y a plusieurs choses qui posent problème dans ton code selon moi:
- l'entête et la page sont formattés entièrement une fois rien que pour récupérer les MotWiki, grosse perte de temps tout de même...
- tous les handlers n'ont pas besoin de formatter la page (notemment edit en mode édition, raw, revisions, referrers etc.), et donc n'ont pas besoin de savoir si ses liens existent ou non... pourtant tu les récupères quand même !
- tu ne tiens pas compte du fait qu'il peut, selon les handlers (par exemple diff, edit en mode aperçu etc.), y a avoir des liens qui ne font normalement pas partie de la page courante (dons mes exemples: un diff d'une page dans laquelle on aurait enlevé des liens, un aperçu d'une page où on est en train d'ajouter des liens). Dès lors ces mots apparaitront comme si la page en question n'existaire pas, alors qu'on n'en sait en fait rien... Pour palier cela il faudrait au moins stocker dans $tab_mots_wiki_definis la liste des mots qu'on n'a voulu récupérer mais qu'on n'a pas trouvés (en tout logique avec la valeur false...). Du coup, on est certain que la page existe (ou non) dans le cas où $tab_mots_wiki_definis['motWiki'] est défini, sinon il faut tout de même vérifier.
- il y a maintenant deux systèmes de mise en cache: ta gestion des MotWiki définis, et les pages mises en cache complètement via LoadPage...
Je vois trois autres solutions:
- En début de script, charger tous les noms de pages dans un tableau. Il faut savoir qu'une requête du type:
- SELECT champ FROM matable
- est très rapide, surtout si champ est indexé (ce qui est le cas du champ "tag" dans la table "pages"). Côté php, remplir un tableau est aussi très rapide, et l'utilisation de mémoire ne pose vraiment aucun problème.
- Récupérer toutes les liens certains (via une requête du type "SELECT to_tag FROM wikini_links LEFT JOIN wikini_pages ON tag = to_tag WHERE from_tag = 'pageCourante'"). Le principe est donc le même que dans le script proposé ici, sauf qu'on ne formatte pas une fois la page pour du beurre... L'inconvéniant est que ça ne marche que pour les liens trackés. (je note d'ailleurs au passage que tu travailles sur une version de wikini où l'arrêt du tracking était encore possible; le bug que j'ai posté l'autre jour n'est donc pas d'origine...)
- Révision complète de la gestion de l'affichage (ma solution préférée, mais que de travail... peut-être pour la version 1.0 de WikiNi ? Ou même la 2.0...) En cours de script, on s'occuperait de bufferiser nous même ce qu'on veut afficher, et ce dans un tableau (par exemple $buffer):
- on accumulerait tout ce qu'on veut afficher dans une variable (ceci peut encore à la limite se faire avec ob, mais j'en doute...) jusqu'à tomber sur un lien interne (MotWiki ou lien forcé)
- lorsqu'on tombe sur un lien interne:
- on stocke tout ce qu'on a accumulé comme nouvelle entrée du tableau $buffer. C'est là que la difficulté arrive:
- on appèle une méthode nécessitant les mêmes paramètres que la méthode Link actuelle (donc je vais garder le même nom dans cet exemple), à savoir $tag, $method, $text, $track
- la fonction Link:
- crée une nouvelle variable $reference vide (par exemple la chaine vide ou le booléen false)
- introduit une nouvelle entrée dans un tableau (par exemple $links). Cette nouvelle entrée ressemblerait à peu près à ceci:
- array( 'tag' => $tag, 'method' => $method, 'text' => $text, 'reference' => &$reference);
- tracke le lien si nécessaire
- retourne $reference
- on stocke la valeur retournée par Link (c'est à dire $reference) dans la tableau $buffer par référence
- et on continue avec ce qu'il reste à afficher, à partir du point 1.
- en fin de script, on appèle une méthode (par exemple Flush) qui va s'occupper de générer tous les liens, en gros:
- Elle parcourt le tableau $links et construit une requête qui va vérifier l'existence de toutes les pages $links[$i]['tag'] dans la base de données en un coup.
- Elle exécute la requête et remplit un tableau avec tous les résultats (c'est à dire toutes les pages qui existaient parmi les $links[$i]['tag'])
- Elle re-parcourt le tableau $links (il faut bien...) en créant tous les liens, suivant si tag existe ou non. Les liens créés sont alors stockés dans $links[$i]['reference'], ce qui a pour effet de modifier $buffer (ben oui: on y a stocké les mêmes références !).
- elle vide $links
- elle affiche (ou retourne) $buffer et le vide.
- (appeler Flush en cours de script fonctionnerait aussi, mais cela réduit l'avantage de cette technique...)
- Je me doute qu'il y a des "oui, mais" qui surgissent, la solution n'est bien entendu pas peaufinée... On peut par exemple imaginer que Format puisse générer des tableaux du même type que $buffer, permettant ainsi de générer le contenu de sortie "dans le désordre". $buffer pourrait aussi être un objet, ce qui permettrait de "protéger" un peu les données. (surtout avec les attributs privés qui débarquent dans php5)
- Je veux bien écrire un petit script si vous voulez quelque chose de plus concret ;-)
--
LordFarquaad
Voir mes commentaires sur
SuggestionsRapiditeDeTraitement (fin de page). --
ProgFou
Pages ayant un lien vers la page courante :
ActionCalendrier
GarfieldFr
GoubS
LordFarquaad
SuggestionsRapiditeDeTraitement
PagesSuiviesParGoubs