Optimiser son site chez Online.net (et les autres)

PHP / MYSQL

Pensez à faire une sauvegarde de votre base de données avant toute opération sur celle-ci !

1 - Fermer proprement ses connexions MYSQL

Lorsqu'on se connecte à la base de données pour faire une quelquonque opération (extraction de données, mise à jour, ajout...), il est impératif de se déconnecter le plus rapidement possible afin de ne pas monopoliser une connexion MYSQL pour rien. En résumé :

  1. connexion à la base de données
  2. opération sur la base
  3. traitement PHP/SQL des données extraites
  4. déconnexion de la base

La plupart des gens ne ferment leur connexion (quand ils la ferment) à la BDD qu'en fin de script, ce qui peut représenter une fuite de ressources serveur pour rien, dès que vous n'avez plus besoin dans votre script de la base de données, il faut fermer la connexion pour la rendre disponible aux autres visiteurs du site.

Exemple de code :

<?php
 //connexion à la base
 $db=mysql_connect("sql.mondomaine", $login, $pwd);
  if(!$db){ print "erreur connection $db<br>"; exit; }

// on choisit la bonne base
 if(!mysql_select_db($base,$db)){
  print "erreur ".mysql_error()."<br>";
  mysql_close($db);
  exit;
 }

// la requête
$sql=" SELECT nom,prenom,adresse FROM tableclients";
if(!mysql_query($sql,$db)){
print "erreur $sql <br>";
}
// utilisation des données de la requête
...
// on ferme la base
mysql_close($db);
?>

2 - Libérer la mémoire qu'on a utilisé dans nos requêtes d'extraction MYSQL

Lorsqu'on a extrait de la BDD des données via une des nombreuses fonctions php à notre disposition (mysql_fetch_array, mysql_fetchrow, mysql_result...), une fois qu'on a terminé d'utiliser ces données, il faut penser à libérer sur le serveur la mémoire mise à notre disposition par le serveur avec la commande mysql_free_result(), en particulier si oin a fait des grosses requêtes, exemple :


<?php
...

// la requête
$sql="SELECT * FROM matable";
$resultat = mysql_query( $sql);

//le traitement
...

//je libère la mémoire
mysql_free_result($resultat);
?>


3 - Créer des requêtes très précises, ne jamais être dans le vague

Si vous avez besoin de 2 champs dans une table ne faites pas un SELECT *, sélectionnez uniquement vos champs. Si vous faîtes une requête par rapport à des critères, essayez de le faire sur des champs indexés, avec des valeurs uniques ex :


PAS BIEN :
$sql="SELECT * FROM tableclient WHERE nom LIKE toto";
$resultat = mysql_query( $sql);

BIEN :
$sql="SELECT nom, telephone, fax, email FROM tableclients WHERE idclient=27";
$resultat = mysql_query( $sql);

4 - mysql_fetch_array, ajoutez le paramètre MYSQL_ASSOC !

La fonction mysql_fetch_array est très fréquemment utilisée pour parcourir les lignes de données d'une requête de sélection afin de les afficher ou bien de les modifier. en général l'utilisation ressemble à ça :

<?php
...

// la requête
$sql="SELECT tel, fax FROM matable";
$resultat = mysql_query( $sql);

//le traitement
while ($ligne = mysql_fetch_array($resultat)) {
echo $ligne['fax']
...
}

?>

L'avantage de mysql_fetch_array c'est qu'il renvoie un tableau de données pour chaque enregistrement que l'on peut afficher numériquement ou par leur nom de champ, par exemple dans l'exemple ci-dessus, echo $ligne[0] ou echo $ligne['tel'] affichent tous les deux le champ téléphone. Mysql_fetch_array nous a en réalité fait deux tableaux, l'un associatif (=avec les noms de champ), l'autre numérique. Pourquoi ? Parce que cette fonction a un paramètre permettant de choisir le type désiré, les 3 options sont : MYSQL_ASSOC (=associatif), MYSQL_NUM (=numérique) et MYSQL_BOTH (=associatif ET numérique). Si on ne précise pas le type de tableau, par défaut les deux types sont créés. Comme en général on préfère utiliser les noms de champ, il vaut mieux mettre :

while ($ligne = mysql_fetch_array($resultat, MYSQL_ASSOC))

5 - Créer une version cache html de ses pages php

Attention, ENORME gain de performance

