-
Web development
[MySQL] Optimisation d'une recherche avec multiples jointures
par Skreo, le 10 Décembre 2009 à 16:42J'ai été confronté à un problème de ralentissements sur EklaBlog dûs à une requête SELECT utilisant de multiples jointures. Nous allons voir dans cet article comment optimiser cela.
La requête en elle-même n'est pas très complexe et utilise uniquement des clés (primary et indexes), mais elle est très lente pour plusieurs raisons :
- Jointure sur 5 tables
- Recherche sur 4 champs chacun dans une table différente
- Tri par ORDER BY
Elle ressemble en gros à ceci :
SELECT a.id, a.blog, a.category_id, a.title, a.content, a.author, a.creadt, c.url
FROM mod_article a
INNER JOIN categories b ON b.id=a.category_id
INNER JOIN blogs c ON c.id=b.blog_id
INNER JOIN articles_tags at ON at.article_id=a.id
INNER JOIN tags t ON t.id=at.tag_id
WHERE t.name="skreo" AND a.online="1" AND b.protect="" AND c.protect=""
ORDER BY a.creadt DESC
LIMIT 10Les tables concernées sont relativement grosses :
- 130 000 entrées dans tags
- 1 400 000 entrées dans articles_tags
- 400 000 entrées dans articles
- 70 000 entrées dans categories
- 130 000 entrées dans tags
- 19 000 entrées dans blogs
D'où un temps d'exécution de la requête pouvant atteindre 30 secondes...
Lors d'un test en local, que je répèterai dans la suite de l'article, j'obtiens une exécution en 0.24s en moyenne quand le pc ne fait rien d'autre.
Solution possible
Une solution possible que m'a indiqué mon ami Guilhem est de stocker un résultat intermédiaire de la requête dans une table de type MEMORY (en RAM) :
CREATE TABLE tags_lookup (
article_id int(10) unsigned NOT NULL,
tag_id int(10) unsigned NOT NULL,
creadt int(10) unsigned NOT NULL,
blog_id int(10) unsigned NOT NULL,
KEY tag_id (tag_id),
KEY creadt (creadt),
KEY blog_id (blog_id)
) ENGINE=MEMORYIl faut ensuite remplir la remplir :
# On vide la table
TRUNCATE TABLE tags_lookup;
# Puis on la remplit entièrement
INSERT INTO tags_lookup
SELECT a.id AS article_id, at.tag_id, a.creadt, c.id AS blog_id
FROM articles a, categories b, blogs c, articles_tags at
WHERE a.online="1" AND b.protect="" AND c.protect=""
AND b.id=a.category_id AND c.id=a.blog_id AND at.article_id=a.id;Il faudra exécuter les requêtes ci-dessus régulièrement pour mettre à jour le contenu de la table, à l'aide d'un bash par exemple.
Pour obtenir les informations qu'on voulait au début, on peut alors exécuter cette requête :
SELECT a.id, a.blog, a.category_id, a.title, a.content, a.author, a.creadt, c.url
FROM tags t
INNER JOIN tags_lookup s ON s.tag_id=t.id
INNER JOIN articles a ON a.id=s.article_id
INNER JOIN categories b ON b.id=a.category_id
INNER JOIN blogs c ON c.id=a.blog _id
WHERE t.name="skreo"
ORDER BY s.creadt DESC
LIMIT 10Lors de mon test, j'obtiens cette fois-ci 0.15s d'exécution en moyenne, ce qui est déjà mieux.
Mais le gros avantage d'une telle structure, c'est qu'on va pouvoir faire une recherche beaucoup plus facile à comprendre pour le serveur MySQL, grâce à des requêtes imbriquées :
SELECT a.id, a.blog, a.category_id, a.title, a.content, a.author, a.creadt, c.url
FROM (
SELECT article_id
FROM tags_lookup
WHERE tag_id=(
SELECT id
FROM tags
WHERE name="skreo"
LIMIT 1
)
ORDER BY creadt DESC
LIMIT 10
) s
INNER JOIN articles a ON a.id=s.article_id
INNER JOIN categories b ON b.id=a.category_id
INNER JOIN blogs c ON c.id=a.blog_id
ORDER BY a.creadt DESC
LIMIT 10;Toujours avec la même base de données, j'obtiens cette fois-ci une exécution presque instantanée : entre 0 et 0.02 secondes !
Conclusion
En mettant en place une table en mémoire vive rassemblant des données correspondant à une partie du traitement d'une grosse requête, on peut donc éviter des jointures très lourdes.
Cette méthode a bien évidemment comme inconvénient d'avoir des données pas toujours ultra fraiches en mémoire, mais ça peut être un bon compromis quand on voit la rapidité des requêtes qui en résultent.
Vous avez d'autres solutions intéressantes ? une meilleure idée ?
5 commentaires
22,6 Kg de flyers EklaBlog
par Skreo, le 16 Mars 2009 à 15:54Un gros colis est arrivé chez moi : les 10 000 flyers EklaBlog !
Va falloir qu'on m'aide à les distribuer ^^
10 commentaires
[Sécurité PHP] Toujours vérifier le Referer lors d'un envoi par POST
par Skreo, le 26 Décembre 2008 à 14:27![[Sécurité PHP] Toujours vérifier le Referer lors d'un envoi par POST](http://data0.eklablog.com/skreo/mod_article313986_1.jpg)
C'est un aspect très souvent négligé de la sécurité des sites web, et qui est pourtant critique.
Par exemple, si Facebook n'avait pas prévu ce genre de sécurité, nous pourrions partir du postulat qu'il y a une chance non négligeable qu'un visiteur soit connecté à Facebook (par session ou cookies). Prenons alors une page web visitée par quelques dizaines ou centaines de visiteurs par jour, un blog par exemple. Sur cette page, nous allons mettre un formulaire identique à celui permettant de modifier le profil sur Facebook. Une ligne de javascript suffira à envoyer le formulaire ( $("id_form").submit() ), avec tout plein de grossièretés qui viendront égayer le profil de l'utilisateur

C'est de cette manière qu'Olivier Duffez s'est fait pirater son compte Gmail puis le nom de domaine Webrankinfo.com ! En effet, le pirate s'est débrouillé pour que M. Duffez se retrouve sur une page envoyant un formulaire vers la page de création de filtre de Gmail. Et comme une session Gmail était ouverte sur l'ordinateur de M. Duffez, le formulaire en question a pu créer un filtre qui transférait tous les e-mails reçus au spammeur. Le spammeur a ensuite utilisé le formulaire "Mot de passe oublié" du registrar chez lequel est enregistré webrankinfo.com, et a donc reçu l'e-mail permettant de modifier le mot de passe.
Alors ? N'est-ce pas critique comme faille ?
Ce type d'attaque est appelé Cross-Site Request Forgery, et n'est pas très connu en raison d'un manque de communication à ce propos.
Voyons comment l'étudier concrètement, et comment éviter ce genre de déboires.Pour vous montrer un exemple concret, créons une page affichant les données reçues par POST :
http://data0.eklablog.com/skreo/perso/test_referer.phpCode de la page :
Ensuite, sur un nom de domaine différent (ici skreo.net), créons un petit formulaire qui envoie des données à cette page :
<form action="http://data0.eklablog.com/skreo/perso/test_referer.php" method="post" id="form_test_referer">
Variable "var" : <input type="text" value="" name="var" /><br />
<input type="submit" value="Envoyer" />
</form>Ce qui donne ceci :
Vous pouvez tester ce formulaire, vous voyez bien qu'on peut envoyer n'importe quelles données par POST vers un autre domaine. Un petit bout de code Javascript à la suite du formulaire permet d'envoyer le formulaire immédiatement sans même demander l'avis du visiteur :
Voyons maintenant comment empêcher dans la majorité des cas ce genre de hacks, avec l'aide du Referer. Je dis bien "dans la majorité des cas", car parfois le visiteur règle son navigateur internet pour que le referer soit vide.
Nous allons donc comparer le Referer avec le nom de domaine de la page courante, et vider la variable $_POST s'il est différent. Reprenons notre fichier test_referer.php et nommons le test_referer2.php :
<?php
// Vérification du Referer pour les variables passées en POST
if(isset($_SERVER['HTTP_REFERER'])
&& $_SERVER['HTTP_REFERER']!=''
&& substr($_SERVER['HTTP_REFERER'], 7, strlen($_SERVER['SERVER_NAME'])) != $_SERVER['SERVER_NAME']){
$_POST = array();
}
?>
<pre>
Données reçues par POST :
$_POST = <?php
print_r($_POST);
?>
</pre>Reprenons notre formulaire pour tester, mais cette fois vers test_referer2.php :
Et le tour est joué !
La page accepte toujours les envois de données par POST provenant du même nom de domaine, mais n'accepte pas les autres !Si vous avez un site web, je vous conseille très fortement de mettre en place cette sécurité, si ce n'est pas déjà le cas

18 commentaires
Détecter les sites que vos visiteurs utilisent
par Skreo, le 1 Août 2008 à 02:34Je me suis souvenu d'un article très intéressant sur lequel j'étais tombé il y a un ou deux mois, présentant une classe javascript permettant de détecter quels réseaux sociaux utilisent les visiteurs. En regardant le code de plus prêt, je l'ai trouvé compliqué pour pas grand chose, avec l'utilisation d'une iframe et un code pas très optimisé.
Voici donc un code que je viens de pondre, parce que je pense qu'il pourra être utile, aussi bien pour vous que pour moi
J'utilise ici le framework Mootools 1.2 , mais je pense que c'est facilement adaptable à d'autres. Le principe est simple, on utilise une "faille" du CSS qui consiste à récupérer le style de liens après avoir appliqué un style particulier aux liens visités ("a:visited").Une petite démo pour commencer : checkvisited.html
On va donc commencer par faire un peu de CSS :
a.test_visited { display: block; }
a.test_visited:visited { display: none; }C'est tout pour le CSS !
Maintenant, le Javascript :// Implémentation de la fonction checkVisited
// pour les chaînes de caractères (String)
String.implement({
checkVisited : function(){
var e = (new Element("a", {
href: this,
"class" : "test_visited"
}))
.inject(document.body),
b = e.getStyle("display")=="none";
e.destroy();
return b;
}
});
// Implémentation de la fonction checkVisited pour
// les tableaux (Array) : tester une liste d'urls
Array.implement({
checkVisited : function(){
var b = false;
this.each(function(s){
if(s.checkVisited())
b = true;
})
return b;
}
});Ces deux implémentations permettent de vérifier une URL ou une liste d'URLs.
On va reprendre l'exemple des réseaux sociaux que traite Aza Raskin dans son article. D'abord, on crée un conteneur pour l'affichage du résultat :
Ensuite, on crée la liste et on la traite :
// Liste de réseaux sociaux
var sites = {
"Digg": ["http://digg.com", "http://digg.com/login"],
"Reddit": ["http://reddit.com", "http://reddit.com/new/", "http://reddit.com/controversial/", "http://reddit.com/top/", "http://reddit.com/r/reddit.com/", "http://reddit.com/r/programming/"],
"StumbleUpon": ["http://stumbleupon.com"],
"Yahoo Buzz": ["http://buzz.yahoo.com"],
"Facebook": ["http://facebook.com/home.php", "http://facebook.com", "https://login.facebook.com/login.php"],
"Del.icio.us": ["https://secure.del.icio.us/login", "http://del.icio.us/", "http://delicious.com"],
"MySpace": ["http://www.myspace.com/"],
"Technorati": ["http://www.technorati.com"],
"Newsvine": ["https://www.newsvine.com", "https://www.newsvine.com/_tools/user/login"],
"Songza": ["http://songza.com"],
"Slashdot": ["http://slashdot.org/"],
"Ma.gnolia": ["http://ma.gnolia.com/"],
"Blinklist": ["http://www.blinklist.com"],
"Furl": ["http://furl.net", "http://furl.net/members/login"],
"Mister Wong": ["http://www.mister-wong.com"],
"Current": ["http://current.com", "http://current.com/login.html"],
"Menaeme": ["http://meneame.net", "http://meneame.net/login.php"],
"Oknotizie": ["http://oknotizie.alice.it", "http://oknotizie.alice.it/login.html.php"],
"Diigo": ["http://www.diigo.com/", "https://secure.diigo.com/sign-in"],
"Funp": ["http://funp.com", "http://funp.com/account/loginpage.php"],
"Blogmarks": ["http://blogmarks.net"],
"Yahoo Bookmarks": ["http://bookmarks.yahoo.com"],
"Xanga": ["http://xanga.com"],
"Blogger": ["http://blogger.com"],
"Last.fm": ["http://www.last.fm/", "https://www.last.fm/login/"],
"N4G": ["http://www.n4g.com"],
"Faves": ["http://faves.com", "http://faves.com/home", "https://secure.faves.com/signIn"],
"Simpy": ["http://www.simpy.com", "http://www.simpy.com/login"],
"Yigg": ["http://www.yigg.de"],
"Kirtsy": ["http://www.kirtsy.com", "http://www.kirtsy.com/login.php"],
"Fark": ["http://www.fark.com", "http://cgi.fark.com/cgi/fark/users.pl?self=1"],
"Mixx": ["https://www.mixx.com/login/dual", "http://www.mixx.com"],
"Google Bookmarks": ["http://www.google.com/bookmarks", "http://www.google.com/ig/add?moduleurl=bookmarks.xml&hl=en"],
"Subbmitt": ["http://subbmitt.com/"]
},
t = []; // Variable dans laquelle on va stocker la liste de sites visités par l'utilisateur
// On vérifie pour chaque site de la liste
for(var i in sites){
if(sites[i].checkVisited())
t.push(i);
}
// On affiche le résultat
$("visited_sites").set("html", "<strong>Sites visités : </strong>" + t.join(", "));C'est fini !
Chez moi, ça fonctionne impeccablement avec Firefox 3, Opéra 9.5, IE 6, et IE 7.
Si vous avez un problème d'incompatibilité ou une suggestion, n'hésitez pas ! Par exemple, on pourrait, comme Aza, traiter pour une URL quelconque l'URL avec et sans les "www".Merci de me faire un petit lien de retour si vous utilisez ce script quelque part

4 commentaires
[PHP] Enlever tous les accents d'une chaîne
par Skreo, le 25 Juillet 2008 à 20:46Voici ma fonction pour enlever tous les accents d'une chaîne de caractères tout en respectant l'encodage (cette fonction traite très bien les textes UTF-8 par exemple) :
function removeAccents($txt){
$txt = str_replace('œ', 'oe', $txt);
$txt = str_replace('Œ', 'Oe', $txt);
$txt = str_replace('æ', 'ae', $txt);
$txt = str_replace('Æ', 'Ae', $txt);
mb_regex_encoding('UTF-8');
$txt = mb_ereg_replace('[ÀÁÂÃÄÅĀĂǍẠẢẤẦẨẪẬẮẰẲẴẶǺĄ]', 'A', $txt);
$txt = mb_ereg_replace('[àáâãäåāăǎạảấầẩẫậắằẳẵặǻą]', 'a', $txt);
$txt = mb_ereg_replace('[ÇĆĈĊČ]', 'C', $txt);
$txt = mb_ereg_replace('[çćĉċč]', 'c', $txt);
$txt = mb_ereg_replace('[ÐĎĐ]', 'D', $txt);
$txt = mb_ereg_replace('[ďđ]', 'd', $txt);
$txt = mb_ereg_replace('[ÈÉÊËĒĔĖĘĚẸẺẼẾỀỂỄỆ]', 'E', $txt);
$txt = mb_ereg_replace('[èéêëēĕėęěẹẻẽếềểễệ]', 'e', $txt);
$txt = mb_ereg_replace('[ĜĞĠĢ]', 'G', $txt);
$txt = mb_ereg_replace('[ĝğġģ]', 'g', $txt);
$txt = mb_ereg_replace('[ĤĦ]', 'H', $txt);
$txt = mb_ereg_replace('[ĥħ]', 'h', $txt);
$txt = mb_ereg_replace('[ÌÍÎÏĨĪĬĮİǏỈỊ]', 'I', $txt);
$txt = mb_ereg_replace('[ìíîïĩīĭįıǐỉị]', 'i', $txt);
$txt = str_replace('Ĵ', 'J', $txt);
$txt = str_replace('ĵ', 'j', $txt);
$txt = str_replace('Ķ', 'K', $txt);
$txt = str_replace('ķ', 'k', $txt);
$txt = mb_ereg_replace('[ĹĻĽĿŁ]', 'L', $txt);
$txt = mb_ereg_replace('[ĺļľŀł]', 'l', $txt);
$txt = mb_ereg_replace('[ÑŃŅŇ]', 'N', $txt);
$txt = mb_ereg_replace('[ñńņňʼn]', 'n', $txt);
$txt = mb_ereg_replace('[ÒÓÔÕÖØŌŎŐƠǑǾỌỎỐỒỔỖỘỚỜỞỠỢ]', 'O', $txt);
$txt = mb_ereg_replace('[òóôõöøōŏőơǒǿọỏốồổỗộớờởỡợð]', 'o', $txt);
$txt = mb_ereg_replace('[ŔŖŘ]', 'R', $txt);
$txt = mb_ereg_replace('[ŕŗř]', 'r', $txt);
$txt = mb_ereg_replace('[ŚŜŞŠ]', 'S', $txt);
$txt = mb_ereg_replace('[śŝşš]', 's', $txt);
$txt = mb_ereg_replace('[ŢŤŦ]', 'T', $txt);
$txt = mb_ereg_replace('[ţťŧ]', 't', $txt);
$txt = mb_ereg_replace('[ÙÚÛÜŨŪŬŮŰŲƯǓǕǗǙǛỤỦỨỪỬỮỰ]', 'U', $txt);
$txt = mb_ereg_replace('[ùúûüũūŭůűųưǔǖǘǚǜụủứừửữự]', 'u', $txt);
$txt = mb_ereg_replace('[ŴẀẂẄ]', 'W', $txt);
$txt = mb_ereg_replace('[ŵẁẃẅ]', 'w', $txt);
$txt = mb_ereg_replace('[ÝŶŸỲỸỶỴ]', 'Y', $txt);
$txt = mb_ereg_replace('[ýÿŷỹỵỷỳ]', 'y', $txt);
$txt = mb_ereg_replace('[ŹŻŽ]', 'Z', $txt);
$txt = mb_ereg_replace('[źżž]', 'z', $txt);
return $txt;
}On peut faire plus simple, certes, mais contrairement à ce qu'on peut voir habituellement, cette fonction traite véritablement tous les accents existants, et fonctionne avec tous les encodages.
3 commentaires
[1] 2 3 4 5

Suivre le flux RSS des articles de cette rubrique
Suivre le flux RSS des commentaires de cette rubrique
Haut de page