<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>dbnewz &#187; tuning</title>
	<atom:link href="http://www.dbnewz.com/tag/tuning/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.dbnewz.com</link>
	<description>le blog français sur les SGBD - MySQL, Oracle et plus...</description>
	<lastBuildDate>Wed, 28 Jul 2010 14:01:15 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Comment réécrire une requête SQL ? Partie 2</title>
		<link>http://www.dbnewz.com/2009/12/21/comment-reecrire-une-requete-sql-partie-2/</link>
		<comments>http://www.dbnewz.com/2009/12/21/comment-reecrire-une-requete-sql-partie-2/#comments</comments>
		<pubDate>Mon, 21 Dec 2009 20:54:33 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=443</guid>
		<description><![CDATA[Dans le précédent post, nous avons optimisé une requête en abandonnant un des principes du SQL (dire au SGBD ce qu&#8217;on souhaite faire, mais pas comment le faire). Ici nous allons voir un exemple où le fait de penser en SQL va nous permettre de rendre performante une requête difficile à améliorer.
Nous repartons de la [...]]]></description>
			<content:encoded><![CDATA[<p>Dans le précédent post, nous avons optimisé une requête en abandonnant un des principes du SQL (dire au SGBD ce qu&#8217;on souhaite faire, mais pas comment le faire). Ici nous allons voir un exemple où le fait de penser en SQL va nous permettre de rendre performante une requête difficile à améliorer.</p>
<p><span id="more-443"></span>Nous repartons de la table du post précédent, remplie avec 500 000 enregistrements :<br />
<code>CREATE TABLE product (<br />
product_id int(11) NOT NULL AUTO_INCREMENT,<br />
category_id int(11) NOT NULL DEFAULT 0,<br />
reference varchar(20) NOT NULL DEFAULT '',<br />
name varchar(25) NOT NULL DEFAULT '',<br />
sold int(11) NOT NULL DEFAULT 0,<br />
PRIMARY KEY (product_id)<br />
) ENGINE=MyISAM;</code></p>
<p>Nous voulons, à partir de cette table de produits, trouver pour chaque catégorie le produit qui s&#8217;est le plus vendu.</p>
<p>Première idée : raisonner en terme de boucle, c&#8217;est-à-dire demander au SGBD de retrouver pour chaque catégorie le produit qui s&#8217;est le plus vendu. Cela donne en SQL la requête suivante :<br />
<code><br />
mysql&gt; SELECT sql_no_cache pdt.*<br />
FROM product pdt<br />
WHERE sold =<br />
(SELECT MAX(sold) FROM product<br />
WHERE category_id = pdt.category_id);<br />
</code></p>
<p>On note la sous-requête corrélée, qui traduit  en SQL notre idée de boucle à travers l&#8217;ensemble des catégories.</p>
<p>Temps d&#8217;exécution : très long&#8230; en effet j&#8217;ai arrêté l&#8217;exécution de la requête au bout de 20 mn, et toujours pas de résultat en vue à ce moment-là ! Un bon index est sans doute nécessaire&#8230;</p>
<p>Examinons le résultat de la commande EXPLAIN :<br />
<code><br />
mysql&gt; EXPLAIN SELECT...\G<br />
***************** 1. row *****************<br />
id: 1<br />
select_type: PRIMARY<br />
table: pdt<br />
type: ALL<br />
possible_keys: NULL<br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
rows: 500000<br />
Extra: Using where<br />
***************** 2. row *****************<br />
id: 2<br />
select_type: DEPENDENT SUBQUERY<br />
table: product<br />
type: ALL<br />
possible_keys: NULL<br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
rows: 500000<br />
Extra: Using where<br />
2 rows in set (0,00 sec)<br />
</code></p>
<p>Effectivement, ce n&#8217;est pas fameux : pour chaque ligne de la table product, MySQL va exécuter la sous-requête, qui elle-même fait un scan complet de la table product&#8230; On est dans une situation bien pire qu&#8217;un CROSS JOIN, ce qui explique la lenteur constatée.</p>
<p>Un index composite sur (category_id, sold) va bien nous permettre d&#8217;améliorer la sous-requête&#8230;<br />
<code><br />
mysql&gt; ALTER TABLE product add index idx_category_sold (category_id,sold);<br />
mysql&gt; EXPLAIN SELECT ...\G<br />
***************** 1. row *****************<br />
id: 1<br />
select_type: PRIMARY<br />
table: pdt<br />
type: ALL<br />
possible_keys: NULL<br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
rows: 500000<br />
Extra: Using where<br />
***************** 2. row *****************<br />
id: 2<br />
select_type: DEPENDENT SUBQUERY<br />
table: product<br />
type: ref<br />
possible_keys: idx_category_sold<br />
key: idx_category_sold<br />
key_len: 4<br />
ref: test.pdt.category_id<br />
rows: 500<br />
Extra: Using index<br />
2 rows in set (0,00 sec)<br />
</code></p>
<p>&#8230; mais il n&#8217;existe pas de moyen d&#8217;améliorer la requête principale.</p>
<p>Le temps d&#8217;exécution après ajout de l&#8217;index est maintenant de 2mn15, ce qui est encore loin d&#8217;être satisfaisant.</p>
<p>La limitation de notre requête, comme nous l&#8217;a montré la commande EXPLAIN, c&#8217;est que pour chacune des 500 000 lignes de la table, MySQL va devoir exécuter la sous-requête. Cela signifie que même en optimisant au mieux la sous-requête, celle-ci sera toujours exécutée 500 000 fois, ce qui est forcément couteux. Ajouter un index ne fait que limiter les dégâts, mais ne suffit pas pour obtenir des performances correctes.</p>
<p>La vraie solution à notre problème va consister à changer de point de vue sur la demande initiale formulée en langage courant, afin de pouvoir écrire la requête d&#8217;une toute autre manière, qui, espérons-le, sera plus efficace.</p>
<p>Nous allons donc raisonner de façon ensembliste. Avec notre table, il nous est possible de constituer deux ensembles : l&#8217;ensemble E1 des informations sur les produits (facile : SELECT * FROM product) et l&#8217;ensemble E2 des produits qui se sont le mieux vendus (facile aussi : SELECT category_id, MAX(sold) FROM product GROUP BY category_id). Si nous sommes capables de faire l&#8217;intersection entre E1 et E2, nous aurons résolu notre problème.</p>
<p>Or faire l&#8217;intersection de deux ensembles se traduit en SQL par une jointure entre deux tables. La solution est donc toute tracée :<br />
<code><br />
mysql&gt; SELECT sql_no_cache pdt.* FROM (<br />
SELECT category_id, MAX(sold) as maxi<br />
FROM product<br />
GROUP BY category_id<br />
) AS maxi_list<br />
INNER JOIN product pdt<br />
ON pdt.category_id = maxi_list.category_id<br />
AND pdt.sold = maxi_list.maxi;<br />
</code></p>
<p>EXPLAIN nous montre comment est exécutée cette nouvelle requête :<br />
<code><br />
mysql&gt; EXPLAIN SELECT ... \G<br />
***************** 1. row *****************<br />
id: 1<br />
select_type: PRIMARY<br />
table:<br />
type: ALL<br />
possible_keys: NULL<br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
rows: 1000<br />
Extra:<br />
***************** 2. row *****************<br />
id: 1<br />
select_type: PRIMARY<br />
table: pdt<br />
type: ref<br />
possible_keys: idx_category_sold<br />
key: idx_category_sold<br />
key_len: 8<br />
ref: maxi_list.category_id,maxi_list.maxi<br />
rows: 1<br />
Extra:<br />
***************** 3. row *****************<br />
id: 2<br />
select_type: DERIVED<br />
table: product<br />
type: range<br />
possible_keys: NULL<br />
key: idx_category_sold<br />
key_len: 4<br />
ref: NULL<br />
rows: 1001<br />
Extra: Using index for group-by<br />
3 rows in set (0,05 sec)<br />
</code></p>
<p>MySQL exécute d&#8217;abord la sous-requête puis place le résultat dans une table temporaire, qui est jointe à la table product. A noter que la jointure se fait avec deux conditions, qui sont nécessaires toutes les deux.</p>
<p>Quel est le temps d&#8217;exécution de cette requete ? 0.06s&#8230; Pas besoin de commentaire, le gain est vertigineux !</p>
<p>Ces deux articles nous ont donc permis de voir que la manière dont une requête est formulée peut avoir des conséquences très importantes sur les temps d&#8217;exécution. Il n&#8217;existe pas de règle pour savoir si une requête est bien écrite ou pas, mais quand vous rencontrez une requête utilisant de bons index mais qui est lente, il peut être très intéressant de réfléchir à son sens pour trouver une réécriture qui sera performante.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/12/21/comment-reecrire-une-requete-sql-partie-2/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>A better MySQLTuner &#8211; Sheeri K. Cabral</title>
		<link>http://www.dbnewz.com/2009/08/22/a-better-mysqltuner-sheeri-k-cabral/</link>
		<comments>http://www.dbnewz.com/2009/08/22/a-better-mysqltuner-sheeri-k-cabral/#comments</comments>
		<pubDate>Sat, 22 Aug 2009 13:39:38 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[OpenSQLCamp]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=334</guid>
		<description><![CDATA[MySQLTuner est un script Perl qui produit un rapport sur la configuration de votre serveur MySQL et donne des pistes d&#8217;optimisation. On peut bien sûr s&#8217;interroger sur la manière dont l&#8217;analyse est faite et surtout sur la pertinence des recommendations. C&#8217;est exactement l&#8217;exercice qu&#8217;a fait Sheeri pour nous, en examinant le script sur toutes ses [...]]]></description>
			<content:encoded><![CDATA[<p>MySQLTuner est un script Perl qui produit un rapport sur la configuration de votre serveur MySQL et donne des pistes d&#8217;optimisation. On peut bien sûr s&#8217;interroger sur la manière dont l&#8217;analyse est faite et surtout sur la pertinence des recommendations. C&#8217;est exactement l&#8217;exercice qu&#8217;a fait Sheeri pour nous, en examinant le script sur toutes ses coutures.</p>
<p>Il en ressort que pas mal d&#8217;affirmations et de recommendations sont hardcodées et ne tiennent absolument pas compte des spécificités de votre base. Un exemple ? Si le cache de requêtes est désactivé, alors le script va systématiquement vous remonter qu&#8217;il s&#8217;agit d&#8217;un problème, même si vous avez sciemment désactivé ce cache.</p>
<p>A partir de toutes ces constatations, Sheeri a commencé à faire évoluer le script, en ajoutant pour l&#8217;instant quelques options intéressantes, comme celle permettant de formater le rapport de manière à le rendre facilement lisible par un tableur : il devient ainsi plus facile de voir l&#8217;évolution de certains indicateurs dans le temps en lançant le script à intervalles réguliers. A suivre !</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/08/22/a-better-mysqltuner-sheeri-k-cabral/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Cardinalité, sélectivité et distributivité d&#8217;un index MySQL : quel impact sur le plan d&#8217;exécution ?</title>
		<link>http://www.dbnewz.com/2008/09/05/cardinalite-selectivite-et-distributivite-dun-index-mysql-quel-impact-sur-le-plan-dexecution/</link>
		<comments>http://www.dbnewz.com/2008/09/05/cardinalite-selectivite-et-distributivite-dun-index-mysql-quel-impact-sur-le-plan-dexecution/#comments</comments>
		<pubDate>Thu, 04 Sep 2008 22:37:26 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[pratique]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=62</guid>
		<description><![CDATA[Nouveau volet de notre série consacrée aux index, la notion de sélectivité d&#8217;un index est ici à l&#8217;honneur.Souvenez-vous du billet précédent : notre table de test contient 1 million de lignes et un champ &#171;&#160;flag&#160;&#187; à la cardinalité très faible : 2.Cardinalité ? Sélectivité ? Avant d&#8217;aller plus loin, deux définitions s&#8217;imposent. Tout d&#8217;abord la [...]]]></description>
			<content:encoded><![CDATA[<p>Nouveau volet de notre série consacrée aux <a href="http://www.dbnewz.com/tag/index/" target="_blank">index</a>, la notion de sélectivité d&#8217;un index est ici à l&#8217;honneur.<br id="jxwn" />Souvenez-vous du <a href="http://www.dbnewz.com/2008/08/19/generer-un-jeu-de-donnees-shell-mysqlslap-et-procedure-stockee/" target="_blank">billet précédent</a> : notre table de test contient 1 million de lignes et un champ &laquo;&nbsp;flag&nbsp;&raquo; à la cardinalité très faible : 2.<br id="ei5l" /><br id="yg2j" />Cardinalité ? Sélectivité ? Avant d&#8217;aller plus loin, deux définitions s&#8217;imposent. Tout d&#8217;abord <strong>la cardinalité d&#8217;un index</strong> : c&#8217;est le nombre de valeurs uniques qu&#8217;il contient.</p>
<p>La sélectivité d&#8217;un index se calcule elle de la façon suivante :<br id="ei5l2" /><br id="w:n." /><strong>Selectivité = Cardinalité(Idx) / Nombre total d&#8217;enregistrements dans la table</strong><br id="hmwy" />Résultat des courses notre index à une sélectivité de 2 /1 000 000 = 2e-6, qui a dit &laquo;&nbsp;peu sélectif&nbsp;&raquo; ?<br id="w:n.0" /></p>
<p>A l&#8217;opposé d&#8217;une si faible sélectivité on trouve celle d&#8217;une clé primaire puisque celle-ci a <strong>une selectivité de 1</strong>. Exprimé autrement : à une &laquo;&nbsp;entrée&nbsp;&raquo; de la clé primaire ne correspond qu&#8217;un seul enregistrement dans la table.</p>
<p>D&#8217;où la règle générale suivante : plus un index est sélectif, plus il est efficace, c&#8217;est à dire capable d&#8217;identifier rapidement la ou les données recherchées.<br id="i5lm" /></p>
<p><strong>L&#8217;optimiseur MySQL</strong> est chargé de transformer votre requête en un plan d&#8217;exécution le plus efficace possible, suit-il à la lettre cette règle générale ?</p>
<p>Vous vous doutez bien que non&#8230;</p>
<p><span id="more-62"></span><strong>Rentrée oblige : </strong><strong id="uxeq">le cas d&#8217;école</strong><br id="uxeq0" /><br id="uxeq1" />Débutons par le cas &laquo;&nbsp;standard&nbsp;&raquo;, celui qui effectivement illustre qu&#8217;un index sélectif est efficace. Un tel index a donc toutes les chances d&#8217;être choisi par l&#8217;optimiseur MySQL lors de la conception du <strong>plan d&#8217;exécution</strong> de la requête (s&#8217;il est pertinent pour celle-ci bien sûr).<br id="tj3h" /><br id="tj3h0" />On utilise ici la base de test <a href="http://dev.mysql.com/doc/" target="_blank">sakila</a> afin d&#8217;afficher la liste des films de l&#8217;acteur dont le nom est &#8216;CRUZ&#8217; :<br id="tj3h1" /> <code><br id="qk4r" />mysql&gt; EXPLAIN SELECT f.title<br id="rzbk" />FROM film f, actor a, film_actor fa<br id="rzbk0" />WHERE a.actor_id = fa.actor_id<br id="rzbk1" />AND fa.film_id = f.film_id<br id="rzbk2" />AND a.last_name = 'CRUZ'\G</code><br id="rzbk3" /><br id="m3yq0" /><code>*************************** 1. row <br id="m3yq1" /> id: 1<br id="m3yq2" /> select_type: SIMPLE<br id="m3yq3" /> table: a<br id="m3yq4" /> type: <strong>ref</strong><br id="m3yq5" />possible_keys: PRIMARY,idx_actor_last_name<br id="m3yq6" /> key: <strong>idx_actor_last_name</strong><br id="m3yq7" /> key_len: 137<br id="m3yq8" /> ref: const<br id="m3yq9" /> rows: <strong>1</strong><br id="m3yq10" /> Extra: Using where; <strong>Using index</strong><br id="m3yq11" />*************************** 2. row <br id="m3yq12" /> id: 1<br id="m3yq13" /> select_type: SIMPLE<br id="m3yq14" /> table: fa<br id="m3yq15" /> type: <strong>ref</strong><br id="m3yq16" />possible_keys: PRIMARY,idx_fk_film_id<br id="m3yq17" /> key: <strong>PRIMARY</strong><br id="m3yq18" /> key_len: 2<br id="m3yq19" /> ref: sakila.a.actor_id<br id="m3yq20" /> rows: <strong>13</strong><br id="m3yq21" /> Extra: <strong>Using index</strong><br id="m3yq22" />*************************** 3. row <br id="m3yq23" /> id: 1<br id="m3yq24" /> select_type: SIMPLE<br id="m3yq25" /> table: f<br id="m3yq26" /> type: <strong>eq_ref</strong><br id="m3yq27" />possible_keys: PRIMARY<br id="m3yq28" /> key: <strong>PRIMARY</strong><br id="m3yq29" /> key_len: 2<br id="m3yq30" /> ref: sakila.fa.film_id<br id="m3yq31" /> rows: <strong>1</strong><br id="m3yq32" /> Extra:<br id="m3yq33" />3 rows in set (0.00 sec)</code><br id="o:ex" /><br id="o:ex0" />La commande EXPLAIN indique le plan d&#8217;exécution de la requête : quelles informations en tirer du point de vue des index utilisés ?</p>
<p>L&#8217;optimiseur a tout d&#8217;abord décidé de restreindre notre recherche grâce au critère &laquo;&nbsp;nom de l&#8217;acteur&nbsp;&raquo;, pour cela il utilise l&#8217;index &laquo;&nbsp;idx_actor_last_name&nbsp;&raquo; de la table &laquo;&nbsp;actor&nbsp;&raquo;.<br id="q3vo" /> &laquo;&nbsp;Using index&nbsp;&raquo; indique que nous sommes ici dans le cas d&#8217;un &laquo;&nbsp;<strong>covering index</strong>&laquo;&nbsp;, c&#8217;est à dire qu&#8217;il est inutile pour MySQL d&#8217;aller lire les enregistrements puisque toute l&#8217;information recherchée (le nom de l&#8217;acteur) est présente dans l&#8217;index. Dans le cas d&#8217;une table MyISAM cela signifie que seul le fichier .MYI (index) est lu, inutile d&#8217;aller lire le .MYD (data).<br id="c5lr" /><br id="mabh" />Jetons un oeil aux index présents sur cette table afin d&#8217;en savoir un peu plus sur l&#8217;index utilisé  (idx_actor_last_name) :<br id="mabh0" /><code><br id="mabh1" />mysql&gt; SHOW INDEX FROM ACTOR\G<br id="kb3v" />*************************** 1. row <br id="kb3v0" /> Table: ACTOR<br id="kb3v1" /> Non_unique: 0<br id="kb3v2" /> Key_name: PRIMARY<br id="kb3v3" />Seq_in_index: 1<br id="kb3v4" /> Column_name: actor_id<br id="kb3v5" /> Collation: A<br id="kb3v6" /> Cardinality: 200<br id="kb3v7" /> Sub_part: NULL<br id="kb3v8" /> Packed: NULL<br id="kb3v9" /> Null:<br id="kb3v10" /> Index_type: BTREE<br id="kb3v11" /> Comment:<br id="kb3v12" />*************************** 2. row <br id="kb3v13" /> Table: ACTOR<br id="kb3v14" /> Non_unique: 1<br id="kb3v15" /> Key_name: <strong>idx_actor_last_name</strong><br id="kb3v16" />Seq_in_index: 1<br id="kb3v17" /> Column_name: last_name<br id="kb3v18" /> Collation: A<br id="kb3v19" /> <strong>Cardinality: 200</strong><br id="kb3v20" /> Sub_part: NULL<br id="kb3v21" /> Packed: NULL<br id="kb3v22" /> Null:<br id="kb3v23" /> Index_type: BTREE<br id="kb3v24" /> Comment:<br id="kb3v25" />2 rows in set (0.02 sec)</code><br id="kb3v26" /><br id="kb3v27" />A priori notre index a une cardinalité de 200&#8230; C&#8217;est à dire que d&#8217;après MySQL, celui-ci contient <strong id="xr.4">approximativement </strong>(<a href="http://dev.mysql.com/doc/refman/5.1/en/show-index.html" target="_blank">dixit la doc</a>) 200 valeurs uniques. Une estimation à prendre en effet avec des pincettes puisque le champ indexé n&#8217;en contient lui-même que 121 :<br id="kb3v29" /><code><br id="kb3v30" />mysql&gt; SELECT COUNT(DISTINCT(last_name)) FROM actor;<br id="xr.40" />+----------------------------+<br id="xr.41" />| COUNT(DISTINCT(last_name)) |<br id="xr.42" />+----------------------------+<br id="xr.43" />|                        121 |<br id="xr.44" />+----------------------------+<br id="xr.45" />1 row in set (0.06 sec)</code><br id="c5lr0" /><br id="c5lr1" />Calculons plutôt la sélectivité de l&#8217;index à partir des chiffres suivants :<br id="xr.46" />S = 121 / 200 (nb d&#8217;enregistrements dans la table)<br id="xr.47" />S = 0,605<br id="eie:" /><br id="eie:0" />C&#8217;est moins bon qu&#8217;une sélectivité de 1 bien sûr, mais cela dit cet index permet de mettre la main relativement rapidement sur le nom recherché. La <strong id="bfq3">distributivité </strong>des données est en effet plutôt &laquo;&nbsp;homogène&nbsp;&raquo;. Entendre par là qu&#8217;un seul acteur ne représente pas 95% de la table, ce qui fausserait l&#8217;idée que l&#8217;on se fait sur la sélectivité de l&#8217;index&#8230; En effet celle-ci ne prend pas en compte (en tout cas dans la formule) la distributivité des données.<br id="kji-" /><br id="kji-0" />Concernant les approximations relevées ci-dessus sur les cardinalités de nos index, <a href="http://www.mysqlperformanceblog.com/2008/09/03/analyze-myisam-vs-innodb/" target="_blank">cet article</a> est entièrement tourné vers ce phénomène. A lire !<br id="avdi" /></p>
<p>Voici un aperçu de la distributivité de la colonne &laquo;&nbsp;last_name&nbsp;&raquo; :</p>
<p>mysql&gt; <code>SELECT last_name, COUNT(last_name) as nb<br id="pogu" />FROM actor<br id="pogu0" />GROUP BY last_name<br id="pogu1" />ORDER BY nb<br id="pogu2" />DESC LIMIT 5;<br id="pogu3" /><br id="pogu4" />+-----------+----+<br id="pogu5" />| last_name | nb |<br id="pogu6" />+-----------+----+<br id="pogu7" />| KILMER    |  5 |<br id="pogu8" />| NOLTE     |  4 |<br id="pogu9" />| TEMPLE    |  4 |<br id="pogu10" />| PECK      |  3 |<br id="pogu11" />| ALLEN     |  3 |<br id="pogu12" />+-----------+----+<br id="pogu13" />5 rows in set (0.00 sec)</code><br id="s7j2" /><br id="s7j20" />Le nom d&#8217;acteur le plus représenté parmi les 121 noms de la table &laquo;&nbsp;actor&nbsp;&raquo; apparaît &laquo;&nbsp;seulement&nbsp;&raquo; 5 fois, on peut donc dire qu&#8217;au pire l&#8217;index posé sur &laquo;&nbsp;last_name&nbsp;&raquo; correspond à 5 enregistrements de la table. Par ailleurs, la moitié environ des noms d&#8217;acteur présents dans la table sont uniques.<br id="eh5y" /><br id="eh5y0" />Pour revenir à notre plan d&#8217;exécution, MySQL a donc utilisé pour cette première étape de &laquo;&nbsp;sélection&nbsp;&raquo; un index plutôt sélectif.<br id="ef1v" /><br id="ef1v0" />Etape suivante : la jointure entre la table &laquo;&nbsp;actor&nbsp;&raquo; et &laquo;&nbsp;film_actor&nbsp;&raquo;.</p>
<p>L&#8217;attribut &laquo;&nbsp;ref&nbsp;&raquo; affiché par la commande EXPLAIN indique que la table &laquo;&nbsp;film_actor&nbsp;&raquo; sera parcourue pour chaque valeur correspondante de l&#8217;index &laquo;&nbsp;idx_actor_last_name&nbsp;&raquo;. Cet index n&#8217;étant pas unique, il existe en effet des cas où à une valeur de l&#8217;index, mettons &laquo;&nbsp;KILMER&nbsp;&raquo;, vont correspondre 5 enregistrements dans la table &laquo;&nbsp;film_actor&nbsp;&raquo;. Ce parcours n&#8217;est ici pas pénalisant puisque nous avons encore une fois affaire à un &laquo;&nbsp;covering index&nbsp;&raquo;, les enregistrements de la table ne sont pas accédés puisque tout ce que nous recherchons est déjà dans l&#8217;index, une telle optimisation est la bienvenue !</p>
<p>Nous reparlerons des covering index dans un billet ultérieur, mais vous connaissez maintenant le principe.<br id="odij" /><br id="odij0" />Dernière étape : la jointure avec la table &laquo;&nbsp;film&nbsp;&raquo;. &laquo;&nbsp;eq_ref&nbsp;&raquo; signifie qu&#8217;un seul enregistrement de la table &laquo;&nbsp;film&nbsp;&raquo; est lu pour chaque enregistrement correspondant de la table &laquo;&nbsp;film_actor&nbsp;&raquo;. Rien d&#8217;étonnant à cela puisque nous passons pour la table &laquo;&nbsp;film&nbsp;&raquo; par une clé primaire qui fait donc la jointure avec la clé primaire de &laquo;&nbsp;film_actor&nbsp;&raquo;, on tombe donc directement sur un seul film identifié par sa clé primaire.<br id="myil" /><br id="myil0" /><strong>Bilan de cet EXPLAIN ?</strong></p>
<p>L&#8217;estimation du nombres d&#8217;enregistrements à parcourir pour aboutir au résultat d&#8217;après l&#8217;optimiseur est de : 1 x 13 x 1 = 13 enregistrements. En parcourant rapidement cet EXPLAIN comme nous venons de le faire, on constate que 13 enregistrements environ sont à parcourir via deux covering index et une clé primaire : la requête a l&#8217;air convenable.</p>
<p><strong>Les &laquo;&nbsp;Index hints&nbsp;&raquo; ou comment &laquo;&nbsp;orienter&nbsp;&raquo; les choix de l&#8217;optimiseur</strong></p>
<p>Quel plan d&#8217;exécution l&#8217;optimiseur aurait-il choisi si nous n&#8217;avions pas eu d&#8217;index ? Quelles conséquences sur le nombre estimé d&#8217;enregistrements à parcourir ?</p>
<p>Pour le savoir, les barbares suppriment leurs index à grands coups de DROP INDEX ou d&#8217;ALTER TABLE plus ou moins heureux, les autres peuvent utiliser ce qu&#8217;on appelle les &laquo;&nbsp;<a href="http://dev.mysql.com/doc/refman/5.1/en/index-hints.html" target="_blank">index hints</a>&nbsp;&raquo; et &laquo;&nbsp;suggérer&nbsp;&raquo; à l&#8217;optimiseur (pour ne pas dire forcer) d&#8217;effectuer certaines actions et pas d&#8217;autres.</p>
<p>Exemple : forcer l&#8217;utilisation d&#8217;un index ou au contraire l&#8217;ignorer ou encore joindre deux tables dans un ordre particulier&#8230;<br id="zjya3" /><br id="zjya4" />Pour forcer le trait (&laquo;&nbsp;cas d&#8217;école&nbsp;&raquo; rappelez-vous), on supprime ici toute possibilité pour l&#8217;optimiseur d&#8217;utiliser un index &laquo;&nbsp;intéressant&nbsp;&raquo; pour lui :</p>
<p><code>mysql&gt; EXPLAIN SELECT f.title</code><br />
FROM film f <strong>IGNORE INDEX</strong> (PRIMARY, idx_title),<br />
actor a <strong>IGNORE INDEX</strong> (PRIMARY, idx_actor_last_name), film_actor<br id="qk4r9" /> fa <strong>IGNORE INDEX</strong> (PRIMARY, idx_fk_film_id)<br id="d8i40" />WHERE a.actor_id = fa.actor_id<br id="d8i41" />AND fa.film_id = f.film_id<br id="d8i42" />AND a.last_name = &#8216;CRUZ&#8217;\G<br id="d8i43" /><br id="qk4r10" />*************************** 1. row <br id="ylwo" /> id: 1<br id="ylwo0" /> select_type: SIMPLE<br id="ylwo1" /> table: a<br id="ylwo2" /> type: <strong>ALL</strong><br id="ylwo3" />possible_keys: NULL<br id="ylwo4" /> key: NULL<br id="ylwo5" /> key_len: NULL<br id="ylwo6" /> ref: NULL<br id="ylwo7" /> rows: <strong>200</strong><br id="ylwo8" /> Extra: Using where<br id="ylwo9" />*************************** 2. row <br id="ylwo10" /> id: 1<br id="ylwo11" /> select_type: SIMPLE<br id="ylwo12" /> table: fa<br id="ylwo13" /> type: <strong>ALL</strong><br id="ylwo14" />possible_keys: NULL<br id="ylwo15" /> key: NULL<br id="ylwo16" /> key_len: NULL<br id="ylwo17" /> ref: NULL<br id="ylwo18" /> rows: <strong>5920</strong><br id="ylwo19" /> Extra: Using where; Using join buffer<br id="ylwo20" />*************************** 3. row <br id="ylwo21" /> id: 1<br id="ylwo22" /> select_type: SIMPLE<br id="ylwo23" /> table: f<br id="ylwo24" /> type: <strong>ALL</strong><br id="ylwo25" />possible_keys: NULL<br id="ylwo26" /> key: NULL<br id="ylwo27" /> key_len: NULL<br id="ylwo28" /> ref: NULL<br id="ylwo29" /> rows: <strong>952</strong><br id="ylwo30" /> Extra: Using where; Using join buffer<br id="ylwo31" />3 rows in set (0.00 sec)<br id="ylwo32" /><br id="ylwo33" />Les résultats sont identiques en 5.1.26 et en 5.0.67, seule la mention du &laquo;&nbsp;<a href="http://dev.mysql.com/doc/refman/5.1/en/using-explain.html" target="_blank">Using join buffer</a>&nbsp;&raquo; fait son apparition en 5.1<br id="a8c7" />Le nombre approximatif d&#8217;enregistrements à parcourir est ici de : 200 x 5920 x 952 = 1 127 168 000, un résultat logique puisque l&#8217;intégralité des trois tables sont parcourues&#8230;</p>
<p>Bien que &laquo;&nbsp;dramatique&nbsp;&raquo; ce plan d&#8217;exécution a ici des conséquences très limitées puisque d&#8217;aussi petites tables sont aisément logées en mémoire. Les index sont stockés dans le key_buffer_size et le cache de l&#8217;OS s&#8217;occupe des données puisque nous sommes en MyISAM (à comparer avec l&#8217;innodb_buffer_pool qui stocke index ET données).<br id="aou." /><br id="zjya6" />Résumons ce &laquo;&nbsp;cas d&#8217;école&nbsp;&raquo; : l&#8217;optimiseur a effectivement choisi des clés primaires (index le plus sélectif possible) ainsi qu&#8217;un index efficace, tout est logique.<br id="z:.a" /><br id="z:.a1" />Voyons maintenant un cas&#8230; &laquo;&nbsp;particulier&nbsp;&raquo; qui va nous apprendre à garder un oeil critique sur les plans d&#8217;exécution proposés par MySQL.<br id="c5lr2" /><br id="ys6o4" /><strong id="jbka">La légende des 30 %</strong><br id="ys6o5" /><br id="ys6o6" />Il était une fois un optimiseur MySQL qui décidait de ne jamais utiliser un index si celui-ci osait sélectionner plus de 30% des enregistrements d&#8217;une table. Pour schématiser, les développeurs de l&#8217;optimiseur considèreraient d&#8217;après leur expérience qu&#8217;en effet une fois cette limite dépassée, un parcours séquentiel de la table serait plus rapide que davantage d&#8217;accès aléatoires aux données (cas d&#8217;un index MyISAM par exemple). La réalité est évidemment beaucoup plus complexe et bien que le code source ne contienne pas en &laquo;&nbsp;dur&nbsp;&raquo; un tel seuil, cette valeur de 30% circule ici et là sur le net, davantage pour illustrer l&#8217;idée de sélectivité que pour réellement indiquer qu&#8217;il existe une telle valeur au sein de l&#8217;optimiseur qui guiderait ses choix si grossièrement.<br id="saf1" /><br id="saf10" />L&#8217;idée à retenir est qu&#8217;<strong>un index sélectif est un bon candidat pour l&#8217;optimiseur</strong> et a donc toutes ses chances d&#8217;être retenu dans le plan d&#8217;exécution final.</p>
<p>Il existe cependant quelques <strong>exceptions</strong>, et il est parfois difficile de comprendre les choix de l&#8217;optimiseur&#8230;<br id="o:mx0" /><br id="o:mx1" />Nous reprenons notre table de test conçue dans le billet précédent.<br id="tvq:" />Rappel : 1 million d&#8217;enregistrements, 3 colonnes : une clé primaire, un TIMESTAMP et un champ &laquo;&nbsp;flag&nbsp;&raquo; (0 ou 1) équitablement réparti (environ 500 000 &laquo;&nbsp;0&#8243; pour autant de &laquo;&nbsp;1&#8243;).<br id="tvq:0" /></p>
<p>L&#8217;index &laquo;&nbsp;flag&nbsp;&raquo; ne possède que 2 entrées à comparer avec le million de rangées de la table t. Autrement dit, à un index correspond en moyenne 500 000 rangées possibles, bref absolument rien de sélectif, c&#8217;est tout le contraire.<br id="cdc6" /><br id="cdc60" />Quel accueil l&#8217;optimiseur MySQL va t&#8217;il réserver à notre index &laquo;&nbsp;flag&nbsp;&raquo; sur une requête du type SELECT count(date) from t WHERE flag = 1 ?<br id="c6-s" /><br id="vyin" />Les tests effectués ci-dessous sont valables pour les versions 5.0.67 et la 5.1.26.<br id="gslg" />Rappel de la structure de la table t et de ses index :<br id="vyin0" /><code><br id="vyin1" />mysql&gt; show create table t;<br id="xxs3" />CREATE TABLE `t` (<br id="xxs30" /> `id` mediumint(8) unsigned NOT NULL auto_increment,<br id="xxs31" /> `date` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,<br id="xxs32" /> `flag` tinyint(4) NOT NULL default '0',<br id="xxs33" /> PRIMARY KEY  (`id`),<br id="xxs34" /> KEY `flag` (`flag`)<br id="xxs35" />) ENGINE=MyISAM AUTO_INCREMENT=1000001 DEFAULT CHARSET=latin1<br id="vsfb" /></code><br id="vsfb0" />Concernant les index nous avons une clé primaire &laquo;&nbsp;id&nbsp;&raquo; et &laquo;&nbsp;flag&nbsp;&raquo;, de type &laquo;&nbsp;index&nbsp;&raquo; :<br id="vyin2" /><code><br id="c6-s0" />mysql&gt; show index from t\G<br id="ei5l4" />*************************** 1. row <br id="ei5l5" /> Table: t<br id="ei5l6" /> Non_unique: 0<br id="ei5l7" /> Key_name: PRIMARY<br id="ei5l8" />Seq_in_index: 1<br id="ei5l9" /> Column_name: id<br id="ei5l10" /> Collation: A<br id="ei5l11" /> <strong id="g69y">Cardinality: 1000000</strong><br id="ei5l12" /> Sub_part: NULL<br id="ei5l13" /> Packed: NULL<br id="ei5l14" /> Null:<br id="ei5l15" /> Index_type: BTREE<br id="ei5l16" /> Comment:<br id="ei5l17" />*************************** 2. row <br id="ei5l18" /> Table: t<br id="ei5l19" /> Non_unique: 1<br id="ei5l20" /> Key_name: flag<br id="ei5l21" />Seq_in_index: 1<br id="ei5l22" /> Column_name: flag<br id="ei5l23" /> Collation: A<br id="ei5l24" /> <strong id="g69y0">Cardinality: 2</strong><br id="ei5l25" /> Sub_part: NULL<br id="ei5l26" /> Packed: NULL<br id="ei5l27" /> Null:<br id="ei5l28" /> Index_type: BTREE<br id="ei5l29" /> Comment:<br id="ei5l30" />2 rows in set (0.01 sec)</code></p>
<p><code>Pour un rappel sur les différents types d'index disponibles, <a href="http://www.dbnewz.com/2008/06/27/les-index-mysql-types-placements-efficacite/" target="_blank">ce billet précédent</a> est tout indiqué.<br id="ubkd" /></code><br id="j60q" />Appliquons notre requête à notre jeu d&#8217;essai :<br id="pn87" /><code><br id="pn870" />mysql&gt; SELECT SQL_NO_CACHE COUNT(date)</code><br />
<code>FROM t WHERE flag = 1;<br id="kbd:" />+-------------+<br id="kbd:0" />| count(date) |<br id="kbd:1" />+-------------+<br id="kbd:2" />|      499959 |<br id="kbd:3" />+-------------+<br id="kbd:4" />1 row in set (1.70 sec)<br id="kbd:5" /></code><br id="gg01" />Un score sensiblement identique est obtenu pour l&#8217;autre valeur de &laquo;&nbsp;flag&nbsp;&raquo; :<br id="gg010" /><code><br id="gg011" />mysql&gt; SELECT SQL_NO_CACHE COUNT(date)</code><br />
<code>FROM t WHERE flag = 0;<br id="gg012" />+-------------+<br id="gg013" />| count(date) |<br id="gg014" />+-------------+<br id="gg015" />|      500041 |<br id="gg016" />+-------------+<br id="gg017" />1 row in set (1.69 sec)<br id="gg018" /></code><br id="gg019" />Voyons maintenant ce qui se passe avec EXPLAIN :<br id="q29j" /><code><br id="qf:l" />mysql&gt; EXPLAIN SELECT SQL_NO_CACHE COUNT(date)</code><br />
<code>FROM t WHERE flag = 1\G<br id="qf:l0" />*************************** 1. row <br id="qf:l1" /> id: 1<br id="qf:l2" /> select_type: SIMPLE<br id="qf:l3" /> table: t<br id="qf:l4" /> type: ref<br id="qf:l5" />possible_keys: flag<br id="qf:l6" /> key: flag<br id="qf:l7" /> key_len: 1<br id="qf:l8" /> ref: const<br id="qf:l9" /> rows: 512463<br id="qf:l10" /> Extra:<br id="qf:l11" />1 row in set (0.00 sec)<br id="qf:l12" /></code><br id="qf:l13" />Surprise ! MySQL utilise notre index alors que la requête retourne environ la moitié de nos enregistrements&#8230; Nous sommes donc loin des &laquo;&nbsp;30%&nbsp;&raquo;, et pourtant les index ont été préférés au parcours complet de la table. On note ici le type &laquo;&nbsp;ref&nbsp;&raquo; qui signifie que chaque ligne de la table t correspondant à la valeur de &laquo;&nbsp;flag&nbsp;&raquo; est lue, c&#8217;est à dire environ 500 000 random I/O.<br id="t3d9" /><br id="t3d90" />Retenons que sans la clause EXPLAIN, cette requête s&#8217;exécute en 1.7 secondes quel que soit la valeur de flag.<br id="a2o0" /><br id="kufg0" /><strong id="ztp1">Un index de trop ?</strong><br id="vj1j" /><br id="vj1j0" />Voyons ce que cette même requête donnerait sans l&#8217;index appliqué au champ &laquo;&nbsp;flag&nbsp;&raquo; :<br id="vj1j1" /><code><br id="vj1j2" />mysql&gt; EXPLAIN SELECT SQL_NO_CACHE count(date)</code><br />
FROM t IGNORE INDEX(flag)<br />
WHERE flag = 1\G<br id="j9hm" />*************************** 1. row <br id="j9hm0" /> id: 1<br id="j9hm1" /> select_type: SIMPLE<br id="j9hm2" /> table: t<br id="j9hm3" /> type: ALL<br id="j9hm4" />possible_keys: NULL<br id="j9hm5" /> key: NULL<br id="j9hm6" /> key_len: NULL<br id="j9hm7" /> ref: NULL<br id="j9hm8" /> rows: 1000000<br id="j9hm9" /> Extra: Using where<br id="j9hm10" />1 row in set (0.00 sec)<br id="kufg1" /><br id="kufg2" />Changement de décor, MySQL ne peut plus utiliser l&#8217;index &laquo;&nbsp;flag&nbsp;&raquo; et effectue <strong>un scan complet de la table</strong>, un million de lignes sont parcourues (deux fois plus qu&#8217;avec l&#8217;utilisation de l&#8217;index &laquo;&nbsp;flag&nbsp;&raquo;) mais séquentiellement cette fois.<br id="j9hm11" /><br id="j9hm12" />Quel est le temps d&#8217;exécution de ce scan complet ?<br id="yzg0" /><code><br id="yzg00" />mysql&gt; SELECT SQL_NO_CACHE COUNT(date)</code><br />
<code>FROM t IGNORE INDEX(flag) WHERE flag = 1\G<br id="c4.0" />*************************** 1. row <br id="c4.00" />count(date): 499959<br id="c4.01" />1 row in set (0.23 sec)<br id="c4.02" /></code><br id="c4.03" />23 centièmes de seconde seulement, le scan complet de la table est donc ici 7 fois plus rapide que l&#8217;accès aux données par les index.<br id="a.87" /><br id="a.870" />Pourquoi l&#8217;optimiseur a t&#8217;il décidé de partir sur un plan d&#8217;exécution basé sur l&#8217;utilisation de l&#8217;index &laquo;&nbsp;flag&nbsp;&raquo; si peu sélectif ? Peut-être considère t-il que 50% des données retournées est un seuil trop bas pour &laquo;&nbsp;désactiver&nbsp;&raquo; les index par rapport au scan complet de la table ?<br id="li43" /><br id="qzrw" />Modifions maintenant la distributivité du champ flag. Nous avions quasiment autant de 0 que de 1 dans la colonne &laquo;&nbsp;flag&nbsp;&raquo;, voyons ce que donnerait 95% de &laquo;&nbsp;1&#8243; et 5% de &laquo;&nbsp;0&#8243;.<br id="sy6v" /><br id="sy6v0" />Pour cela on modifie la procédure stockée utilisée précedemment pour le remplissage de la table &laquo;&nbsp;t&nbsp;&raquo; et en deux étapes on charge d&#8217;abord les 950 000 lignes de &laquo;&nbsp;1&#8243; puis les 50 000 lignes de &laquo;&nbsp;0&#8243;, reste alors à mélanger le tout :<br id="qzrw0" /><code><br id="qzrw1" />mysql&gt; CREATE TABLE t2 LIKE t;<br id="y5b70" />Query OK, 0 rows affected (0.03 sec)<br id="y5b71" /><br id="y5b716" />mysql&gt; INSERT INTO t2 SELECT * FROM t ORDER BY RAND();<br id="y5b717" />Query OK, 1000000 rows affected (12.59 sec)<br id="y5b718" />Records: 1000000  Duplicates: 0  Warnings: 0<br id="akm6" /></code><br id="akm60" />t2 est désormais équivalente à la table t à ceci près qu&#8217;elle contient 95% de &laquo;&nbsp;0&#8243;, le reste en &laquo;&nbsp;1&#8243;, le tout aléatoirement réparti.<br id="p21m" /><code><br id="p21m0" />mysql&gt; EXPLAIN SELECT COUNT(date)</code><br />
<code>FROM t2 WHERE flag = 1\G<br id="p21m1" />*************************** 1. row <br id="p21m2" /> id: 1<br id="p21m3" /> select_type: SIMPLE<br id="p21m4" /> table: t2<br id="p21m5" /> type: ref<br id="p21m6" />possible_keys: flag<br id="p21m7" /> key: flag<br id="p21m8" /> key_len: 1<br id="p21m9" /> ref: const<br id="p21m10" /> rows: 950277<br id="p21m11" /> Extra:<br id="p21m12" />1 row in set (0.01 sec)<br id="jveh" /></code><br id="jveh0" />Même résultat que précédemment, quelque soit la valeur de flag, &laquo;&nbsp;1&#8243; ou &laquo;&nbsp;0&#8243; : MySQL passe toujours par l&#8217;index s&#8217;il est disponible :<br id="p21m13" /><code><br id="y5b720" />mysql&gt; select COUNT(date) </code><code>FROM </code><code>t2</code><br />
<code>WHERE flag = 0;<br id="ca:z" />+-------------+<br id="ca:z0" />| count(date) |<br id="ca:z1" />+-------------+<br id="ca:z2" />|       50000 |<br id="ca:z3" />+-------------+<br id="ca:z4" />1 row in set (0.23 sec)<br id="xn93" /><br id="xn930" />mysql&gt; select </code><code>COUNT</code><code>(date) </code><code>FROM t2 IGNORE INDEX(flag) WHERE </code><code>flag = 0;<br id="h6.l1" /> +-------------+<br id="h6.l2" /> | count(date) |<br id="h6.l3" /> +-------------+<br id="h6.l4" /> |       50000 |<br id="h6.l5" /> +-------------+<br id="h6.l6" /> 1 row in set (0.17 sec)<br id="ca:z5" /><br id="ca:z6" />mysql&gt; select </code><code>COUNT</code><code>(date) </code><code>FROM </code><code>t2</code><br />
<code>WHERE </code><code>flag = 1;<br id="d_8g" />+-------------+<br id="d_8g0" />| count(date) |<br id="d_8g1" />+-------------+<br id="d_8g2" />|      950000 |<br id="d_8g3" />+-------------+<br id="d_8g4" />1 row in set (3.17 sec)<br id="h6.l" /><br id="hw030" />mysql&gt; select </code><code>COUNT</code><code>(date) FROM t2 IGNORE INDEX(flag)</code><br />
<code>WHERE </code><code>flag = 1;<br id="hl1w1" />+-------------+<br id="hl1w2" />| count(date) |<br id="hl1w3" />+-------------+<br id="hl1w4" />|      950000 |<br id="hl1w5" />+-------------+<br id="hl1w6" />1 row in set (0.20 sec)<br id="hw031" /></code><br id="hw032" />&#8230; On constate que les résultats sont quasiment équivalents pour le cas où flag = 0, c&#8217;est effectivement la valeur pour laquelle l&#8217;index est le plus sélectif, en revanche l&#8217;écart se creuse encore davantage entre le scan complet de la table et l&#8217;accès aux données via l&#8217;index : le parcours complet de la table est 16 fois plus rapide.<br id="dqrm" /><br id="wbt." />La sélectivité de notre index est la même dans la table t2 que dans la table t1, seule la distributivité des données de la colonne flag a été modifiée pour en faire un index vraiment peu sélectif dans le cas où &laquo;&nbsp;flag&nbsp;&raquo; = 1 puisque dans cette configuration ce sont 95% des données de la table qui sont sélectionnées, la légende prend un coup de vieux&#8230;</p>
<p><strong>Conclusion</strong><br id="ge:3" /><br id="t9bc" /> Il est parfois difficile de comprendre les choix de l&#8217;optimiseur, il existe des règles générales et des exceptions troublantes. L&#8217;optimiseur est un logiciel <strong>complexe</strong>, très efficace la plupart du temps mais pas parfait. <a href="http://dev.mysql.com/tech-resources/articles/mysql_5.0_psea1.jpg" target="_blank">L&#8217;architecture</a> même de MySQL, basé sur des moteurs &laquo;&nbsp;externes&nbsp;&raquo; (&laquo;&nbsp;pluggable storage engine&nbsp;&raquo;), complique sa tâche : il lui est difficile, selon les cas, de déterminer où seront stockées les données. Cette information est pourtant cruciale pour prendre des décisions lourdes de conséquences. De plus chaque moteur gère les index à sa façon&#8230; <br id="opzq" /> Sommes-nous en présence de MyISAM (index en mémoire, données cachées par l&#8217;OS), sur du InnoDB ? (index clustered, données triées selon la clé primaire &laquo;&nbsp;accolées&nbsp;&raquo; à l&#8217;index), ou bien encore sur du MEMORY (les lectures aléatoires ou random I/O sont beaucoup moins coûteuses en mémoire que sur disque), les différents système de cache sont-ils activés ? Les données sont-elles toutes en mémoire ou en partie sur disque ? On peut également parier sur le fait que <a href="http://www.dbnewz.com/2008/05/13/les-ssd-solid-state-drive-une-technologie-davenir-pour-nos-sgbd/" target="_blank">l&#8217;apparition des disques SSD</a> devrait par exemple impacter les choix de l&#8217;optimiseur à l&#8217;avenir&#8230;<br id="c8g2" /> <br id="zgjw" /> Bref, à moins d&#8217;avoir écrit soi-même le code source de l&#8217;optimiseur, il est difficile de tout maîtriser de A à Z en ce qui le concerne, <strong>le conseil est alors le suivant</strong> : il faut appliquer régulièrement <a href="http://dev.mysql.com/doc/refman/5.1/en/using-explain.html" target="_blank">EXPLAIN </a>sur les requêtes importantes de vos applications. <strong>Savoir lire un plan d&#8217;exécution</strong> et ainsi comprendre ce que MySQL a decidé pour votre requête permet de détecter avec un peu d&#8217;expérience si le plan d&#8217;exécution est efficace.<br id="a-0l" /> Utilisez le cas échéant les &laquo;&nbsp;index hints&nbsp;&raquo; pour modifier si besoin le parcours proposé : <strong>IGNORE INDEX, FORCE INDEX, ou encore STRAIGHT JOIN</strong> si vous souhaitez modifier l&#8217;ordre de jointures de vos tables&#8230;<br id="yjwv" /> <br id="yjwv0" /> Sans se focaliser sur les fameux 30%, un index sélectif est un bon candidat. Nous avons rencontré un cas où un parcours total de la table aurait été plus efficace, cependant c&#8217;est parfois l&#8217;inverse qui se produit, un index même peu sélectif peut s&#8217;avérer payant, il faut donc <strong>tester </strong>si le plan d&#8217;exécution proposé vous semble perfectible.</p>
<p>Derniers détails d&#8217;importance : <strong>on ne peut pas comparer uniquement deux plans d&#8217;exécution uniquement sur l&#8217;estimation du nombre d&#8217;enregistrements à parcourir</strong>&#8230; La preuve dans notre exemple, le parcours total et séquentiel de la table (1 million d&#8217;enregistrement) s&#8217;avère plus rapide que les 500 000 accès aléatoires issus de l&#8217;index. On retiendra également de notre exemple qu&#8217;un index n&#8217;est pas systématiquement synonyme de meilleures performances&#8230; (Même si c&#8217;est le cas en général).<br id="k9no" /> <br id="k9no0" /> L&#8217;exemple étudié ici est un cas particulier au sens où c&#8217;est plutôt le parcours complet de la table (full scan) que l&#8217;on cherche d&#8217;habitude à éviter. Sachez qu&#8217;il existe une variable serveur, <a href="http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#option_mysqld_max_seeks_for_key" target="_blank">max_seeks_for_key</a> (on en parle aussi <a href="http://dev.mysql.com/doc/refman/5.1/en/how-to-avoid-table-scan.html" target="_blank">ici </a>et <a href="http://rackerhacker.com/2007/08/03/obscure-mysql-variable-explained-max_seeks_for_key/" target="_blank">là</a>) sur laquelle il est possible de jouer pour orienter les choix de l&#8217;optimiseur. <br id="g7sp" /></p>
<p>Enfin, puisqu&#8217;il est difficile d&#8217;aborder toutes les problématiques possibles autour de la sélectivité d&#8217;un index dans un seul billet, voici quelques autres articles sur la même thématique afin d&#8217;aller encore plus loin :<br id="qmk5" /></p>
<ul>
<li> <a href="http://www.mysqlperformanceblog.com/2006/06/02/indexes-in-mysql/" target="_blank">L&#8217;article</a> qui m&#8217;a incité à effectuer mes propres tests.</li>
</ul>
<ul>
<li> <a href="http://www.mysqlperformanceblog.com/2008/04/28/the-mysql-optimizer-the-os-cache-and-sequential-versus-random-io/" target="_blank">Une excellente illustration</a> des difficultés parfois rencontrées par l&#8217;optimiseur, un coup de pouce du DBA permet alors de réduire la requête de 2.5 jours à 1h !</li>
<li>Un court article de <a href="http://mysqldatabaseadministration.blogspot.com/2006/07/indexes-low-index-selectivity-and.html" target="_blank">Farhan Mashraqi</a> sur la sélectivité.</li>
<li>Arjen&#8217;s journal <a href="http://arjen-lentz.livejournal.com/122399.html" target="_blank">sur l&#8217;utilité d&#8217;un index</a></li>
</ul>
<ul>
<li>Une belle formule issue du manuel MySQL : <a href="http://dev.mysql.com/doc/refman/5.1/en/estimating-performance.html" target="_blank">Comment estimer l&#8217;efficacité d&#8217;une requête ?</a></li>
</ul>
<p>N&#8217;oubliez pas de lire également les commentaires associés à ces articles&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/09/05/cardinalite-selectivite-et-distributivite-dun-index-mysql-quel-impact-sur-le-plan-dexecution/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Les index MySQL : types, placements, efficacité</title>
		<link>http://www.dbnewz.com/2008/06/27/les-index-mysql-types-placements-efficacite/</link>
		<comments>http://www.dbnewz.com/2008/06/27/les-index-mysql-types-placements-efficacite/#comments</comments>
		<pubDate>Fri, 27 Jun 2008 06:52:44 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[4.0]]></category>
		<category><![CDATA[4.1]]></category>
		<category><![CDATA[5.0]]></category>
		<category><![CDATA[5.1]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=48</guid>
		<description><![CDATA[Déjà trois semaines d&#8217;écoulées depuis que certains d&#8217;entre vous, les &#171;&#160;héros&#160;&#187;, ont posé leurs questions (oui il est possible de devenir un héros rien qu&#8217;en lisant dbnewz ! Les véritables héros sont d&#8217;ailleurs abonnés au tout nouveau flux feedburner   )
Trois semaines d&#8217;attente, cela mérite un billet digne de ce nom, c&#8217;est parti.
Indexer, pourquoi [...]]]></description>
			<content:encoded><![CDATA[<p>Déjà trois semaines d&#8217;écoulées depuis que certains d&#8217;entre vous, les &laquo;&nbsp;héros&nbsp;&raquo;, ont posé leurs questions (oui il est possible de <a href="http://www.dbnewz.com/2008/06/05/les-index-mysql-la-serie-dont-vous-etes-le-heros/" target="_blank">devenir un héros</a> rien qu&#8217;en lisant dbnewz ! Les véritables héros sont d&#8217;ailleurs abonnés au tout nouveau flux <a href="http://feeds.feedburner.com/Dbnewz" target="_blank">feedburner</a> <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' />  )</p>
<p>Trois semaines d&#8217;attente, cela mérite un billet digne de ce nom, c&#8217;est parti.</p>
<p><strong>Indexer, pourquoi ?</strong><br id="dz-w" /><br id="yo1g" />L&#8217;indexation peut avoir plusieurs buts : <br id="gpy7" />- Accéder à ses données plus rapidement, les index sont en effet l&#8217;outil le plus puissant pour <strong>accélérer les temps d&#8217;exécution de vos requêtes</strong> jusqu&#8217;à parfois plusieurs centaines de % !<br id="gpy70" />- Définir le degré d&#8217;unicité d&#8217;une colonne donnée : chaque champ doit-il être unique ? les doublons sont-ils autorisés ?<br id="e0_r" /><br id="uby5" /><strong>Principe de fonctionnement</strong><br id="kvk1" /><br id="kvk10" />Lorsque vous envoyez une requête à votre serveur MySQL, celle-ci est d&#8217;abord confiée au &laquo;&nbsp;parseur&nbsp;&raquo; SQL qui a pour but de vérifier si la syntaxe de votre demande est correcte. Cette étape franchie, la requête passe par &laquo;&nbsp;l&#8217;optimiseur&nbsp;&raquo;. Il s&#8217;agit ici de déterminer le <strong>plan d&#8217;exécution</strong> de la requête afin que celle-ci s&#8217;exécute le plus rapidement possible.</p>
<p>L&#8217;optimiseur détecte si d&#8217;éventuels index sont disponibles, si c&#8217;est le cas il décidera de s&#8217;en servir&#8230; ou pas : il est parfois plus rapide de ne pas se servir d&#8217;un index <strong>!</strong> Nous verrons pourquoi au cours de cette série d&#8217;articles.</p>
<p>Une fois le plan d&#8217;exécution achevé, c&#8217;est le moteur de stockage qui prend le relais, celui-ci peut être vu comme un &laquo;&nbsp;module&nbsp;&raquo; de MySQL :</p>
<p><a href="http://dev.mysql.com/tech-resources/articles/mysql_5.0_psea1.html" target="_blank"><img class="alignnone" src="http://dev.mysql.com/tech-resources/articles/mysql_5.0_psea1.jpg" alt="Les moteurs de stockage MySQL sont des \" width="362" height="261" /></a></p>
<p><span id="more-48"></span>Pour schématiser, et dans un monde idéal, lorsqu&#8217;un index est disponible et &laquo;&nbsp;compatible&nbsp;&raquo; avec votre requête, l&#8217;optimiseur MySQL peut décider de l&#8217;utiliser afin d&#8217;éviter de parcourir l&#8217;ensemble des données des tables concernées.</p>
<p>Un exemple couramment employé pour illustrer ce propos consiste à imaginer la difficultée que nous aurions à retrouver quelqu&#8217;un dans l&#8217;annuaire si nous connaissions son nom mais pas l&#8217;alphabet (qui est notre index)&#8230; Transposé au monde informatique cela donne un serveur MySQL qui compare une à une les entrées du botin pour trouver toutes celles qui correspondent au nom recherché. Si au contraire ce nom est indexé, et si celui-ci commence par exemple par &#8216;T&#8217;, le serveur sait directement qu&#8217;il doit démarrer sa recherche à partir du &#8216;T&#8217;. Imaginez l&#8217;impact en terme de gain de temps lorsque plusieurs jointures sont concernées : deux tables de 10 000 lignes chacune forment un produit cartésien de 100 000 000 lignes environ à étudier&#8230;<br id="mxtu" /><br id="mxtu0" />Les index n&#8217;ont hélas pas que des avantages :<br id="mxtu4" />- Les opérations de mises à jour (INSERT, UPDATE, DELETE) sont en effet ralenties puisqu&#8217;en plus des données, les index doivent eux aussi être mis à jour lors de ces opérations, c&#8217;est le prix à payer&#8230; Ce prix peut néamoins se &laquo;&nbsp;négocier&nbsp;&raquo;, nous verrons cela plus tard.<br id="q.-8" /><br id="q.-80" /><strong>Notre terrain de jeu, la base &laquo;&nbsp;world&nbsp;&raquo;</strong><br id="w4ef" /></p>
<p>MySQL propose au téléchargement plusieurs bases d&#8217;exemple dont <a href="http://dev.mysql.com/doc/" target="_blank">&laquo;&nbsp;world&nbsp;&raquo; et &laquo;&nbsp;sakila&nbsp;&raquo;</a>. Elles épargnent le soin aux utilisateurs de MySQL souhaitant tester quelques requêtes de se constituer eux-mêmes une base de test, celles-ci sont prêtes à l&#8217;emploi.</p>
<p>Nous utiliserons pour nos tests la base &laquo;&nbsp;world&nbsp;&raquo;. Très simple puisque constituée uniquement de trois petites tables MyISAM (Country, CountryLanguage et City), elle permet de se concentrer uniquement sur les index sans perdre de temps à assimiler un schéma plus complexe.<br id="m4x6" /><br id="m4x60" />Si vous souhaitez transformer le script de création SQL de la base &laquo;&nbsp;world&nbsp;&raquo; en une version graphique &laquo;&nbsp;presque&nbsp;&raquo; MCD (les relations entre les tables ne sont pas générées automatiquement pour cette base), le billet précédent est fait pour vous, <a href="http://www.dbnewz.com/2008/06/22/dbdesigner-4-generer-son-mcd-par-reverse-engineering/" target="_blank">les étapes d&#8217;installation et de génération du MCD par &laquo;&nbsp;reverse engineering&nbsp;&raquo; avec DBDesigner 4</a> y sont décrites.<br id="lfl2" /><br id="lfl20" />Voici ce qu&#8217;on peut obtenir pour cette base à partir de DBDesigner :<br id="lfl21" /><br id="lfl22" /><a href="http://www.dbnewz.com/wp-content/uploads/2008/06/world_db_small.png" target="_blank"><img class="alignnone size-thumbnail wp-image-50" title="world_db_small" src="http://www.dbnewz.com/wp-content/uploads/2008/06/world_db_small-150x150.png" alt="" width="150" height="150" /></a><br id="lfl23" /><br id="lfl24" /></p>
<p><strong id="xz5z">Quel type d&#8217;index choisir : PRIMARY KEY, UNIQUE, ou INDEX ?</strong><br id="txto" /><br id="txto0" />Choisissez le type de vos index avec soin :<br id="txto1" />- Une clé primaire (<strong>PRIMARY KEY</strong>) est strictement unique, les NULL ne sont pas autorisés.<br id="e.px" />- Un index de type <strong>UNIQUE </strong>est comparable à une clé primaire, mis à part pour les valeurs NULL puisque celles-ci sont autorisées (et potentiellement en plusieurs occurences).<br id="fy.m" />- Un index de type <strong>INDEX </strong>ou <strong>KEY </strong>(c&#8217;est un alias) signifie simplement que l&#8217;on souhaite indexer une colonne susceptible de contenir des doublons.<br id="blrx" /><br id="blrx0" />Les index de type <strong>FULLTEXT</strong> et <strong>SPATIAL</strong> sont particuliers et méritent un épisode à eux seuls, ils seront donc évoqués ultérieurement.<br id="k7_h" /><br id="k7_h0" />Passons rapidement sur l&#8217;étape de <a href="http://dev.mysql.com/doc/refman/5.0/fr/create-index.html" target="_blank">déclaration d&#8217;un index</a>, celle-ci s&#8217;effectue soit au moment de la création de la table, soit plus tard comme ici :<br id="or3." /><br id="or3.0" />mysql&gt; <code>CREATE INDEX idx_district ON City (District);<br id="n0.-" />Query OK, 4079 rows affected (0.04 sec)<br id="n0.-0" />Records: 4079  Duplicates: 0  Warnings: 0</code><br id="n0.-1" /><br id="n0.-2" />Nous venons de créer un index de type INDEX (autorise les doublons) sur la colonne District de la table City.<br id="n0.-3" /><br id="n0.-4" />Attention, si nous avions tenté la même chose avec un index de type UNIQUE&#8230;<br id="l3as" /><br id="l3as0" />mysql&gt; <code>CREATE UNIQUE INDEX idx_district ON City (District);<br id="l3as1" />ERROR 1062 (23000): Duplicate entry 'Zuid-Holland' for key 2</code><br id="dh1k" /><br id="dh1k0" />&#8230; MySQL nous signale qu&#8217;il y&#8217;a déjà des doublons dans la colonne District, impossible donc de créer un index de type UNIQUE sur celle-ci.<br id="najp" /><br id="najp0" /><strong>Pour supprimer cet index :</strong><br id="najp1" />mysql&gt; <code>DROP INDEX idx_district ON City;</code><br id="a8y02" />Ou :<br id="a8y04" />mysql&gt; <code>ALTER TABLE City DROP INDEX idx_district;</code><br id="bim:" /><strong><br id="bim:0" />Pour visualiser les index d&#8217;une table :</strong><br id="zw.m" /><br id="zw.m0" />mysql&gt; SHOW INDEX FROM Country;</p>
<p><strong>Attention aux doublons !</strong><br id="zw.m17" /><br id="bim:2" />Inutile de rajouter un index de type &laquo;&nbsp;INDEX&nbsp;&raquo; ou encore &laquo;&nbsp;UNIQUE&nbsp;&raquo; sur un champ qui est déjà clé primaire par exemple&#8230; Vous dupliqueriez inutilement les index avec à la clé un gaspillage d&#8217;espace disque/mémoire, des ralentissements inutiles lors des mises à jour, davantage de travail pour l&#8217;optimiseur&#8230;<br id="fq0r" /><br id="p-i7" /><strong id="kw9g2">Quels types de champ indexer ?</strong><br id="bo1r" /><br id="bo1r0" />INT, VARCHAR, BLOB&#8230; ? Quels sont les meilleurs candidats à l&#8217;indexation ?</p>
<p><strong>Plus l&#8217;index est court, mieux c&#8217;est</strong> : un index est en permanence comparé à d&#8217;autres valeurs (celles recherchées), ces comparaisons sont plus rapides si la zone à comparer est plus courte. Des index concis occupent également moins de place sur disque, génèrent moins d&#8217;I/O (activité disque s&#8217;ils ne sont pas en mémoire) et peuvent ainsi être stockés en plus grand nombre dans une même quantité de RAM (pensez au <a href="http://dev.mysql.com/doc/refman/5.0/en/server-system-variables.html#option_mysqld_key_buffer_size" target="_blank">key_buffer_size</a> de MyISAM par exemple).<br id="e1fk" /><br id="e1fk0" />Bref, si vous désirez stocker une liste de noms de villes sous forme de chaînes de caractères, sachez qu&#8217;il est inutile de réserver un champ de type CHAR(255) : rares sont celles qui atteindront cette longueur, pensez plutôt au VARCHAR qui s&#8217;adaptera à la longueur de vos valeurs.<br id="g6j1" />Plus malin encore, lors de la conception de votre base de données, intéressez-vous aux différentes formes de normalisation : 1NF, 2NF, 3NF, ces méthodes permettent d&#8217;obtenir un schéma qui permet de partir sur de bonnes bases.<br id="ist_" />En gardant cet exemple des villes, si vous stockez dans une table &laquo;&nbsp;inscrits&nbsp;&raquo; toutes les infos contextuelles à un utilisateur, dont sa ville, envisagez de stocker dans une table &laquo;&nbsp;ville&nbsp;&raquo;, toutes les infos qui s&#8217;y rapportent : nom, population, etc. Reliez ensuite votre table &laquo;&nbsp;inscrits&nbsp;&raquo; à la table &laquo;&nbsp;ville&nbsp;&raquo; par la valeur de la clé primaire de ville et vous supprimerez ainsi tous ces libellés de villes identiques au profit d&#8217;un ID bien plus rapide et économique.<br id="yxd7" /></p>
<p><strong>Soyez radins !</strong></p>
<p>Vos index seront d&#8217;autant plus efficaces s&#8217;ils sont apposés sur des champs bien adaptés à vos données. <strong>Ne gaspillez pas le capital &laquo;&nbsp;performance&nbsp;&raquo; de vos index</strong> en utilisant un INT pour stocker par exemple la vitesse légale sur autoroute en France (par temps sec) : 130 km/h&#8230;  Un TINY INT UNSIGNED suffira (permet de stocker les valeurs de 0 à 255). Un INT permet lui de stocker des valeurs comprises entre <code id="cps40" class="literal" style="font-family: Verdana;">-2.147.483.648</code> <code id="cps42" class="literal" style="font-family: Verdana;">2.147.483.647, et en rajoutant l'attri</code>but UNSIGNED, on obtient un rayon d&#8217;action de 0 à plus de 4 milliards ! A quoi bon utiliser un INT dans cet exemple ? Quand on sait de plus qu&#8217;un INT occupe quatre fois plus de place qu&#8217;un TINY INT, l&#8217;impact sur les performances et la perte d&#8217;espace avec une table de plusieurs millions d&#8217;enregistrements est évident&#8230;</p>
<p>Prenez connaissance des <a href="http://dev.mysql.com/doc/refman/5.0/en/numeric-types.html" target="_blank">caractéristiques des types de données</a> que vous utilisez. Visualisez également <a href="http://dev.mysql.com/doc/refman/5.0/fr/storage-requirements.html" target="_blank">la taille requise</a>, c&#8217;est un élement qui peut s&#8217;avérer dissuasif.<br id="x-cm" /><br id="x-cm0" />Pour résumer, n&#8217;indexez pas une colonne en fonction de son type, mais prenez soin dans un premier temps de définir celles-ci avec le type de données qui leur convient le mieux, le plus économique. Prévoyez une marge néanmoins : n&#8217;utilisez pas un TINY INT même UNSIGNED pour un identifiant de clé primaire AUTO_INCREMENT concernant une newsletter d&#8217;un grand service commercial : si tout se passe bien vous devriez rapidement dépasser le seuil des 255 inscrits&#8230; Le type de données juste &laquo;&nbsp;au-dessus&nbsp;&raquo;, SMALLINT UNSIGNED, qui permet d&#8217;aller jusqu&#8217;à 65535, est sans doute plus confortable.<br id="t9cg" /><br id="t9cg0" /><strong id="n1bv">Le tips dbnewz : </strong>utilisez la commande <strong id="dwmm">PROCEDURE ANALYSE()</strong><br id="n1bv0" /><br id="n1bv1" />Cette commande analyse pour vous vos tables et vous propose le type idéal pour vos données&#8230; si vous en avez bien sûr, ça ne peut pas vous aider lors d&#8217;une création de table. En revanche, elle permet &laquo;&nbsp;d&#8217;auditer&nbsp;&raquo; vos enregistrements actuels, d&#8217;en tirer quelques statistiques et propose le type le plus adapté :<br id="n3yt" /><br id="uv4e" />mysql&gt; <code>SELECT Name FROM Country PROCEDURE ANALYSE(10,256)\G<br id="uv4e1" /> Field_name: world.Country.<strong>Name</strong><br id="uv4e2" /> Min_value: Afghanistan<br id="uv4e3" /> Max_value: Zimbabwe<br id="uv4e4" /> <strong>Min_length: 4</strong><br id="uv4e5" /> <strong>Max_length: 44</strong><br id="uv4e6" /> Empties_or_zeros: 0<br id="uv4e7" /> Nulls: 0<br id="uv4e8" /><strong>Avg_value_or_avg_length: 10.0962</strong><br id="uv4e9" /> Std: NULL<br id="uv4e10" /> <strong>Optimal_fieldtype</strong>: VARCHAR(44) NOT NULL<br id="uv4e11" />1 row in set (0.00 sec)</code><br id="uv4e12" /><br id="n3yt0" />Vous pouvez bien sûr effectuer cette requête sur l&#8217;intégralité des champs de l&#8217;une de vos tables (SELECT *&#8230;)<br id="lgar" />Les paramètres fournis à <a href="http://dev.mysql.com/doc/refman/5.1/en/procedure-analyse.html" target="_blank">PROCEDURE ANALYSE ()</a> sont à modifier en fonction de vos souhaits. Par défaut cette fonction a tendance à vouloir transformer toutes vos chaînes de caractères en champs <a href="http://dev.mysql.com/doc/refman/5.1/en/enum.html" target="_blank">ENUM</a> (stockés sous forme numérique en interne), à vous de définir combien de champs ENUM vous êtes prêts à utiliser. Réservez-les pour les cas où le champ représente une courte liste &laquo;&nbsp;fermée&nbsp;&raquo;, ex &laquo;&nbsp;M&nbsp;&raquo; ou &laquo;&nbsp;F&nbsp;&raquo;, les jours de la semaine par exemple, etc.<br id="hj59" /><br id="hj590" /><strong id="ny9g">Où placer ses index : quels sont les champs à indexer ?</strong><br id="ny9g0" /><br id="ny9g1" />Les champs concernés par une clause <strong>WHERE, ORDER BY, GROUP BY, MIN(), MAX()</strong>, ainsi que les champs qui permettent de <strong>relier des tables entre elles</strong>, sont de bons candidats à l&#8217;indexation, exemple :<br id="abxv" /><br id="aln7" /><code>SELECT ci.Name, ci.Population <br id="aln70" />FROM City ci INNER JOIN Country co ON ci.CountryCode = co.Code<br id="aln71" />WHERE ci.Population &gt; 5000000<br id="aln72" />ORDER BY ci.District ASC LIMIT 3</code><br id="aln73" /><br id="ulja" />- Le champ CountryCode de la table City ainsi que le champ Code de la table Country sont tous les deux à indexer. C&#8217;est d&#8217;ailleurs le cas ici puisque ces deux champs sont respectivement clés primaires de la table City et Country.<br id="ulja0" />- Le champ Population est intéressant à indexer, il permet à MySQL de parcourir très rapidement les villes par leur population triée et évite de comparer l&#8217;intégralité des populations de chaque ville, une à une.<br id="fv9-" />- Le champ District est également un candidat à l&#8217;indexation, il peut aider MySQL à trier les données plus rapidement.<br id="o0-s" /><br id="o0-s0" />A retenir : on indexe en priorité les champs impliqués dans les clauses évoqués ci-dessus (en gras), pas forcément ceux présents dans le SELECT.<br id="aln75" /><br id="o98j6" /><strong>Les index composés (ou multiples) et la règle du leftmost prefixing</strong><br id="ebx3" /><br id="ebx30" />&laquo;&nbsp;Faut-il préférer un index unique ou composé&nbsp;&raquo; était <a href="http://www.dbnewz.com/2008/06/05/les-index-mysql-la-serie-dont-vous-etes-le-heros/#comments" target="_blank">l&#8217;une des questions posées</a> par l&#8217;un d&#8217;entre vous il y&#8217;a quelques semaines&#8230;<br id="zkm0" /><br id="zkm00" />Un index composé doit se construire en fonction des requêtes que vous effectuez sur la table concernée.<br id="ku.e" />Prenons pour exemple la table City et ses cinq champs (ID, Name, CountryCode, District, Population).<br id="ku.e0" /><br id="ku.e1" />Si les seules requêtes que vous avez sur City sont du type :</p>
<p><code>SELECT ... FROM City WHERE Name = "..."</code></p>
<p>&#8230; Indexez Name et tout ira bien.</p>
<p>Si en revanche il vous arrive de trier non seulement sur &laquo;&nbsp;Name&nbsp;&raquo; mais également sur le code du pays :</p>
<p><code>SELECT ... FROM City WHERE Name = "..." AND CountryCode = "..."</code><br id="f50i0" /><br id="f50i1" />Dans ce cas, plutôt que de laisser MySQL comparer tous les CountryCode de la table avec votre recherche (ex : &laquo;&nbsp;FRA&nbsp;&raquo;), indexez la colonne CountryCode&#8230; Oui mais pas toute seule !<br id="lkbv" />Considérez en effet pour l&#8217;instant que MySQL n&#8217;utilise qu&#8217;un index par table, l&#8217;optimiseur MySQL choisit donc le plus restrictif afin que votre requête s&#8217;exécute le plus rapidement possible (nous verrons plus tard les cas particuliers où MySQL peut tirer parti de plusieurs index).<br id="w0mb" />Conséquence, il vous faut trouver un index qui soit utilisable pour vos deux critères de recherche : &laquo;&nbsp;Name&nbsp;&raquo; et &laquo;&nbsp;CountryCode&nbsp;&raquo;. La solution : créez un index multiple sur ces deux champs.<br id="tilr" /><br id="tilr0" />Dès lors que vous utilisez un index multiple, <strong>la règle </strong><strong>du leftmost prefixing</strong> rentre en jeu. Trop souvent méconnue, elle permet pourtant de créer ses index de façon efficace et d&#8217;éviter les doublons.<br id="ny9g2" /><br id="mqai" />Afin d&#8217;illustrer cette règle, ajoutons cette fois à la table City un index multiple sur les champs Name, CountryCode, District et Population :</p>
<p>mysql&gt; <code>CREATE INDEX name_cc_dis_pop ON City (Name, CountryCode, District, Population);</code><br id="ub980" /></p>
<p>Voici ce que l&#8217;on obtient avec le <strong>SHOW INDEX</strong> correspondant (la clé primaire existait déjà à la création de la table, ci-dessous une vue partielle des résultats réels) :</p>
<p>mysql&gt; <code>SHOW INDEX FROM City;<br id="cvh60" /></code></p>
<p>|<code> <strong>Key_name</strong> | <strong>Seq_in_index</strong> | <strong>Column_name</strong> <br id="cvh62" />+-------+------------+-----------------+</code></p>
<p><code><strong>|name_cc_dis_pop</strong> |   <strong>1</strong> |  <strong>Name</strong> </code></p>
<p><strong>|</strong><code><strong>name_cc_dis_pop</strong> |   <strong>2</strong> | <strong>CountryCode</strong> </code></p>
<p><code><strong>|</strong><strong>name_cc_dis_pop</strong> |   <strong>3</strong> | <strong>District</strong> </code></p>
<p><code> <strong>|name_cc_dis_pop</strong> |   <strong>4</strong> | <strong>Population</strong> |  <br id="cvh68" />+-------+------------+-----------------+</code></p>
<p>On remarque par rapport au SHOW INDEX précédent que cette fois-ci nous avons la colonne &laquo;&nbsp;Seq_in_index&nbsp;&raquo; qui s&#8217;incrémente pour chaque colonne qui compose notre index multiple. La position de chaque index dans cette séquence a une importance, c&#8217;est ce que nous allons voir maintenant.</p>
<p>Tel quel, cet index multiple sera potentiellement utilisé pour les requêtes de ce type :</p>
<p><code>SELECT ... FROM City WHERE Name = ... AND CountryCode = ... AND District = ... AND Population = ...<br id="w2qj" />SELECT ... FROM City WHERE Name = ... AND CountryCode = ... AND District = ... <br id="w2qj0" />SELECT ... FROM City WHERE Name = ... AND CountryCode = ... <br id="w2qj1" />SELECT ... FROM City WHERE Name = ...</code><br id="w2qj2" /><br id="w2qj3" />Une fois cette &laquo;&nbsp;logique&nbsp;&raquo; acquise, on comprend qu&#8217;il est inutile de rajouter un index sur Name par exemple puisque cette colonne est déjà indexée grâce à cet index multiple. Idem pour notre exemple précédent  d&#8217;index multiple concernant les champs Name et CountryCode, là encore inutile de recréer un index sur ces deux champs puisque ces derniers sont déjà représentés dans notre dernier exemple.<br id="niuy" /><br id="niuy0" />En revanche, si l&#8217;ordre de vos champs ne respecte pas l&#8217;ordre de séquence de votre index, comme ici :</p>
<p><code>SELECT ... FROM City WHERE Name = ... AND Population = ..</code></p>
<p>Cette requête ne bénéficiera pas complètement de l&#8217;index multiple précedemment crée, cela dit l&#8217;optimiseur tirera sûrement parti de cet index pour la colonne Name, mais pas pour le second critère de recherche.<br id="ewwh0" /><br id="ewwh1" />De même si votre requête est du type :</p>
<p><code>SELECT ... FROM City WHERE CountryCode = ...<br id="ewwh3" />SELECT ... FROM City WHERE District = ...<br id="ewwh4" />SELECT ... FROM City WHERE Population = ...<br id="ewwh5" />SELECT ... FROM City WHERE CountryCode = ... AND District = ... AND Population = ...</code><br id="ewwh6" /></p>
<p>&#8230; et autres variations qui ne débutent pas avec &laquo;&nbsp;Name&nbsp;&raquo; et ne respectent pas ensuite l&#8217;ordre de séquence de l&#8217;index (Name, CountryCode, District, et Population), l&#8217;index ne sera pas utilisé.<br id="z9sx" /><br id="z9sx0" />En conséquence, il est donc tout à fait légitime d&#8217;indexer par ailleurs la colonne Population seule si vous avez des requêtes du type :</p>
<p><code>SELECT ... FROM City WHERE Population &gt; ... </code><br id="p_16" /><br id="ny9g4" /><strong>Mesurez l&#8217;efficacité des index avec EXPLAIN</strong><br id="n6b2" /></p>
<p>Impossible d&#8217;évoquer les index sans parler de la commande <strong>EXPLAIN</strong>. Absolument <strong id="kjkb">fondamentale</strong>, elle affiche le plan d&#8217;exécution décidé par l&#8217;optimiseur MySQL et vous permet de mesurer si oui ou non vos index sont réellement utilisés.<br id="tqy0" /><br id="tqy00" />Reprenons une des premières requêtes de ce billet et rajoutons-lui le mot clé EXPLAIN :<br id="fo2v" />(on considère ici que la table City ne contient que sa clé primaire, pas les index rajoutés précedemment)</p>
<p>mysql&gt; <code><strong>EXPLAIN </strong>SELECT ci.Name, ci.Population<br id="dhkz" />FROM City ci INNER JOIN Country co ON ci.CountryCode = co.Code<br id="dhkz0" />WHERE ci.Population &gt; 5000000<br id="dhkz1" />ORDER BY ci.District ASC LIMIT 3\G<br id="dhkz2" /></code></p>
<p><code>*************************** 1. row *************<br />
id: 1<br />
select_type: SIMPLE<br />
table: ci<br />
type: ALL<br />
<strong>possible_keys: NULL</strong><br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
<strong>rows: 4079</strong><br />
Extra: Using where; Using filesort<br />
*************************** 2. row *************<br />
id: 1<br />
select_type: SIMPLE<br />
table: co<br />
type: eq_ref<br />
<strong>possible_keys: PRIMARY</strong><br />
<strong>key: PRIMARY</strong><br />
key_len: 3<br />
<strong>ref: world.ci.CountryCode</strong><br />
<strong>rows: 1</strong><br />
<strong>Extra: Using index</strong><br />
2 rows in set (0.00 sec)</code></p>
<p>La commande EXPLAIN sera étudiée plus précisemment dans un autre épisode, pour le moment contentons-nous de prêter attention aux champs en gras :</p>
<p>- Sur la première ligne le type ALL signale que MySQL doit effectuer un &laquo;&nbsp;full table scan&nbsp;&raquo; c&#8217;est à dire parcourir entièrement la table City qui compte 4079 enregistrements, ceci afin de repérer quelles sont les villes qui ont une population supérieure à 5M d&#8217;habitants. Aucun index/key n&#8217;a pu être utilisé (possible_keys : NULL) pour résoudre cette partie de la requête. La colonne &laquo;&nbsp;rows&nbsp;&raquo; indique le nombre approximatif d&#8217;enregistrements que MySQL pense devoir analyser pour mener à bien l&#8217;opération.</p>
<p>- La seconde ligne nous indique que cette fois MySQL a un candidat pour l&#8217;indexation, il s&#8217;agit de la clé primaire de la table Country, la jointure s&#8217;effectuant avec le champ &laquo;&nbsp;CountryCode&nbsp;&raquo; de la table City, qui est également une clé primaire. Résultat : MySQL effectue la correspondance très rapidement (rows : 1 et extra : Using index).<br id="sewv" /><br id="sewv0" />Ceci répond à une des questions posées précedemment par un lecteur : &laquo;&nbsp;Quand préférer le FULL TABLE SCAN à l&#8217;index ?&nbsp;&raquo; C&#8217;est en fait le travail de l&#8217;optimiseur, il doit déterminer si oui ou non un index vous fera gagner du temps. Il se peut qu&#8217;il se trompe (rarement), nous verrons comment orienter ses choix si besoin.<br id="ai:q" /><br id="ai:q0" />Rajoutons maintenant un index de type &laquo;&nbsp;INDEX&nbsp;&raquo; sur la colonne Population :<br id="ai:q1" /></p>
<p>mysql&gt; <code>CREATE INDEX idx_pop ON City (Population);</code><br id="j6rd" /><br id="j6rd0" />Puis appliquons à nouveau la même commande EXPLAIN, on obtient cette fois :</p>
<p><code>*************************** 1. row ************<br />
id: 1<br />
select_type: SIMPLE<br />
table: ci<br />
type: range<br />
<strong>possible_keys: idx_pop</strong><br />
<strong>key: idx_pop</strong><br />
key_len: 4<br />
ref: NULL<br />
<strong>rows: 25</strong><br />
Extra: Using where; Using filesort<br />
*************************** 2. row ************<br />
id: 1<br />
select_type: SIMPLE<br />
table: co<br />
type: eq_ref<br />
<strong>possible_keys: PRIMARY<br />
key: PRIMARY</strong><br />
key_len: 3<br />
ref: world.ci.CountryCode<br />
<strong>rows: 1</strong><br />
Extra: Using index<br />
2 rows in set (0.02 sec)<br id="ecl19" /></code><br id="ecl110" />Notre index a permis à MySQL de lire <strong>160 fois moins de lignes</strong> cette fois-ci par rapport à l&#8217;exemple précédent&#8230; Seules 25 lignes de la table City sont lues désormais (au lieu des 4079 lignes précedemment parcourues). C&#8217;est un gain très intéressant en termes de ressources serveur ! Amateurs de chiffres, des benchmarks sont prévus dans la suite de cette série.<br id="laec" /><br id="laec0" />Afin de patienter jusqu&#8217;aux prochains épisodes justement, vous pouvez commencer par appliquer les quelques conseils de ce billet tout en relisant pourquoi pas l&#8217;article concernant <a href="http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/" target="_blank">l&#8217;implémentation des index (BTREE ou HASH)</a> selon le type de votre moteur de stockage (MyISAM, InnoDB ou MEMORY). <br id="dhkz10" /><br id="m:ru" />Il reste certaines de vos questions en suspens, elles ne sont pas oubliées et seront débattues ici très prochainement.</p>
<p>Si vous avez des questions ou des remarques concernant cette première étape, n&#8217;hésitez pas.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/06/27/les-index-mysql-types-placements-efficacite/feed/</wfw:commentRss>
		<slash:comments>17</slash:comments>
		</item>
		<item>
		<title>Choisir l&#8217;implémentation de ses index : &#171;&#160;B-TREE&#160;&#187; ou &#171;&#160;HASH&#160;&#187;, quelles différences ?</title>
		<link>http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/</link>
		<comments>http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/#comments</comments>
		<pubDate>Sun, 18 May 2008 23:12:15 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[4.1]]></category>
		<category><![CDATA[5.0]]></category>
		<category><![CDATA[5.1]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/</guid>
		<description><![CDATA[Préambule technique à une série de futurs articles, je ne vous en dis pas plus, l&#8217;épisode du jour a pour point de départ un moteur de stockage MySQL avec à la clé la possibilité, ou pas, de définir l&#8217;implémentation de ses index : B-TREE ou HASH.
Ce choix n&#8217;est en effet pas toujours disponible, c&#8217;est même [...]]]></description>
			<content:encoded><![CDATA[<p>Préambule technique à une série de futurs articles, je ne vous en dis pas plus, l&#8217;épisode du jour a pour point de départ un moteur de stockage MySQL avec à la clé la possibilité, ou pas, de définir l&#8217;implémentation de ses index : B-TREE ou HASH.</p>
<p>Ce choix n&#8217;est en effet pas toujours disponible, c&#8217;est même plutôt rare puisque seul le moteur de stockage MEMORY vous permet depuis la version 4.1 de MySQL, d&#8217;effectuer ce choix. Nous ne parlerons pas ici du MySQL Cluster et de son moteur NDB qui sera abordé spécifiquement dans un autre épisode.</p>
<p>Pourquoi alors se soucier de ce type d&#8217;implémentation si seul le moteur MEMORY offre la possibilité de choisir ?<br />
- MyISAM et InnoDB pourraient à l&#8217;avenir proposer ce choix.<br />
- Afin de comprendre plus finement comment fonctionnent les index que vous utilisez tous les jours, se pencher sur la façon dont ils sont implémentés permet de mieux appréhender certains résultats.</p>
<p><span id="more-39"></span></p>
<p><strong>L&#8217;index B-TREE</strong></p>
<p>Star incontestée de l&#8217;implémentation de nos index sous MySQL, l&#8217;index B-TREE est partout. Et pour cause : c&#8217;est la seule implémentation disponible sous MyISAM et InnoDB, vos index sont donc systématiquement stockés sous forme de B-TREE si vos tables sont issues d&#8217;un de ces moteurs de stockage.</p>
<p>&laquo;&nbsp;Soient L et U deux entiers naturels non nuls tels que L ≤ U. On définit alors un L-U arbre B [...]&laquo;&nbsp;, &#8230; C&#8217;est exact, il y&#8217;a plusieurs façons d&#8217;expliquer le fonctionnement d&#8217;un index B-TREE, les formules mathématiques en sont une et <a href="http://fr.wikipedia.org/wiki/Arbre_B" target="_blank">wikipédia </a>s&#8217;en chargeant très bien, je ne vais donc pas recopier les formules mathématiques ici mais vous proposer plutôt un schéma :</p>
<p><img src="http://upload.wikimedia.org/wikipedia/en/5/5f/B-tree.png" alt="B-tree" width="303" height="88" /></p>
<p>Ce schéma est en fait issu <a href="http://slady.net/java/bt/" target="_blank">d&#8217;une animation</a> (essayez-là), cette applet java permet d&#8217;appréhender de façon presque ludique le fonctionnement du B-TREE. Amusez-vous à insérer des données et à prédire leur destination finale. Une fois que vous vous sentez à l&#8217;aise en insertion, testez le mode suppression et dites bonjour à la récursivité&#8230;</p>
<p>En deux mots, un B-TREE est un <strong>arbre équilibré</strong> (cette notion se perçoit bien visuellement), dont les données sont <strong>triées</strong>. A la base se trouve une racine de laquelle partent des noeuds qui se divisent ou fusionnent selon les données insérées. Chaque noeud a plusieurs clés ou valeurs, ce qui permet de diminuer la complexité de l&#8217;arbre et réduit la nécessité d&#8217;équilibrage (répartition des données).</p>
<p>Si les index B-TREE sont choisis par défaut pour les deux moteurs les plus utilisés, MyISAM et InnoDB, ça n&#8217;est pas par hasard. Polyvalents, les index B-TREE permettent d&#8217;effectuer un vaste choix de comparaisons.  Tous les opérateurs suivants sont autorisés : =, &gt;, &gt;=, &lt;, &lt;=, la commande SQL &laquo;&nbsp;BETWEEN&nbsp;&raquo; se rajoutant à cette liste.</p>
<p>Les valeurs extrèmes des noeuds peuvent être considérées comme des bornes qui déterminent l&#8217;emplacement potentiel d&#8217;une clé recherchée ou à insérer.</p>
<p><strong>Le HASH index</strong></p>
<p>Contrairement aux moteurs MyISAM et InnoDB, le moteur MEMORY utilise par défaut une implémentation de type HASH. Cela ne veut pas dire que vous ne pouvez pas créer de B-TREE pour autant pour une colonne en particulier :</p>
<p>mysql&gt; create table essai (id1 int, id2 int, unique using hash (id1), unique using btree (id2)) engine = memory;<br />
Query OK, 0 rows affected (0.05 sec)</p>
<p><code>mysql&gt; show create table essai;<br />
----------------------------------+<br />
| Table | Create Table<br />
----------------------------------+<br />
| essai | CREATE TABLE `essai` (<br />
`id1` int(11) default NULL,<br />
`id2` int(11) default NULL,<br />
UNIQUE KEY `id1` <strong>USING HASH</strong> (`id1`),<br />
UNIQUE KEY `id2` <strong>USING BTREE</strong> (`id2`)<br />
) ENGINE=MEMORY DEFAULT CHARSET=latin1</code></p>
<p>Quel est le principe de fonctionnement d&#8217;un index de type HASH ? Encore une fois, un schéma permet de se fixer les idées :</p>
<p><img src="http://www.dbnewz.com/wp-content/uploads/2008/05/20080519_hash_index.png" alt="hash index" width="362" height="195" /></p>
<p>Issu de l&#8217;article consacré aux <a href="http://fr.wikipedia.org/wiki/Table_de_hachage" target="_blank">tables de hachage</a> sur wikipedia, il permet de bien comprendre ce qui se passe.</p>
<p>On observe sur ce schéma que lors d&#8217;une recherche une fonction de hachage est appliquée sur la clé, ici &laquo;&nbsp;John Smith&nbsp;&raquo;. Une fois hachée, cette chaîne de caractères devient un index qui pointe vers la donnée recherchée (le numéro de téléphone).<br />
A noter : les données ne sont pas <strong>ordonnées</strong>.</p>
<p>Conséquence de ce mécanisme, les index HASH permettent uniquement les comparaisons d&#8217;égalité effectuées via les opérateurs &laquo;&nbsp;=&nbsp;&raquo; ou &laquo;&nbsp;&lt;=&gt;&nbsp;&raquo;. Rappelons que &laquo;&nbsp;&lt;=&gt;&nbsp;&raquo; permet de considérer la valeur NULL comme une valeur &laquo;&nbsp;normale&nbsp;&raquo;, exemple :</p>
<p><code>mysql&gt; select NULL = NULL;<br />
+-------------+<br />
| NULL = NULL |<br />
+-------------+<br />
|        NULL        |<br />
+-------------+<br />
1 row in set (0.00 sec)</code></p>
<p><code>mysql&gt; select NULL &lt;=&gt; NULL;<br />
+---------------+<br />
| NULL &lt;=&gt; NULL |<br />
+---------------+<br />
|             1             |<br />
+---------------+</code></p>
<p>Bref, les index HASH ne permettent pas de répondre à tous les types de requête.</p>
<p>Démonstration en utilisant la base bien connue &laquo;&nbsp;world&nbsp;&raquo; <a href="http://dev.mysql.com/doc/" target="_blank">disponible au téléchargement</a> sur le site de MySQL.<br />
Issue de cette base, la table &laquo;&nbsp;country&nbsp;&raquo; contient 239 pays dont la clé primaire est un code de trois caractères, &#8216;FRA&#8217; pour la France par exemple. On cherche à déterminer quels sont les pays dont le code abrégé est supérieur à la chaîne de caractère &#8216;TOTO&#8217; .</p>
<p>Par défaut cette table est en MyISAM. Nous sommes donc en présence d&#8217;une implémentation B-TREE pour l&#8217;index. On effectue un EXPLAIN sur une requête très simple :</p>
<p>mysql&gt; explain select * from country where code &gt; &#8216;TOTO&#8217;\G<br />
*************************** 1. row ******</p>
<p>id: 1<br />
select_type: SIMPLE<br />
table: country<br />
type: range<br />
possible_keys: PRIMARY<br />
key: PRIMARY<br />
key_len: 3<br />
ref: NULL<br />
rows: 27<br />
Extra: Using where<br />
1 row in set (0.00 sec)</p>
<p>(A noter : le &laquo;&nbsp;\G&nbsp;&raquo; permet un affichage vertical à partir du client mysql.)</p>
<p>On voit ici que la clé primaire est bien utilisée pour répondre à cette requête (key : PRIMARY).</p>
<p>Modifions le moteur de stockage pour du MEMORY et réappliquons la même requête :</p>
<p>mysql&gt; alter table country engine = MEMORY;<br />
Query OK, 239 rows affected (0.06 sec)<br />
Records: 239  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; explain select * from country where code &gt; &#8216;TOTO&#8217;\G<br />
*************************** 1. row ************</p>
<p>id: 1<br />
select_type: SIMPLE<br />
table: country<br />
type: ALL<br />
possible_keys: PRIMARY<br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
rows: 239<br />
Extra: Using where<br />
1 row in set (0.00 sec)</p>
<p>On voit ici que la recherche n&#8217;a pas pu s&#8217;effectuer via la clé primaire implémentée en HASH. La clé primaire est bien détectée comme candidate potentielle (possible_keys : PRIMARY) mais elle n&#8217;est finalement pas retenue (key : NULL) et c&#8217;est finalement un parcours total de la table qui sera effectué (type : ALL).</p>
<p>Les données n&#8217;étant pas ordonnées, l&#8217;indexation du champ &laquo;&nbsp;Code&nbsp;&raquo; n&#8217;a malgré tout pas permis de déterminer qu&#8217;elles étaient les valeurs des codes suivants ou précédents.</p>
<p>Bien évidemment sur un tel exemple à la volumétrie aussi faible, l&#8217;impact sur les performances est insignifiant. Néanmoins ceci permet de comprendre que la transformation d&#8217;une table MyISAM à une table MEMORY par un simple ALTER TABLE ne permet pas forcément d&#8217;obtenir le même plan d&#8217;exécution sur les deux moteurs et ceci pour une définition de table identique.<br />
Il faut donc surveiller le plan d&#8217;exécution avant et après la transformation, ainsi que mesurer les performances obtenues (benchmarks) afin de mesurer l&#8217;impact du passage d&#8217;un moteur à l&#8217;autre. Accéder à des données en mémoire est bien sûr plus rapide qu&#8217;aller les chercher sur disque mais peut-être avez-vous réellement besoin qu&#8217;un index particulier soit pris en compte, à vous de le vérifier.</p>
<p>A noter enfin qu&#8217;il est possible avec le moteur MEMORY de définir pour une colonne donnée un HASH index non unique, c&#8217;est une implémentation des HASH index un peu particulière choisie par MySQL. <a href="http://dev.mysql.com/doc/refman/5.1/en/memory-storage-engine.html" target="_blank">La documentation</a> conseille d&#8217;ailleurs à ce sujet d&#8217;utiliser l&#8217;implémentation B-TREE pour les champs dont les données sont trop redondantes. En effet, des ralentissements proportionnels à cette redondance sont à prévoir lors des UPDATE et DELETE. Un billet de <a href="http://www.mysqlperformanceblog.com/2008/02/01/performance-gotcha-of-mysql-memory-tables" target="_blank">Peter Zaitsev</a> relate parfaitement ce phénomène.</p>
<p><strong>InnoDB et l&#8217;ADAPTIVE </strong><strong>HASH INDEX</strong></p>
<p>Particularité d&#8217;InnoDB, ce moteur est capable de &laquo;&nbsp;transformer&nbsp;&raquo; les index B-TREE en équivalent HASH si l&#8217;optimiseur détecte qu&#8217;un gain est possible, d&#8217;où le nom de cette technique : <a href="http://dev.mysql.com/doc/refman/5.0/en/innodb-adaptive-hash.html" target="_blank">ADAPTIVE HASH INDEX</a>, (attention la traduction française utilise &laquo;&nbsp;base en mémoire&nbsp;&raquo;, il s&#8217;agit d&#8217;une &laquo;&nbsp;table&nbsp;&raquo;).<br />
Dans un tel cas, les index restent implémentés en B-TREE, cependant InnoDB peut créer à la volée des versions HASH de ces derniers. La commande SHOW INNODB STATUS est alors toute indiquée pour observer <a href="http://mysqldba.blogspot.com/2006/07/show-innodb-status-adaptive-hash-index.html" target="_blank">les performances relatives aux deux implémentations</a>.</p>
<p><strong>A retenir :</strong></p>
<p>- Parmi MyISAM, InnoDB et MEMORY, seul le moteur de stockage MEMORY vous permet de choisir l&#8217;implémentation (B-TREE ou HASH) de vos index.<br />
- Ce choix a une incidence sur le plan d&#8217;exécution de vos requêtes.<br />
- Les index de type HASH sont très performants mais restreints aux  opérateurs &laquo;&nbsp;=&nbsp;&raquo; et &laquo;&nbsp;&lt;=&gt;&nbsp;&raquo;.<br />
- Les index de type B-TREE sont les plus utilisés et plus polyvalents (&laquo;&nbsp;=&nbsp;&raquo;, &laquo;&nbsp;&gt;&nbsp;&raquo;, &laquo;&nbsp;&gt;=&nbsp;&raquo;, &laquo;&nbsp;&lt;&nbsp;&raquo;, &laquo;&nbsp;&lt;=&nbsp;&raquo;).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>log-slave-updates</title>
		<link>http://www.dbnewz.com/2007/07/05/log-slave-updates/</link>
		<comments>http://www.dbnewz.com/2007/07/05/log-slave-updates/#comments</comments>
		<pubDate>Thu, 05 Jul 2007 16:42:00 +0000</pubDate>
		<dc:creator>pébé</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[réplication]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/2007-07/19-log-slave-updates.htm</guid>
		<description><![CDATA[Des erreurs en enchaînant des réplications? J&#8217;ai retrouvé souvent la même erreur sur des configurations en &#171;&#160;multi master&#160;&#187; ou celles qui utilisent des &#171;&#160;relay slave&#160;&#187;.  Imaginons que vous ayez 3 serveurs A -&#62; B -&#62; C. A étant le master, B le relay slave et C le slave. Tout se passe bien, le résultat [...]]]></description>
			<content:encoded><![CDATA[<p>Des erreurs en enchaînant des réplications? J&#8217;ai retrouvé souvent la même erreur sur des configurations en &laquo;&nbsp;multi master&nbsp;&raquo; ou celles qui utilisent des &laquo;&nbsp;relay slave&nbsp;&raquo;.  Imaginons que vous ayez 3 serveurs A -&gt; B -&gt; C. A étant le master, B le relay slave et C le slave. Tout se passe bien, le résultat de la commande &laquo;&nbsp;show slave status&nbsp;&raquo; vous informe que tout roule et pourtant le slave (C) n&#8217;est pas à jour. Alors pourquoi? magie vaudou?<br />
Non! Vous avez tout simplement oublié d&#8217;ajouter sur votre relay slave (B) le paramètre &laquo;&nbsp;log-slave-updates&nbsp;&raquo;.</p>
<p>Un slave n&#8217;enregistre pas dans son journal (ses binlogs) les commandes qu&#8217;il reçoit de son master.   Donc dans notre cas (B) est à jour mais (C) n&#8217;a aucun moyen de connaître les commandes car (C) lit juste les binlogs de (B).</p>
<p>En rajoutant log-slave-updates dans le my.cnf de (B), il écrira toutes les commandes dans les binlogs et (C) sera ainsi à jour.</p>
<p>C&#8217;est aussi un bon moyen pour contrôler toutes les commandes exécutées sur votre slave. Cela m&#8217;a permis un jour de trouver la source d&#8217;un problème. Un client mal configuré venait mettre à jour un slave et faisait ainsi planter la réplication.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2007/07/05/log-slave-updates/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Innodb et &#171;&#160;autoshrink&#160;&#187;</title>
		<link>http://www.dbnewz.com/2007/06/18/innodb-et-autoshrink/</link>
		<comments>http://www.dbnewz.com/2007/06/18/innodb-et-autoshrink/#comments</comments>
		<pubDate>Mon, 18 Jun 2007 21:09:26 +0000</pubDate>
		<dc:creator>pébé</dc:creator>
				<category><![CDATA[IBMDB2]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/2007-06/18-innodb-et-autoshrink.htm</guid>
		<description><![CDATA[Aujourd&#8217;hui j&#8217;ai reçu un message d&#8217;un collègue qui était surpris de voir que son &#171;&#160;datafile&#160;&#187; InnoDB gardait la même taille aprés avoir effacé des tables ou des éléments de ses tables.
C&#8217;est tout à fait normal! Toutes les bases de données marchent de la même facon. Un tablespace s&#8217;agrandira tout seul si vous le créez en [...]]]></description>
			<content:encoded><![CDATA[<p>Aujourd&#8217;hui j&#8217;ai reçu un message d&#8217;un collègue qui était surpris de voir que son &laquo;&nbsp;datafile&nbsp;&raquo; InnoDB gardait la même taille aprés avoir effacé des tables ou des éléments de ses tables.<br />
C&#8217;est tout à fait normal! Toutes les bases de données marchent de la même facon. Un tablespace s&#8217;agrandira tout seul si vous le créez en mode autoextend et ce sous MySQL mais aussi Oracle, IBM DB2,&#8230; Si vous n&#8217;activez pas la fonction autoextend quand la base voudra allouer de la place, vous verrez une belle erreur.</p>
<p>La question est pourquoi? Performances!</p>
<p>Pour gagner en performances, vous voulez que votre base pré alloue de l&#8217;espace disque, des blocks de data pour vous. Nous parlons alors d&#8217;extend. Vu ces conditions veut on vraiment désallouer des extends pour les réallouer ensuite? La réponse est non!</p>
<p>Comment alors récupérer de l&#8217;espace? La seule solution est d&#8217;exporter les data, d&#8217;effacer les files et importer le tout.</p>
<p>Que peut on faire pour optimiser ça?</p>
<p>Effacer un maximum avant d&#8217;ajouter de l&#8217;information et ainsi contrôler du mieux que l&#8217;on peut la taille de votre tablespace.</p>
<p>Et pour MySQL?</p>
<p>Après avoir discuté avec <a href="http://www.mysqlperformanceblog.com/about/" target="_blank">Peter Zaitsev</a> et <a href="http://www.innodb.com/" target="_blank">Heikki Tuuri</a>, il semblerait que &laquo;&nbsp;innodb_file_per_table&nbsp;&raquo; qui est apparu avec  MySQL 4.1.3, pourrait contourner le problème.  Cela semblerai être plus performant qu&#8217;un seul tablespace.</p>
<p>&laquo;&nbsp;innodb_file_per_table&nbsp;&raquo; est un paramètre de configuration dans le my.cnf, et est pris en compte seulement lors de la création d&#8217;une table. Donc pour migrer d&#8217;un seul tablespace vers plusieurs fichiers vous devrez encore une fois faire un export/import.</p>
<p>Une fois ceci fait, si vous effacez vos entrées et faite un &laquo;&nbsp;OPTIMIZE TABLE&nbsp;&raquo; l&#8217;OS libérera l&#8217;espace.  Chaque table a son propre fichier .ibd et la commande recrée un fichier,  efface l&#8217;ancien et renomme le nouveau.</p>
<p>Ce qui veut dire qu&#8217;a un instant t vous avez 2 fois les data, prévoyez donc une place suffisante sur votre disque et n&#8217;attendez pas le dernier moment.</p>
<p>Pour conclure, autoextend c&#8217;est bien mais vous devez néanmoins surveillez ça de prés.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2007/06/18/innodb-et-autoshrink/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Com_admin_commands</title>
		<link>http://www.dbnewz.com/2007/06/18/com_admin_commands/</link>
		<comments>http://www.dbnewz.com/2007/06/18/com_admin_commands/#comments</comments>
		<pubDate>Mon, 18 Jun 2007 09:59:12 +0000</pubDate>
		<dc:creator>pébé</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[livres]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/2007-06/17-com_admin_commands.htm</guid>
		<description><![CDATA[Si vous jouez un peu avec MySQL vous devez déjà connaitre la commande SHOW STATUS. Elle vous donne un snapshot de tous les compteurs interne de MySQL. Ces compteurs traquent des événements précis, par exemple:
mysql&#62; show status like &#8216;Com_select&#8217;;
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;+
&#124; Variable_name &#124; Value      &#124;
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;+
&#124; Com_select    &#124; 2293615720 &#124;
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;+
1 [...]]]></description>
			<content:encoded><![CDATA[<p>Si vous jouez un peu avec MySQL vous devez déjà connaitre la commande SHOW STATUS. Elle vous donne un snapshot de tous les compteurs interne de MySQL. Ces compteurs traquent des événements précis, par exemple:</p>
<p>mysql&gt; show status like &#8216;Com_select&#8217;;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;+<br />
| Variable_name | Value      |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;+<br />
| Com_select    | 2293615720 |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;+<br />
1 row in set (0.00 sec)</p>
<p>Voila le nombre de select qu&#8217;il y a eu sur ce server, 2.3 milliards&#8230; oups&#8230;</p>
<p>mysql&gt; show status like &#8216;Questions&#8217;;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8211;+<br />
| Variable_name | Value     |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8211;+<br />
| Questions     | 533719732 |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8211;+<br />
1 row in set (0.00 sec)</p>
<p>&#8216;Questions&#8217; est le nombre de requêtes et commandes envoyées au serveur et devrait donc être logiquement la somme de toutes les &laquo;&nbsp;Com_*&nbsp;&raquo;.<br />
Est ce un bug? 533 millions alors que rien qu&#8217;en select je suis déjà à 2.3 milliard?</p>
<p>En fait tous ces compteurs sont des entiers non signés. Donc sur des plateformes 32bit nous atteignons un maximum de 4.2 milliards avant que cela revienne à zéro. Donc tout est bien normal ici.</p>
<p>Maintenant la valeur qui est bizarre pour moi est &#8216;Com_admin_commands&#8217;.</p>
<p>mysql&gt; show status like &#8216;Com_admin_commands&#8217;;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;+&#8212;&#8212;&#8212;&#8212;+<br />
| Variable_name      | Value      |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;+&#8212;&#8212;&#8212;&#8212;+<br />
| Com_admin_commands | 2731594764 |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;+&#8212;&#8212;&#8212;&#8212;+<br />
1 row in set (0.00 sec)</p>
<p>2,7 milliards, ok mais à quoi correspond une commande admin? Je n&#8217;ai pas trouvé une seule ligne de documentation la dessus. Donc la seule solution est de regarder le code source (J&#8217;adore l&#8217;Open Source).  C&#8217;est ainsi que l&#8217;on découvre que cela correspond à</p>
<ul>
<li>COM_TABLE_DUMP: demande au serveur d&#8217;envoyer la définition des tables et les data au format RAW. C&#8217;est utilisé par la réplication si vous utilisez LOAD DATA FROM MASTER. C&#8217;est commande est obsolète, NE PLUS UTILISER.</li>
<li>COM_CHANGE_USER: dit au serveur que le client veut changer le user associé à la session &#8211; mysql_change_user( ).</li>
<li>COM_BINLOG_DUMP:  demande au serveur d&#8217;envoyer un flux constent des binlogs à partir d&#8217;une certaine valeur &#8211; mysqlbinlog</li>
<li>COM_SHUTDOWN: commande l&#8217;arrêt du serveur &#8211; mysql_shutdown( )</li>
<li>COM_PING: ping pour MySQL &#8211; mysql_ping( )</li>
<li>COM_DEBUG: force un dump dans le log d&#8217;erreurs &#8211; mysql_dump_debug_info( )</li>
</ul>
<p>Pour plus de détails la dessus, je vous conseille le nouveau livre de Sasha Pachev aux éditions O&#8217;Reilly, <a href="http://www.amazon.fr/Understanding-MySQL-Internals-Sasha-Pachev/dp/0596009577/" target="_blank">Understanding MySQL Internals</a> qui est sorti en Avril 2007 et a été présenté pendant la conférence MySQL.</p>
<p>Donc revenons à cette base, nous avons 2.3 milliards de commandes admin, et cela semble être le plus vraisemblablement à cause de la fonction ping. Je me demande vraiment à quoi cela peut servir si ce n&#8217;est à garder des connections ouvertes pour rien.</p>
<ul>
<li>à t, je ping tout est ok</li>
<li>à t+1, je perd ma connection</li>
<li>à t+2, j&#8217;execute ma commande et j&#8217;ai pas de connection</li>
</ul>
<p>C&#8217;est une fonction faite pour les clients qui gardent les connections en attente pendant longtemps&#8230;ce qui est INUTILE avec MySQL. Au lieu de garder des connections dans le vide&#8230; FERMEZ les!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2007/06/18/com_admin_commands/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>max_connections</title>
		<link>http://www.dbnewz.com/2007/05/22/max_connections/</link>
		<comments>http://www.dbnewz.com/2007/05/22/max_connections/#comments</comments>
		<pubDate>Mon, 21 May 2007 22:20:47 +0000</pubDate>
		<dc:creator>pébé</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[pratique]]></category>
		<category><![CDATA[tuning]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/2007-05/15-max_connections.htm</guid>
		<description><![CDATA[Aujourd&#8217;hui fut la journée des &#8216;too many connections&#8217;. En effet pas loin de 3 applications ont planté du fait que le nombre maximum de connexions MySQL avait été atteint. Le message d&#8217;erreur est très parlant. Comment est ce possible? mysqld autorise max_connections+1 clients à se connecter. Le &#8216;+1&#8242; est une extra connexion réservée aux comptes [...]]]></description>
			<content:encoded><![CDATA[<p>Aujourd&#8217;hui fut la journée des &#8216;too many connections&#8217;. En effet pas loin de 3 applications ont planté du fait que le nombre maximum de connexions MySQL avait été atteint. Le message d&#8217;erreur est très parlant. Comment est ce possible? mysqld autorise max_connections+1 clients à se connecter. Le &#8216;+1&#8242; est une extra connexion réservée aux comptes ayant le privilège SUPER. Donc si votre user applicatif à ce privilège, vous vous retrouvez bloqués.  Prenez comme principe d&#8217;avoir les privilèges minimaux pour vos utilisateurs, INSERT, UPDATE, DELETE et SELECT suffisent largement.</p>
<p>La valeur par défaut du paramètre est de 100.</p>
<p><code><br />
mysql&gt; show variables like 'max_connections';<br />
+-----------------+-------+<br />
| Variable_name   | Value |<br />
+-----------------+-------+<br />
| max_connections | 100   |<br />
+-----------------+-------+<br />
</code></p>
<p>Donc à vous de connaître vos besoins et d&#8217;allouer un nombre maximum suffisant pour votre application. Cela peut être utile pour vous montrer une erreur logicielle si le nombre est trop important.</p>
<p>Maintenant quelle est le problème si j&#8217;autorise trop de connexions simultanées.  Tout simplement d&#8217; utiliser trop de mémoire et de planter votre serveur. Il faut garder en tête cette rapide équation:</p>
<p>Un connexion ( MySQL thread ) utilise en RAM au maximum:<br />
( thread_stack + net_buffer_length + max_allowed_packet + read_buffer_size + join_buffer_size + tmp_table_size + myisam_sort_buffer_size )</p>
<p>Sachant que vous avez déjà alloué de la mémoire à votre buffer pool / key cache, restez vigilant à ne pas dépasser le total de votre serveur.</p>
<p>Avec MySQL 5.0, une nouvelle variable est apparue, &#8216;max_user_connections&#8217; pour limiter le nombre de connexions concurrentes pour un même utilisateur. C&#8217;est une variable globale pour TOUS les utilisateurs et activée pour les valeurs &gt; 0.</p>
<p><code><br />
mysql&gt; show variables like 'max_user_connections';<br />
+----------------------+-------+<br />
| Variable_name        | Value |<br />
+----------------------+-------+<br />
| max_user_connections | 0     |<br />
+----------------------+-------+<br />
</code></p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2007/05/22/max_connections/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