On va s'attaquer à l'un des points faibles des serveurs et d'Online en particulier, les performances php/mysql. Vous avez créé des pages qui extraient des données d'une base, qui intègrent des morceaux de code (un menu par exemple) via la fonction include, ce sont donc des pages générées dynamiquement. Mais cela fait-il de votre site un vrai site dynamique pour autant ? Dans l'immense majorité des cas, notre contenu est en réalité statique, nous ne mettons pas à jour les données de la base quotidiennement et à chaque visite, le serveur regénère la même page à la virgule près.

Si votre site n'est mis à jour qu'une fois par jour, une fois par semaine ou même une fois par mois, vous rendez-vous compte de l'immense gâchis que réprésente la génération de page systématique ? Imaginons que vous ne changiez votre contenu qu'une fois par jour et que vous ayez 500 visiteurs sur cette journée, le serveur web aura dû régénérer 500 fois la même page alors qu'en réalité 1 seule fois était nécessaire. Résultat, le serveur peut mettre plusieurs secondes pour calculer la page à chaque visiteur alors qu'une simple page html pré-calculée aurait pu être envoyée instantanément.

La solution ? La fonction ci-dessous créera une version html de n'importe quel fichier php, il suffit de lui mettre en paramètres l'adresse du script php et l'adresse du fichier html ( ($_SERVER["DOCUMENT_ROOT"].'mobilier/index.html';).

function miseencache($a, $b) {
 if (file_exists($b)) {    print 'Le fichier'.$b.' existe
';} else { //obtenir le html produit par la page php ob_start(); include($a); $HTML =ob_get_contents(); ob_end_clean(); // maintenant on crée un fichier ou mettre ce html $ecriture = fopen ( $b , "w" ) or die ( "Impossible d ouvrir $b en ecriture" ); // je vérouille le fichier en écriture par mesure de sûreté if ( !flock ( $ecriture , LOCK_EX + LOCK_NB )) { echo ( "could not lock $ecriture" ); exit; } // j'écris le contenu fwrite ( $ecriture , $HTML , strlen ( $HTML ) ); // dévérouillage du fichier flock ( $ecriture , LOCK_UN ); // fermeture du fichier fclose ( $ecriture ); } }

Votre site est réellement dynamique car mis à jour en permanence (forums par exemple) ? Ok, mais toutes vos pages sont elles concernées ? N'y a-t-il pas des pages qui ne changent jamais comme la présentation de l'entreprise, la page de contact, la page des liens etc. ? Appliquez au moins ce script sur ces pages. Je vous signale aussi qu'il existe des tas de systèmes de cache beaucoup plus évolués pour les sites réellement dynamiques (PEAR cache, JPcache...), et que la plupart des systèmes de gestion de contenu intègrent un système de cache, c'est le cas de SPIP par exemple, mais pas de Mambo. Si vous voulez utiliser un système de gestion du contenu (CMS), le critère impératif à prendre en compte pour l'hébergement chez Online c'est la qualité de son système de cache des pages générées.

6 - Utilisez régulièrement la commande SQL OPTIMIZE

Les bases de données SQL ont tendance à se fragmenter un peu au fur et à mesure qu'on les utilise, surtout si on fait beaucoup d'insertions/suppressions. Ce problème est similaire à celui de la fragmentation des partitions FAT sous Windows 9x qui nous obligeait à défragmenter les disques avec Scandisk. Il existe une fonction MYSQL équivalente (sauf qu'elle est quasiment instantanée à appliquer, pas comme scandisk ;-) ), elle s'appelle optimize. Elle est d'ailleurs accessible depuis phpmyadmin. Voici un petit script générique que vous pouvez lancer de temps en temps, il réorganisera vos tables et améliorera donc les performances tout en vous faisant gagner un poil d'espace disque :

   
<?php 

$base='votredomaine.com';
$login='votredomaine.com';
$pwd='votremotdepasseSQL';
$serveur='sql.votredomaine.com';

 //connexion à la base
 $db=mysql_connect($serveur, $login, $pwd);
  if(!$db){ print "erreur connection $db<br>"; exit; }

// on choisit la bonne base
 if(!mysql_select_db($base,$db)){
  print "erreur ".mysql_error()."<br>";
  mysql_close($db);
  exit;
 }

$tables = mysql_list_tables($base);
 echo 'BASE :'.$base.'<br>';
while (list($table_name) = mysql_fetch_array($tables)) {
   $sql = "OPTIMIZE TABLE $table_name";
   mysql_query($sql) or exit(mysql_error());
	 echo $table_name.' : optimisée<br>';
}

mysql_close($db);
?>

7 - compressez vos pages HTML avec gzip

Online n'a pas installé le module mod_gzip de PHP qui permet d'envoyer des pages compressées au navigateur de manière transparente, par contre vous pouvez le faire manuellement en utilisant les commandes ob_start(); et ob_end_flush(); :

<?php
ob_start('ob_gzhandler');  

//votre script, votre HTML...

ob_end_flush();
?>

Le gain peut être extrèmement important, la compression gzip fonctionne sur le principe de la redondance du texte, une page HTML de 10ko passe en général à 3ko, par contre un gros fichier HTML de 150ko passe à 20ko environ ! Il faut donc utiliser cette fonction par rapport vos page, étant donné qu'on fait travailler le serveur PHP, je ne pense pas qu'il faut l'utiliser si vous avez déjà des problèmes de performances PHP/MYSQL (pages qui moulinent dans le vide, interruption du script, erreur de connexion...), par contre si vous ne faites que des includes ou que vous avez un contenu essentiellement textuel, c'est une solution parfaite pour donner un coup de fouet à la vitesse de transfert des pages.

8 - Clients de longue date, assurez-vous que vos tables ne sont pas en ISAM

Mysql a plusieurs formats de stockage de données, son format des débuts s'appelait ISAM, il est aujourd'hui obsolète et remplacé depuis par le format MyISAM plus performant et permettant plus de choses (mettre des index sur les champs TEXT et BLOB par exemple). Vérifiez que vos tables sont bien au format MyISAM et pas dans l'ancien format ISAM ! Vous pouvez vérifier ça dans la colonne TYPE de PhpMyadmin et modifier ça dans les options des propriétés de tables. Voici un script qui convertira toutes vos tables vers le format MyISAM :

   
<?php 

$base='votredomaine.com';
$login='votredomaine.com';
$pwd='votremotdepasseSQL';
$serveur='sql.votredomaine.com';

 //connexion à la base
 $db=mysql_connect($serveur, $login, $pwd);
  if(!$db){ print "erreur connection $db<br>"; exit; }

// on choisit la bonne base
 if(!mysql_select_db($base,$db)){
  print "erreur ".mysql_error()."<br>";
  mysql_close($db);
  exit;
 }

$tables = mysql_list_tables($base);
 echo 'BASE : '.$base.'<br>';
while (list($table_name) = mysql_fetch_array($tables)) {
   $sql = "ALTER TABLE $table_name TYPE=MYISAM";
   mysql_query($sql) or exit(mysql_error());
	 echo $table_name.' : passée en MyISAM<br>';
}

mysql_close($db);
?>

9 - Indexez vos tables MySQL et utilisez la commande EXPLAIN

Attention, ENORME gain de performance

Prenons l'exemple d'une table "produits" de 71.052 articles comportant les champs code, designation, prix, fabricant. Si je veux avoir la liste des articles à 50 €, je fais donc la commande SQL suivante :

SELECT code,designation FROM produits WHERE prix=50

Vous remarquerez au passage que je ne ramène pas le champ prix, inutile de surchager la requête avec puisque tous les articles seront forcément à 50 euros. La requête a necessité 0,20 seconde chez Online et a donné 14 produits comme résultats.

Maintenant, au lieu de faire une requête depuis un script PHP, allons dans PHPMyAdmin et lançons la même requête dans la zone de saisie SQL, mais ajoutons EXPLAIN devant :

EXPLAIN SELECT code,designation FROM produits WHERE prix=50

Voici le résultat :

table type possible_keys key key_len ref rows Extra
produits ALL NULL NULL NULL NULL 71052 Using where

Vous aurez compris que j'ai demandé à Mysql de me donner une rapide évaluation de la qualité de ma requête. Il y aurait beaucoup à dire sur les requêtes EXPLAIN, je ne me pencherai ici que sur la signification des champs possible_keys, key et rows. Possible_keys est la liste des index potentiellement utilisables dans cette requête, key est l'index sélectionné par Mysql et rows indique le nombre de lignes que Mysql a dû lire pour exécuter ma requête.

Je n'ai aucun index de mis sur ma table, la base de données a donc dû analyser toutes les fiches produits pour me sélectionner 14 fiches !

Cliquons dans PhpMyadmin sur l'icône permettant d'indexer le champ prix et relançons la requête explain :

table type possible_keys key key_len ref rows Extra
produits ref prix prix 9 const 19 Using where

Voilà qui est intéressant ! Mysql utilise l'index que j'ai défini sur le champ prix ce qui lui permet de ne parcourir que 19 fiches produits au lieu de 71052. Online ne met plus que 0,01 seconde pour effectuer la requête, nous avons amélioré la performance d'un facteur 20 en cliquant sur une simple icone d'index dans PHPMyAdmin :-). On pourrait d'ailleurs encore améliorer les performances en affinant nos types d'index, mais je pense que cette démonstration suffira à vous convaincre : il faut absolument indexer les champs clefs de votre base, les meilleurs candidats à l'indexation sont les champs que vous utilisez dans vos requêtes après l'instruction WHERE.

10 - Méfiez vous comme de la peste du comparateur SQL LIKE

Beaucoup de manuels sur PHP/MYSQL et de sites d'apprentissage en ligne utilisent systématiquement le comparateur LIKE dans les requêtes de sélection, seulement regardez ce que donne la requête du conseil n°9 avec l'opérateur like :

EXPLAIN SELECT code,designation FROM produits WHERE prix LIKE 50

Voici le résultat :

table type possible_keys key key_len ref rows Extra
produits ALL NULL NULL NULL NULL 71052 Using where

L'utilisation de LIKE a désactivé l'utilisation de l'index ! Inutile donc de peaufiner vos index si vous utilisez LIKE dans vos requêtes comme comparateur d'égalité, ils seront tout bonnement ignorés.

11 - N'utilisez pas des extensions de fichiers .php3 mais .php

Contrairement à la majorité des hébergeurs (en fait tous ceux que je connais), Online ne fait pas interpréter les anciens scripts en php3 par son serveur PHP4 mais par un ancien serveur PHP3. Inutile de préciser que si vos scripts sont exécutés en php3, ils iront beaucoup moins vite qu'en php4, voire même ne fonctionneront pas, la limite d'exécution d'un script sur ce serveur étant de 8 secondes contre 18 secondes sur le serveur PHP4. Le problème existe en particulier pour le gestionnaire de contenu SPIP qui n'utilise que php3, il existe une version en .php : SPIP 1.7.2 en .php

12 - Mettez en place le cache HTTP

Attention, ENORME gain de performance

Nous avons déjà vu que si vos pages ne sont pas dynamiques ou bien mises à jour peu fréquemment, utiliser un cache html peut être très interessant pour soulager les ressources PHP/Mysql. En poussant la logique plus loin, à quoi sert de renvoyer une page au visiteur s'il l'a déjà visitée récemment et en garde une copie en cache ?

Pour que le visiteur ne télécharge la page que si elle n'est pas présente dans son cache, nous allons envoyer dans les entêtes http du fichier les commandes donnant la date d'expiration de la page, le navigateur ayant déjà visité votre page dans le passé et l'ayant encore en cache ne chargera la page que si sa version est notée comme ayant expiré.

Voici une fonction permettant de fixer la durée de vie de la page :

   
<?php 
function expiration($temps){
 Header("Cache-Control: must-revalidate");

 $delai = 86400 * $temps;
 // remplacer par la ligne suivante pour exprimer la durée en heures
 // $delai = 3600 * $temps;
 $ExpStr = "Expires: " . gmdate("D, d M Y H:i:s", time() + $delai) . " GMT";
 Header($ExpStr);
 }
 
?>

Attention, il faut envoyer les entêtes avant tout autre contenu de la page, même un simple espace placé avant l'appel de cette fonction provoquera une erreur du script.

13 - Ne recalculez pas une valeur inutilement dans une boucle

Une erreur que je vois assez souvent passer est de refaire le même calcul x fois dans une boucle, si ce calcul ne dépend pas de variables de la boucle, il n'y a aucune raison de le faire plusieurs fois, il faut faire le calcul avant de rentrer dans la boucle et mettre le résultat dans une variable.

Voici un exemple courant où il est inutile de faire calculer par PHP la taille d'une chaîne à chaque itération puisque cette taille ne change pas :


PAS BIEN :   
<?php 
for($x=0; $x<$sizeof($y);$x++)
	{
   // traitements
   }
?>


BIEN :   
<?php 
$taille_y = sizeof($y);
for($x=0; $x<$taille_y;$x++)
	{
   // traitements
   }
?>