<?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; index</title>
	<atom:link href="http://www.dbnewz.com/tag/index/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>Tue, 31 Jan 2012 16:03:38 +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>Méthodes de suppression des index inutiles</title>
		<link>http://www.dbnewz.com/2011/09/05/methodes-de-suppression-des-index-inutiles/</link>
		<comments>http://www.dbnewz.com/2011/09/05/methodes-de-suppression-des-index-inutiles/#comments</comments>
		<pubDate>Mon, 05 Sep 2011 15:59:32 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[maatkit]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=916</guid>
		<description><![CDATA[Les vacances étant terminées, nous allons boucler notre tour de vue des index inutiles en voyant quels outils vont nous aider à découvrir les index qui peuvent être supprimés. Le dernier article présentait en effet des indications qui fonctionnent généralement bien mais qui ont l&#8217;inconvénient de demander beaucoup de travail manuel et de laisser de [...]]]></description>
			<content:encoded><![CDATA[<p>Les vacances étant terminées, nous allons boucler notre tour de vue des index inutiles en voyant quels outils vont nous aider à découvrir les index qui peuvent être supprimés. Le dernier article présentait en effet des indications qui fonctionnent généralement bien mais qui ont l&#8217;inconvénient de demander beaucoup de travail manuel et de laisser de côté tout un pan d&#8217;index qui peuvent être inutiles : ceux qui ne sont pas en doublon ni redondants, qui n&#8217;ont pas une cardinalité faible mais qui ne sont tout simplement pas utilisés par l&#8217;application.<br />
<span id="more-916"></span></p>
<h3>Idée générale</h3>
<p>Si vous avez bien lu l&#8217;article précédent, vous avez probablement remarqué que la principale difficulté est qu&#8217;il n&#8217;existe quasiment jamais de règle absolue permettant de savoir à coup sûr qu&#8217;un index est inutile (exception notable : les index en doublon repérés par mk-duplicate-key-checker et qui peuvent être supprimés dans 99% des cas sans problème). Finalement, la seule méthode qui semble marcher est la suivante :<br />
- Récupérer les requêtes exécutées sur le serveur<br />
- Regarder le plan d&#8217;exécution de ces requêtes pour voir les index utilisés<br />
- Comparer avec le schéma des tables pour en déduire les index non utilisés<br />
- Supprimer les index repérés<br />
La difficulté se situe surtout dans la 2ème étape (construire la liste des index utilisés à partir du plan d&#8217;exécution). Heureusement il existe au moins 2 manières, en fonction de votre version de MySQL, pour réussir cette étape.</p>
<h3>Index_statistics</h3>
<p>Les heureux utilisateurs de MariaDB ou Percona Server ont la chance d&#8217;avoir userstats v2, un patch exceptionnel développé à l&#8217;origine par Google. Ce patch ajoute un nombre incalculable de statistiques sur les utilisateurs, les tables mais aussi, ce qui nous intéresse ici, les index. Pour activer la fonctionnalité, il suffit de changer une variable :<br />
<code>mysql&gt; SET GLOBAL userstat_running = 1;</code></p>
<p>A partir de ce moment, la table <code>INDEX_STATISTICS</code> de la base <code>INFORMATION_SCHEMA</code> va se remplir. Les colonnes sont très simples à comprendre : <code>TABLE_SCHEMA</code>, <code>TABLE_NAME</code> et <code>INDEX_NAME</code> localisent l&#8217;index et <code>ROWS_READ</code> donne le nombre de lignes lues dans l&#8217;index. Evidemment, pour obtenir des statistiques significatives et fiables, il faut attendre suffisamment longtemps avant de consulter le contenu de la table. Une bonne question est de savoir ce que signifie &laquo;&nbsp;suffisamment longtemps&nbsp;&raquo; : disons qu&#8217;il faut que chaque requête potentielle de l&#8217;application ait pu être exécutée, en pensant bien aux requêtes rares telles que les requêtes faites dans des cronjobs par exemple.</p>
<p>Un petit exemple sur des données réelles ?<br />
<code><br />
mysql&gt; SELECT * FROM INFORMATION_SCHEMA.INDEX_STATISTICS WHERE TABLE_NAME='main';<br />
+--------------+------------+------------+-----------+<br />
| TABLE_SCHEMA | TABLE_NAME | INDEX_NAME               | ROWS_READ  |<br />
+--------------+------------+------------+-----------+<br />
| biz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  | main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      | user_id&nbsp;&nbsp;&nbsp;        |  231751890 |<br />
| biz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  | main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      | created&nbsp;&nbsp;&nbsp;                  |    4456658&nbsp;&nbsp; |<br />
| biz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| PRIMARY&nbsp;&nbsp;&nbsp;&nbsp;| 1748023896&nbsp;|<br />
| biz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      | modified_idx&nbsp;&nbsp;&nbsp; |  266636148 |<br />
| biz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  | main&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      | mobile_idx&nbsp;&nbsp;&nbsp; |        550 |<br />
+--------------+------------+---------------+------------+<br />
</code><br />
et voyons en parallèle le schéma de la table :<br />
<code><br />
mysql&gt; SHOW CREATE TABLE main\G<br />
********* 1. row *********<br />
Table: main<br />
Create Table: CREATE TABLE main (<br />
&nbsp;&nbsp;...<br />
&nbsp;&nbsp;PRIMARY KEY (main_id),<br />
&nbsp;&nbsp;KEY user_id (user_id),<br />
&nbsp;&nbsp;KEY created (created),<br />
&nbsp;&nbsp;KEY language_idx (language),<br />
&nbsp;&nbsp;KEY mobile_idx (mobile,deleted,web,published),<br />
&nbsp;&nbsp;KEY country_mobile_idx (country,mobile),<br />
&nbsp;&nbsp;KEY modified_idx (modified)<br />
) ENGINE=InnoDB;<br />
</code><br />
Qu&#8217;en déduit-on ? En premier lieu, que les index language_idx et country_mobile_idx ne sont jamais utilisés, ils ne servent donc à rien et peuvent être supprimés. Ensuite, en comparant les chiffres, on voit que le serveur a lu 1,7 milliards de lignes dans la clé primaire, 550 dans l&#8217;index mobile_idx : aucun doute mobile_idx ne sert à rien lui non plus !</p>
<p>Et voilà, de manière très simple, nous avons trouvé des index qui peuvent être supprimés.<br />
On peut même automatiser la découverte des index non utilisée <a href="http://www.mysqlperformanceblog.com/2008/09/12/unused-indexes-by-single-query/">avec une petite requête qui va bien</a>.</p>
<p>Quelles sont les limitations de cette méthode ? Très simple, autant les index qui ne sont jamais utilisés sont visibles immédiatement, autant on peut se poser la question de la pertinence de certains index. Et malheureusement, les chiffres donnés par la table INDEX_STATISTICS ne sont pas suffisants. Exemple : l&#8217;index created affiche un nombre de lignes lues environ 500 fois plus faibles que la clé primaire : cela signifie-t-il qu&#8217;il n&#8217;est pas très utile ou qu&#8217;au contraire il est très utile mais seulement sur certaines requêtes ?</p>
<h3>mk-index-usage</h3>
<p>Quand on ne dispose de userstats, la seule solution consiste à collecter toutes les requêtes pendant une période suffisamment longue, par exemple en mettant <code>long_query_time</code> à 0 pour que toutes les requêtes aboutissent dans le slow query log, et à se tourner vers Maatkit en espérant y trouver notre bonheur. Ca tombe bien, <code>mk-index-usage</code> est justement un outil lisant un fichier de log et délivrant de nombreuses informations intéressantes (en déterminant en particulier le plan d&#8217;exécution des requêtes).</p>
<p>Par défaut, <code>mk-index-usage</code> prend en entrée un fichier de log au format slow query log et affiche une liste de requêtes SQL pour supprimer les index inutiles :<br />
<code><br />
$ mk-index-usage slow.log<br />
slow.log:   5% 08:28 remain<br />
slow.log:  11% 07:20 remain<br />
slow.log:  14% 08:36 remain<br />
...<br />
slow.log:  93% 00:37 remain<br />
slow.log:  97% 00:12 remain<br />
</code><br />
ALTER TABLE `main`.`adfit` DROP KEY `status`; &#8212; type:non-unique<br />
&#8230;<br />
ALTER TABLE `main`.`biz` DROP KEY `created`, DROP KEY `modified_idx`; &#8212; type:non-unique<br />
</code><br />
Surprise ! Le script a bien identifié modified_idx comme inutile, mais pas mobile_idx. Et il a décidé que created pouvait être supprimé alors que nous avons vu que ce n'était pas forcément évident.</p>
<p>Comme d'habitude avec Maatkit, si vous êtes curieux et que vous lisez la documentation, vous verrez qu'il existe une multitude d'options permettant d'obtenir bien plus d'informations sur l'usage des index sur votre plate-forme. De plus, cette méthode est compatible avec toutes les versions de MySQL</p>
<h3>Conclusion</h3>
<p>Les deux méthodes présentées ici peuvent vous faire gagner beaucoup de temps en vous aidant à identifier les index candidats à la suppression. Bien entendu, comme souvent, il ne serait pas très prudent de faire confiance aveuglément à un script pour gérer votre plate-forme : votre travail consistera à vérifier s'il faut effectivement enlever les index qui sont susceptibles d'être supprimés. Normalement, ce type d'index ne devrait représenter qu'une petite portion des index de votre application, le gain de temps sera donc appréciable.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2011/09/05/methodes-de-suppression-des-index-inutiles/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Index candidats à la suppression</title>
		<link>http://www.dbnewz.com/2011/07/06/index-candidats-a-la-suppression/</link>
		<comments>http://www.dbnewz.com/2011/07/06/index-candidats-a-la-suppression/#comments</comments>
		<pubDate>Wed, 06 Jul 2011 13:10:14 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[maatkit]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=895</guid>
		<description><![CDATA[Après avoir constaté dans les articles précédents que les index inutiles causent des baisses de performances non négligeables, nous allons voir dans cet article qu&#8217;il n&#8217;est pas aussi simple qu&#8217;il y paraît de déterminer si un index est utile ou non, même si dans certains cas la réponse semble évidente.

A première vue, trois catégories d&#8217;index [...]]]></description>
			<content:encoded><![CDATA[<p>Après avoir constaté dans les articles précédents que les index inutiles causent des baisses de performances non négligeables, nous allons voir dans cet article qu&#8217;il n&#8217;est pas aussi simple qu&#8217;il y paraît de déterminer si un index est utile ou non, même si dans certains cas la réponse semble évidente.<br />
<span id="more-895"></span><br />
A première vue, trois catégories d&#8217;index sont bien placés pour être qualifiés d&#8217;inutiles : les index en doublon, les index redondants et les index à faible cardinalité. Regardons chaque catégorie en détail.</p>
<h3>Index en doublon</h3>
<p>Les index en doublon sont simplement ceux qui sont définis plusieurs fois. Un exemple simple pour commencer :<br />
<code>CREATE TABLE t (<br />
&nbsp;&nbsp;id int(11) DEFAULT NULL,<br />
&nbsp;&nbsp;KEY a (id),<br />
&nbsp;&nbsp;KEY b (id)<br />
);</code></p>
<p>Notons que MySQL n&#8217;empêche en aucun cas ce genre de définition erronée.</p>
<p>Un autre exemple, un peu plus compliqué :<br />
<code>CREATE TABLE u (<br />
&nbsp;&nbsp;id int(11) NOT NULL DEFAULT '0',<br />
&nbsp;&nbsp;UNIQUE KEY (id),<br />
&nbsp;&nbsp;KEY b (id)<br />
);</code></p>
<p>La logique était : je crée une contrainte d&#8217;unicité sur la colonne id, puis un index pour accélérer les requêtes. Sauf qu&#8217;avec MySQL, toutes les contraintes sont vérifiées avec des index, ce qui signifie que la création de la contrainte d&#8217;unicité crée implicitement un index (idem pour <code>PRIMARY KEY</code> et <code>FOREIGN KEY</code>).</p>
<p>Un dernier exemple :<br />
<code>CREATE TABLE v (<br />
&nbsp;&nbsp;id int(11) NOT NULL DEFAULT '0',<br />
&nbsp;&nbsp;col varchar(10) DEFAULT NULL,<br />
&nbsp;&nbsp;PRIMARY KEY (id),<br />
&nbsp;&nbsp;KEY a (col),<br />
&nbsp;&nbsp;FULLTEXT KEY b (col)<br />
) ENGINE=MyISAM;</code></p>
<p>Ici, nous avons bien deux index sur col, mais ce ne sont pas des index de même type, ils ne sont donc pas en doublon. Attention aux excès de zèle !</p>
<p>Les index en doublon n&#8217;apportent rien, il est donc toujours utile de pouvoir les repérer pour ensuite les supprimer. Un bon outil pour ce travail est <code>mk-duplicate-key-checker</code> de Maatkit. L&#8217;outil est très simple d&#8217;usage et la documentation complète.</p>
<p>Par exemple, pour vérifier si notre table u (dans la base test) contient des index en doublon, on peut utiliser la ligne suivante :<br />
<code>$ mk-duplicate-key-checker h=localhost,u=root,D=test -t u<br />
# ##################################<br />
# test.u<br />
# ##################################</code></p>
<p><code># b is a duplicate of PRIMARY<br />
# Key definitions:<br />
#   KEY `b` (`id`)<br />
#   PRIMARY KEY (`id`),<br />
# Column types:<br />
#	  `id` int(11) not null default '0'<br />
# To remove this duplicate index, execute:<br />
ALTER TABLE `test`.`u` DROP INDEX `b`;</code></p>
<p><code># ##################################<br />
# Summary of indexes<br />
# ##################################</code></p>
<p><code># Size Duplicate Indexes   0<br />
# Total Duplicate Indexes  1<br />
# Total Indexes            2</code></p>
<p>L&#8217;outil suggère avec raison que l&#8217;index b est en doublon de la clé primaire et propose sous la forme d&#8217;un <code>ALTER TABLE</code> une requête pour supprimer cet index.</p>
<p>La même ligne de commande, en version longue :<br />
<code>$ mk-duplicate-key-checker --host localhost --user root --databases test --tables t</code></p>
<p>Il est bien sûr possible d&#8217;examiner en une seule passe toutes les tables de la base test :<br />
<code>$ mk-duplicate-key-checker --host localhost --user root --databases test<br />
# ##################################<br />
# test.t<br />
# ##################################</code></p>
<p><code># a is a duplicate of b<br />
# Key definitions:<br />
#   KEY `a` (`id`),<br />
#   KEY `b` (`id`)<br />
# Column types:<br />
#	  `id` int(11) default null<br />
# To remove this duplicate index, execute:<br />
ALTER TABLE `test`.`t` DROP INDEX `a`;</code></p>
<p><code># ##################################<br />
# test.u<br />
# ##################################</code></p>
<p><code># b is a duplicate of id<br />
# Key definitions:<br />
#   KEY `b` (`id`)<br />
#   UNIQUE KEY `id` (`id`),<br />
# Column types:<br />
#	  `id` int(11) not null default '0'<br />
# To remove this duplicate index, execute:<br />
ALTER TABLE `test`.`u` DROP INDEX `b`;</code></p>
<p><code># ##################################<br />
# Summary of indexes<br />
# ##################################</code></p>
<p><code># Size Duplicate Indexes   0<br />
# Total Duplicate Indexes  2<br />
# Total Indexes            7</code></p>
<p>Parfait, <code>mk-duplicate-key-checker</code> a bien identifié les doublons et ne s&#8217;est pas fait piéger par l&#8217;index FULLTEXT de la table v.</p>
<h3>Index redondants</h3>
<p>Pour les index de type BTREE (c&#8217;est-à-dire tous les index sauf les index HASH de Memory et les index FULLTEXT de MyISAM), l&#8217;optimiseur de requêtes de MySQL est capable d&#8217;utiliser un préfixe gauche d&#8217;un index composite. Tous les index portant sur des préfixes gauche d&#8217;un index composite ne sont donc théoriquement pas utiles.</p>
<p>Pas clair ? Regardez la table suivante :<br />
<code>CREATE TABLE w (<br />
&nbsp;&nbsp;id int(11) NOT NULL DEFAULT '0',<br />
&nbsp;&nbsp;col varchar(10) DEFAULT NULL,<br />
&nbsp;&nbsp;col2 varchar(10) DEFAULT NULL,<br />
&nbsp;&nbsp;KEY id (id),<br />
&nbsp;&nbsp;KEY id_col (id,col)<br />
);</code></p>
<p>Et considérez la requête suivante :<br />
<code>SELECT * FROM w WHERE id = 1</code></p>
<p><code>EXPLAIN</code> nous indique que l&#8217;index sur id est utilisé :<br />
<code>mysql&gt; EXPLAIN SELECT * FROM w WHERE id = 1\G<br />
********* 1. row ********<br />
&nbsp;&nbsp;&nbsp;id: 1<br />
&nbsp;&nbsp;&nbsp;select_type: SIMPLE<br />
&nbsp;&nbsp;&nbsp;table: w<br />
&nbsp;&nbsp;&nbsp;type: ref<br />
&nbsp;&nbsp;&nbsp;possible_keys: id,id_col<br />
&nbsp;&nbsp;&nbsp;key: id<br />
&nbsp;&nbsp;&nbsp;key_len: 4<br />
&nbsp;&nbsp;&nbsp;ref: const<br />
&nbsp;&nbsp;&nbsp;rows: 1<br />
&nbsp;&nbsp;&nbsp;Extra:<br />
</code><br />
Mais si on demande à l&#8217;optimiseur de ne pas considérer l&#8217;index sur id, on voit que la requête va s&#8217;exécuter de manière aussi efficace en utilisant l&#8217;index sur (id,col) :</p>
<p><code>mysql&gt; EXPLAIN SELECT * FROM w IGNORE INDEX(id) WHERE id = 1\G<br />
******** 1. row ********<br />
&nbsp;&nbsp;&nbsp;id: 1<br />
&nbsp;&nbsp;&nbsp;select_type: SIMPLE<br />
&nbsp;&nbsp;&nbsp;table: w<br />
&nbsp;&nbsp;&nbsp;type: ref<br />
&nbsp;&nbsp;&nbsp;possible_keys: id_col<br />
&nbsp;&nbsp;&nbsp;key: id_col<br />
&nbsp;&nbsp;&nbsp;key_len: 4<br />
&nbsp;&nbsp;&nbsp;ref: const<br />
&nbsp;&nbsp;&nbsp;rows: 1<br />
&nbsp;&nbsp;&nbsp;Extra:<br />
</code><br />
On peut donc éliminer sans problème l&#8217;index sur id.</p>
<p>Faut-il donc systématiquement supprimer les index redondants ? Eh bien non, il convient d&#8217;apporter quelques nuances. Disons que la majeure partie du temps, un index redondant est inutile et peut être supprimé, mais qu&#8217;il existe des situations où il faudra conserver tous les index.</p>
<p>Par exemple, ce sera souvent le cas quand les colonnes indexées sont de grande taille (que ceux qui n&#8217;utilisent jamais de <code>VARCHAR(255)</code> lèvent la main <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> ). Pour avoir un cas concret de ce type de situation, vous pouvez lire <a href="http://www.mysqlperformanceblog.com/2007/08/28/redundant-index-is-not-always-bad/">cet article</a>.</p>
<p>Comment savoir si un index redondant est utile ou non ? La seule solution utilisable est bien souvent de faire des tests.</p>
<p>Que dit <code>mk-duplicate-key-checker</code> sur les index redondants ?</p>
<p><code>$ mk-duplicate-key-checker h=localhost,u=root,D=test -t w<br />
# ##################################<br />
# test.w<br />
# ##################################</code></p>
<p><code># id is a left-prefix of id_col<br />
# Key definitions:<br />
#   KEY `id` (`id`),<br />
#   KEY `id_col` (`id`,`col`)<br />
# Column types:<br />
#	  `id` int(11) not null default '0'<br />
#	  `col` varchar(10) default null<br />
# To remove this duplicate index, execute:<br />
ALTER TABLE `test`.`w` DROP INDEX `id`;</code></p>
<p><code># ##################################<br />
# Summary of indexes<br />
# ##################################</code></p>
<p><code># Size Duplicate Indexes   16<br />
# Total Duplicate Indexes  1<br />
# Total Indexes            2</code></p>
<p>L&#8217;outil considère toujours les index redondants comme candidats à la suppression. A vous de voir s&#8217;il vaux mieux le conserver ou non.</p>
<h3>Index à faible cardinalité</h3>
<p>Arnaud a déjà parlé en détail de la notion de<a href="http://www.dbnewz.com/2008/09/05/cardinalite-selectivite-et-distributivite-dun-index-mysql-quel-impact-sur-le-plan-dexecution/"> cardinalité d&#8217;un index</a>. En résumé, la cardinalité est le nombre de valeurs uniques de l&#8217;index. Plus elle est élevée, plus l&#8217;index sera efficace dans son filtrage.</p>
<p>Si la cardinalité est faible, l&#8217;index perd de son intérêt. D&#8217;ailleurs, l&#8217;optimiseur de requêtes n&#8217;utilisera pas l&#8217;index s&#8217;il estime qu&#8217;il n&#8217;est pas utile. Voici donc de bons candidats à la suppression ! Cependant là encore, il convient de nuancer.</p>
<p>Imaginez un site de e-commerce avec une table contenant les commandes. Cette table va contenir un champ indiquant le statut des commandes, avec par exemple les valeurs &#8216;archivée&#8217; et &#8216;en cours&#8217;. Au bout de quelque temps, il est certain que presque toutes les lignes de la table auront le statut &#8216;archivée&#8217;. Cependant, si vous écrivez une requête qui porte sur les commandes &#8216;en cours&#8217;, un index sur la colonne statut sera sûrement utile, même si sa cardinalité sera ridicule (2 en l&#8217;occurence).</p>
<p>Ce qui joue ici n&#8217;est pas seulement la cardinalité de l&#8217;index, mais aussi la distribution des valeurs et surtout les requêtes que vous faites. Si avec la même table des commandes, votre voisin ne s&#8217;intéresse qu&#8217;aux commandes archivées, l&#8217;index n&#8217;a plus aucun intérêt. C&#8217;est d&#8217;ailleurs une situation que l&#8217;on rencontre fréquemment : les requêtes que l&#8217;on peut faire en mode transactionnel ou en mode décisionnel sont radicalement différentes, exigeant souvent d&#8217;avoir des installations dédiées avec une indexation différente.</p>
<h3>Conclusion</h3>
<p>Cet article a montré que le repérage des index inutiles est loin d&#8217;être évident. Mis à part les index en doublon qu&#8217;on peut repérer et éliminer facilement, il est nécessaire d&#8217;avoir une bonne connaissance des requêtes qui sont exécutées afin de déterminer si un index est inutile. Le prochain article va se concentrer sur des méthodes plus systématiques d&#8217;identification des index inutiles.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2011/07/06/index-candidats-a-la-suppression/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Le coût des index inutiles &#8211; 2nde partie</title>
		<link>http://www.dbnewz.com/2011/05/27/le-cout-des-index-inutiles-2nde-partie/</link>
		<comments>http://www.dbnewz.com/2011/05/27/le-cout-des-index-inutiles-2nde-partie/#comments</comments>
		<pubDate>Fri, 27 May 2011 15:50:34 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=885</guid>
		<description><![CDATA[Dans l&#8217;article précédent, nous nous étions demandés quelle était la dégradation des performances en écriture quand on ajoute des index. On peut élargir la réflexion en se penchant sur les conditions qui améliorent ou diminuent la vitesse d&#8217;écriture dans une table.

Avant de commencer de nouvelles expérimentations, rappelons les conditions du test. La table utilisée a [...]]]></description>
			<content:encoded><![CDATA[<p>Dans l&#8217;article précédent, nous nous étions demandés quelle était la dégradation des performances en écriture quand on ajoute des index. On peut élargir la réflexion en se penchant sur les conditions qui améliorent ou diminuent la vitesse d&#8217;écriture dans une table.<br />
<span id="more-885"></span><br />
Avant de commencer de nouvelles expérimentations, rappelons les conditions du test. La table utilisée a la structure suivante :<br />
<code><br />
CREATE TABLE (<br />
&nbsp;&nbsp;id int(11) NOT NULL AUTO_INCREMENT,<br />
&nbsp;&nbsp;col_a varchar(30) NOT NULL DEFAULT '',<br />
&nbsp;&nbsp;col_b varchar(30) NOT NULL DEFAULT '',<br />
&nbsp;&nbsp;col_c varchar(30) NOT NULL DEFAULT '',<br />
PRIMARY KEY (id)<br />
);<br />
</code><br />
et nous cherchons à insérer un millions de lignes à l&#8217;aide de la commande <code>LOAD DATA INFILE</code>.</p>
<p>Rappelons aussi que pour améliorer les performances en écriture dans l&#8217;absolu, il existe d&#8217;autres pistes que nous n&#8217;explorerons pas dans cet article :</p>
<ul>
<li>si la table utilise InnoDB ou XtraDB, modifier certaines variables de configuration (<code>innodb_log_file_size</code>,<code>innodb_flush_log_at_trx_commit</code>,<br />
<code>innodb_adaptive_flushing</code>, etc)</li>
<li>si la table utilise MyISAM, modifier d&#8217;autres variables de configuration (<code>myisam_sort_buffer_size</code>, <code>bulk_insert_buffer_size</code>)</li>
<li>faire des insertions multithreadées</li>
<li>désactiver les vérifications de clés étrangères ou de clés uniques</li>
<li>&#8230;</li>
</ul>
<p>Pour commencer nos test, examinons le comportement de MyISAM. Le paramètre clé est la taille du cache d&#8217;index (<code>key_buffer_size</code>), nous faisons donc un essai avec un cache d&#8217;index suffisamment grand pour pouvoir contenir tous les index et un essai avec un cache trop petit.</p>
<p>MyISAM, key_buffer_size grand :<br />
Seulement la clé primaire : 4s<br />
Clé primaire + index sur col_a : 6,6s<br />
Clé primaire + index sur col_b : 9,3s<br />
Clé primaire + index sur (col_a,col_b) : 6,8s</p>
<p>MyISAM, key_buffer_size petit :<br />
Seulement la clé primaire : 4s<br />
Clé primaire + index sur col_a : 6,8s<br />
Clé primaire + index sur col_b : 9,3s<br />
Clé primaire + index sur (col_a,col_b) : 6,8s</p>
<p>A comparer avec les résultats obtenus la dernière fois avec InnoDB :<br />
Seulement la clé primaire : 15s<br />
Clé primaire + index sur col_a : 28,5s<br />
Clé primaire + index sur col_a + index sur col_b : 44,5s<br />
Clé primaire + index sur (col_a,col_b) : 36,3s</p>
<p>Très intéressant ! MyISAM se révèle plus performant qu&#8217;InnoDB, et de loin.</p>
<p>Attention, ne faisons pas de généralités sur la vitesse de MyISAM ! Ce test se déroule sur un serveur hors production qui n&#8217;exécute que les <code>LOAD DATA INFILE</code>. Il est certain que l&#8217;écart entre InnoDB et MyISAM ne serait pas aussi important si les insertions étaient multithreadées (verrouillage de toute la table pour MyISAM) et si le serveur recevait également des requêtes en lecture (les écritures seraient peut-être rapides mais les lectures souffriraient sans doute).</p>
<p>De plus, on voit que la taille du cache d&#8217;index n&#8217;influence quasiment pas le temps de chargement, ce qui est plutôt inattendu. Une explication possible serait que les données et index ne soient pas réellement écrits sur le disque, mais restent dans le cache de l&#8217;OS. L&#8217;hypothèse semble raisonnable puisqu&#8217;un coup d&#8217;oeil sur iostat pendant les <code>LOAD DATA INFILE</code> montre que les écritures sur le disque sont nettement plus nombreuses quand la table est en InnoDB.</p>
<p>Faisons donc un autre test avec MyISAM, pour lequel le cache d&#8217;index est petit et la quantité de mémoire disponible pour l&#8217;OS est trop faible pour pouvoir contenir les données et les index :<br />
Seulement la clé primaire : 6,8s<br />
Clé primaire + index sur col_a : 14s<br />
Clé primaire + index sur col_b : 24s<br />
Clé primaire + index sur (col_a,col_b) : 18s</p>
<p>Les performances sont bien moins bonnes qu&#8217;avant, mais restent encore supérieures à celles d&#8217;InnoDB !</p>
<p>Concentrons nous maintenant sur InnoDB et voyons l&#8217;influence du type de clé primaire sur le temps de chargement. Pour simplifier, mettons-nous toujours dans le cas où le buffer_pool est suffisamment grand pour contenir données et index.</p>
<p>Dans un premier temps, optons pour une clé primaire composite (id,col_c) :<br />
Seulement la clé primaire : 17,8s<br />
Clé primaire + index sur col_a : 40s<br />
Clé primaire + index sur col_b : 72s<br />
Clé primaire + index sur (col_a,col_b) : 51,7s</p>
<p>On voit qu&#8217;avoir une clé primaire composite a un impact non négligeable lorsqu&#8217;il n&#8217;existe pas d&#8217;autres index (20% de perte de performance) et que cet impact négatif est encore plus important quand on ajoute des index. Ce résultat était attendu, puisqu&#8217;avec InnoDB, chaque index secondaire contient la clé primaire : plus la clé primaire est volumineuse, plus les index secondaires sont volumineux et longs à mettre à jour.</p>
<p>Dans un second temps, supprimons la clé primaire :<br />
Seulement la clé primaire : 14,6s<br />
Clé primaire + index sur col_a : 29,2s<br />
Clé primaire + index sur col_b : 45,5s<br />
Clé primaire + index sur (col_a,col_b) : 38,1s</p>
<p>Les temps obtenus sont proches du cas initial où id est la clé primaire. Comment expliquer ce résultat ? InnoDB a besoin d&#8217;une clé primaire pour toutes ses tables. Si vous ne la définissez pas vous-même et qu&#8217;il n&#8217;existe pas d&#8217;index unique n&#8217;ayant aucune valeur NULL, InnoDB créé une clé primaire cachée sous la forme d&#8217;un entier. On se retrouve donc avec une structure du type suivant :<br />
<code><br />
CREATE TABLE (<br />
&nbsp;&nbsp;hidden_id int NOT NULL AUTO_INCREMENT,<br />
&nbsp;&nbsp;id int(11) NOT NULL,<br />
&nbsp;&nbsp;col_a varchar(30) NOT NULL DEFAULT '',<br />
&nbsp;&nbsp;col_b varchar(30) NOT NULL DEFAULT '',<br />
&nbsp;&nbsp;col_c varchar(30) NOT NULL DEFAULT '',<br />
&nbsp;&nbsp;PRIMARY KEY (hidden_id)<br />
);<br />
</code><br />
On se retrouve avec la même structure qu&#8217;initialement, mais avec une colonne de plus, donc avec une volumétrie un peu plus importante, ce qui explique les résultats similaires mais légèrement inférieurs.</p>
<p>Que pouvons-nous conclure de tous ces test ? Tout d&#8217;abord, que le moteur de stockage a une grande influence sur les résultats. Si vous avez le choix du moteur, des tests s&#8217;imposent pour déterminer lequel est le meilleur, en ayant soin de prendre en compte la charge réelle de l&#8217;application (connexions concurrentes, lectures et écritures). Ensuite, que pour InnoDB, le choix de la clé primaire est critique : la clé primaire doit être la plus compacte possible, sous peine de performances très dégradées. Enfin, que pour MyISAM comme pour InnoDB, il vaut mieux limiter le nombre d&#8217;index qui ralentissent considérablement toutes les écritures.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2011/05/27/le-cout-des-index-inutiles-2nde-partie/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Le coût des index inutiles</title>
		<link>http://www.dbnewz.com/2011/05/16/le-cout-des-index-inutiles/</link>
		<comments>http://www.dbnewz.com/2011/05/16/le-cout-des-index-inutiles/#comments</comments>
		<pubDate>Mon, 16 May 2011 14:48:02 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=880</guid>
		<description><![CDATA[On vous a tout le temps dit et redit que les index étaient indispensables pour les performances en lecture d&#8217;une base de données et vous avez eu droit à des exemples spectaculaires où les temps de réponses sont divisés par 10 000 ou par 1 000 000 rien qu&#8217;en ajoutant un index judicieux. Bien. On [...]]]></description>
			<content:encoded><![CDATA[<p>On vous a tout le temps dit et redit que les index étaient indispensables pour les performances en lecture d&#8217;une base de données et vous avez eu droit à des exemples spectaculaires où les temps de réponses sont divisés par 10 000 ou par 1 000 000 rien qu&#8217;en ajoutant un index judicieux. Bien. On vous a également prévenu que chaque index posé dégrade les écritures et qu&#8217;il ne faut donc pas en abuser. Mais vous a-t-on déjà montré quel type de dégradation en écriture on peut attendre quand on ajoute un index ? C&#8217;est ce dont nous allons parler dans cet article.<br />
<span id="more-880"></span></p>
<p>Prenons une table InnoDB toute simple :</p>
<p><code><br />
CREATE TABLE  (<br />
id int(11) NOT NULL AUTO_INCREMENT,<br />
col_a varchar(30) NOT NULL DEFAULT '',<br />
col_b varchar(30) NOT NULL DEFAULT '',<br />
col_c varchar(30) NOT NULL DEFAULT '',<br />
PRIMARY KEY (id)<br />
) ENGINE=InnoDB;<br />
</code></p>
<p>et testons l&#8217;influence des index sur le temps de chargement d&#8217;un million de lignes au moyen d&#8217;un <code>LOAD DATA INFILE</code>.</p>
<p>Cas 1, données et index tiennent en mémoire (innodb_buffer_pool suffisamment grand) :<br />
Seulement la clé primaire : 15s<br />
Clé primaire + index sur col_a : 28,5s<br />
Clé primaire + index sur col_a + index sur col_b : 44,5s<br />
Clé primaire + index sur (col_a,col_b) : 36,3s</p>
<p>Cas 2, données et index ne tiennent plus en mémoire :<br />
Seulement la clé primaire : 22,5s<br />
Clé primaire + index sur col_a : 370s<br />
Clé primaire + index sur col_a + index sur col_b : 1144s<br />
Clé primaire + index sur (col_a,col_b) : 716s</p>
<p>Quelles conclusions pouvons-nous faire de ces tests ? Tout d&#8217;abord, les insertions sont nettement plus rapides quand la table tient en mémoire. Ce n&#8217;est pas une surprise, tout va toujours plus vite lorsqu&#8217;on travaille en mémoire que lorsqu&#8217;on travaille sur le disque. Les différences sont cependant spectaculaires, puisque le temps de chargement dans le cas 2 est x1,5 dans le meilleur cas et x25 dans le pire des cas.</p>
<p>On remarque également qu&#8217;à chaque index ajouté, le temps de chargement augmente de manière très importante (x3 avec 2 index pour le cas 1 et x50 pour le cas 2), même lorsque la table tient en mémoire.</p>
<p>La conclusion de ce petit test est donc la suivante : les index inutiles dégradent très fortement les performances en écriture, et cela même si les données tiennent intégralement en mémoire. Si vous avez des problèmes de capacité d&#8217;écriture sur vos serveurs MySQL, par exemple avec des slaves qui ont du mal à répliquer les requêtes du master sans prendre de retard, une bonne piste consiste à partir à la chasse aux index inutiles. Reste une dernière question : comment déterminer les index inutiles ? C&#8217;est loin d&#8217;être évident, et nous en reparlerons en détail dans un prochain article.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2011/05/16/le-cout-des-index-inutiles/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Instrumentation et performance</title>
		<link>http://www.dbnewz.com/2011/01/24/instrumentation-et-performance/</link>
		<comments>http://www.dbnewz.com/2011/01/24/instrumentation-et-performance/#comments</comments>
		<pubDate>Mon, 24 Jan 2011 16:52:21 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[maatkit]]></category>
		<category><![CDATA[performance]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=828</guid>
		<description><![CDATA[Instrumenter son application correctement représente un pas important dans la recherche des performances optimales. De bons outils permettent également de gagner du temps, qui est toujours précis. Cet article va vous donner un exemple de la valeur ajoutée que peut procurer un bon outil : le simple fait d&#8217;obtenir un rapport précis sur un problème [...]]]></description>
			<content:encoded><![CDATA[<p>Instrumenter son application correctement représente un pas important dans la recherche des performances optimales. De bons outils permettent également de gagner du temps, qui est toujours précis. Cet article va vous donner un exemple de la valeur ajoutée que peut procurer un bon outil : le simple fait d&#8217;obtenir un rapport précis sur un problème rencontré permet de résoudre en 5 minutes un gros problème de performance qui ne trouvait pas de solution depuis des semaines.<span id="more-828"></span></p>
<p>L&#8217;application en question dispose d&#8217;une table user permettant (surprise !!) de stocker les informations sur les utilisateurs. Cette table contient environ 30 millions de lignes et a l&#8217;allure suivante :</p>
<p><code><br />
CREATE TABLE user (<br />
  &nbsp;&nbsp;user_id int(11) NOT NULL AUTO_INCREMENT,<br />
  &nbsp;&nbsp;login varchar(30) NOT NULL DEFAULT '',<br />
  &nbsp;&nbsp;name varchar(50) NOT NULL DEFAULT '',<br />
  &nbsp;&nbsp;col_a varchar(30) NOT NULL DEFAULT '',<br />
  &nbsp;&nbsp;col_b varchar(30) NOT NULL DEFAULT '',<br />
  &nbsp;&nbsp;col_c varchar(30) NOT NULL DEFAULT '',<br />
  &nbsp;&nbsp;PRIMARY KEY (user_id),<br />
  &nbsp;&nbsp;UNIQUE KEY login (login)<br />
) ENGINE=InnoDB;<br />
</code><br />
Notons que la base ne tient pas en mémoire.</p>
<p>Un rapport quotidien basé sur <code>mysqldumpslow</code> envoyait les requêtes lentes de l&#8217;application. Parmi ces requêtes, on en trouvait une très étrange, prenant plus de 30s :<br />
<code><br />
Count: 195  Time=33.05s (6445s)  Lock=0.00s (0s)  Rows=129903.9 (25331256), user[db]@81hosts<br />
  SELECT user_id FROM user WHERE login = N<br />
</code></p>
<p>Pourquoi est-il étrange que cette requête soit aussi lente ? Tout simplement parce qu&#8217;il existe un index sur la colonne login et que la table étant en InnoDB, on peut profiter de l&#8217;index cluster pour couvrir la requête avec l&#8217;index login. Si la dernière phrase ne vous a paru très claire, regardez donc <a href="http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/">cet article</a> et <a href="http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/">celui-là</a> également.</p>
<p>On peut confirmer facilement la validité de notre hypothèse sur le plan d&#8217;exécution avec <code>EXPLAIN</code> en prenant un login au hasard :<br />
<code><br />
mysql&gt; EXPLAIN SELECT user_id FROM test.user WHERE login="aqkme123456"\G<br />
***** 1. row *****<br />
          &nbsp;&nbsp; id: 1<br />
 &nbsp;&nbsp; select_type: SIMPLE<br />
     &nbsp;&nbsp;   table: user<br />
     &nbsp;&nbsp;    type: const<br />
&nbsp;&nbsp;&nbsp;possible_keys: login<br />
   &nbsp;&nbsp;       key: login<br />
     &nbsp;&nbsp; key_len: 32<br />
    &nbsp;&nbsp;      ref: const<br />
    &nbsp;&nbsp;     rows: 1<br />
  &nbsp;&nbsp;      Extra: Using index<br />
</code></p>
<p>Alors certes, la base ne tenant pas en mémoire, on ne sait pas si l&#8217;index login est en mémoire ou pas, mais dans la mesure où on accède directement à la bonne ligne, il n&#8217;y a vraiment aucune raison pour que la requête prenne plus de 30 secondes. Et en effet, les différents essais que j&#8217;ai pu faire avec des vraies valeurs donnent toujours un résultat instantané.</p>
<p>Poussons donc la réflexion : le problème de <code>mysqldumpslow</code>, c&#8217;est que l&#8217;outil ne donne qu&#8217;un exemple avec des valeurs abstraites. Il nous faudrait ici un exemple avec une valeur qui a provoqué une requête de 30s. Un autre outil permettrait-il d&#8217;avoir un tel exemple ? Oui, avec <code>mk-query-digest</code> de Maatkit par exemple.</p>
<p>Installons donc <code><a href="http://www.dbnewz.com/2010/09/22/outils-danalyse-de-requetes-lentes-mk-query-digest/">mk-query-digest</a></code>, demandons lui de générer un rapport sur les logs de la veille, et regardons l&#8217;exemple réel donné par l&#8217;outil :</p>
<p><code><br />
SELECT user_id FROM user WHERE login = 123456<br />
</code></p>
<p>Et voilà, soudainement tout est devenu limpide !! Non ? Mais si, regardez bien, la condition <code>WHERE</code> porte sur la colonne login de type <code>VARCHAR</code> et on passe un entier ! Ce qui change tout car MySQL convertit login en entier, ce qui empêche l&#8217;utilisation de l&#8217;index (rappelez-vous, MySQL ne connaît pas les<a href="http://www.dbnewz.com/2009/04/01/le-trigger-au-secours-des-function-based-index-fbi/"> function-based index</a>) et conduit donc à un parcours complet de la table.</p>
<p>En réalité, cette analyse n&#8217;est pas tout à fait exacte car l&#8217;index sur login est couvrant pour la requête : il est donc possible de faire un parcours complet de la table non pas en parcourant la clé primaire mais en parcourant l&#8217;index sur login. Quelle stratégie préfère choisir MySQL ?<br />
<code><br />
mysql&gt; EXPLAIN SELECT user_id FROM test.user WHERE login=123456\G<br />
***** 1. row *****<br />
 &nbsp;&nbsp;          id: 1<br />
 &nbsp;&nbsp; select_type: SIMPLE<br />
 &nbsp;&nbsp;       table: user<br />
 &nbsp;&nbsp;        type: index<br />
&nbsp;&nbsp;&nbsp;possible_keys: login<br />
&nbsp;&nbsp;          key: login<br />
&nbsp;&nbsp;      key_len: 32<br />
     &nbsp;&nbsp;     ref: NULL<br />
    &nbsp;&nbsp;     rows: 33153173<br />
    &nbsp;&nbsp;    Extra: Using where; Using index<br />
</code></p>
<p>MySQL choisit d&#8217;utiliser l&#8217;index sur login. Etait-ce la meilleure idée, autrement dit, était-il plus rapide de scanner la table avec l&#8217;index sur login (ce qui conduit à de nombreuses lectures aléatoires) plutôt que de scanner la table séquentiellement ? Pour y répondre, le seul moyen est de faire un test avec les 2 méthodes, en utilisant le modificateur <code>IGNORE INDEX(login)</code> dans le second cas pour forcer le scan de la table. Et dans ce cas, les tests montrent que le scan avec l&#8217;index sur login est plus rapide.</p>
<p>Dernier point, qui montre à la fois que MySQL convertit bien le champ login en entier et que la requête est complètement erronée : si je joue la requête <code>SELECT user_id FROM user WHERE login = 123456</code> sur mes données de production, j&#8217;obtiens non pas un résultat mais plus de 2000 ! Pourquoi un tel résultat alors que le login est censé être unique ? Tout simplement parce qu&#8217;en convertissant le champ login en entier, on perd de l&#8217;information : toutes les chaînes de caractères commençant par &laquo;&nbsp;123456&#8243; seront converties vers l&#8217;entier 123456 :<br />
<code><br />
mysql&gt; SELECT CONVERT("123456toto",UNSIGNED);<br />
+--------------------------------+<br />
| CONVERT("123456toto",UNSIGNED) |<br />
+--------------------------------+<br />
|                         123456 |<br />
+--------------------------------+<br />
</code><br />
<code><br />
mysql&gt; SHOW WARNINGS;<br />
+---------+------+-------------------------------------------------+<br />
| Level   | Code | Message                                         |<br />
+---------+------+-------------------------------------------------+<br />
| Warning | 1292 | Truncated incorrect INTEGER value: '123456toto' |<br />
+---------+------+-------------------------------------------------+<br />
</code></p>
<p>La correction du problème a finalement été très simple : il a suffi au développeur de forcer l&#8217;ajout des quotes dans la requête.</p>
<p>Morale de l&#8217;histoire : utiliser de bons outils peut vous faire gagner un temps précieux ! Notez que si j&#8217;avais été très méticuleux, j&#8217;aurais pu arriver au même résultat avec <code>mysqldumpslow</code>, puisque la ligne qui m&#8217;était indiquée donnait <code>WHERE login = N</code>, où N représente la valeur abstraite d&#8217;un entier alors qu&#8217;il aurait fallu avoir <code>WHERE login = S</code>, S représentant une chaîne. Mais c&#8217;était plus facile avec <code>mk-query-digest</code>, non ?</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2011/01/24/instrumentation-et-performance/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>le trigger au secours des function-based index (FBI)</title>
		<link>http://www.dbnewz.com/2009/04/01/le-trigger-au-secours-des-function-based-index-fbi/</link>
		<comments>http://www.dbnewz.com/2009/04/01/le-trigger-au-secours-des-function-based-index-fbi/#comments</comments>
		<pubDate>Wed, 01 Apr 2009 06:36:55 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[5.0]]></category>
		<category><![CDATA[5.1]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=228</guid>
		<description><![CDATA[Constat : les FBI (function-based index) ne sont pas disponibles en MySQL.
Comment faire dans ce cas pour obtenir par exemple l&#8217;équivalent de l&#8217;index suivant ?
CREATE INDEX idx_str_len ON my_table (length(str));
Une des solutions les plus simples consiste à utiliser un trigger qui assurera la mise à jour d&#8217;une colonne supplémentaire que l&#8217;on créera dans la table [...]]]></description>
			<content:encoded><![CDATA[<p>Constat : les FBI (function-based index) ne sont pas disponibles en MySQL.</p>
<p>Comment faire dans ce cas pour obtenir par exemple l&#8217;équivalent de l&#8217;index suivant ?</p>
<p><code>CREATE INDEX idx_str_len ON my_table (<strong>length</strong>(str));</code></p>
<p>Une des solutions les plus simples consiste à utiliser un <a href="http://dev.mysql.com/doc/refman/5.1/en/create-trigger.html" target="_blank">trigger</a> qui assurera la mise à jour d&#8217;une colonne supplémentaire que l&#8217;on créera dans la table cible.</p>
<p>Partons de la table suivante :</p>
<p><code>CREATE TABLE `t` (<br />
`id` mediumint(8) unsigned NOT NULL auto_increment,<br />
`date` timestamp NOT NULL,<br />
`str` varchar(100) NOT NULL default '0',<br />
PRIMARY KEY  (`id`)<br />
) ENGINE=MyISAM;</code></p>
<p>Le but est de pouvoir obtenir une réponse rapide à la requête suivante :</p>
<p>select sql_no_cache count(*) from t where length(str) between 20 and 70;</p>
<p>Afin de simuler un jeu d&#8217;essai similaire à ma table de production, j&#8217;alimente ma table de test grâce à <a href="http://www.dbnewz.com/2008/08/19/generer-un-jeu-de-donnees-shell-mysqlslap-et-procedure-stockee/" target="_blank">une procédure stockée</a> déjà évoquée sur dbnewz.</p>
<p>Je la modifie ici afin d&#8217;obtenir une longueur aléatoire pour ma chaîne de caractère &laquo;&nbsp;str&nbsp;&raquo; :</p>
<p><code>delimiter //<br />
CREATE PROCEDURE fill_table(nb_rows INT)<br />
BEGIN<br />
DECLARE i INT DEFAULT 0;<br />
REPEAT<br />
SET i = i + 1;<br />
INSERT INTO t (str) VALUES(repeat('a', round(rand()*100)));<br />
UNTIL i &gt;= nb_rows<br />
END REPEAT;<br />
END;<br />
//<br />
delimiter ;</code></p>
<p>Afin de valider deux scénarios pour ce billet, j&#8217;ai utilisé deux machines, une machine perso et un serveur pro. Deux configurations différentes : la perso n&#8217;est absolument pas &laquo;&nbsp;tunée&nbsp;&raquo;, en configuration strictement d&#8217;origine (5.1.32), la seconde a des processeurs et de la RAM à revendre.<br />
J&#8217;afficherai donc les temps d&#8217;exécution des deux machines à titre de comparaison.</p>
<p><span id="more-228"></span>Pour renseigner ma table de test à hauteur de 2 millions d&#8217;enregistrements, j&#8217;appelle ma procédure stockée :</p>
<p>Perso :<br />
mysql&gt; call fill_table(2000000);<br />
Query OK, 1 row affected (1 min 52.97 sec)</p>
<p>Pro :<br />
mysql&gt; call fill_table(2000000);<br />
Query OK, 1 row affected (42.23 sec)</p>
<p>Je souhaite tester ma requête sous InnoDB, je modifie le moteur de la table :</p>
<p>mysql&gt; alter table t engine=&#8217;innodb&#8217;;<br />
Query OK, 2000000 rows affected (43.57 sec)<br />
Records: 2000000  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; alter table t engine=&#8217;innodb&#8217;;<br />
Query OK, 2000000 rows affected (13.10 sec)<br />
Records: 2000000  Duplicates: 0  Warnings: 0</p>
<p>J&#8217;ai testé pour vous : il est <strong>bien plus rapide</strong> d&#8217;insérer en MyISAM et de faire l&#8217;ALTER en InnoDB que d&#8217;insérer directement en InnoDB :</p>
<p>Test de chargement avec <strong>table en InnoDB</strong> <strong>d&#8217;origine</strong> sur la machine <strong>perso</strong> :<br />
mysql&gt; call fill_table(<strong>100000</strong>);<br />
Query OK, 1 row affected (1 min 14.15 sec)</p>
<p>=&gt; Déjà 30 secondes de plus (70% plus lent) pour 20x moins de données !</p>
<p>La machine pro (en 5.0.56) est bien plus rapide mais valide elle aussi la stratégie de passer par MyISAM en premier lieu :<br />
Chargement d&#8217;une <strong>table en InnoDB</strong> sur serveur <strong>pro</strong> :<br />
mysql&gt; call fill_table(2000000);<br />
Query OK, 1 row affected (1 min 11.16 sec)</p>
<p>=&gt; A comparer avec les 42 sec (chargement MyISAM natif) + 13 sec (alter InnoDB) = 55 s. Au moins 30% plus rapide.</p>
<p>On obtient une table contenant des enregistrements du type :</p>
<p>mysql&gt; select * from t limit 5;<br />
+&#8212;-+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br />
| id | date                | str<br />
+&#8212;-+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br />
|  1 | 2009-03-30 22:18:02 | aaaaaaaa<br />
|  2 | 2009-03-30 22:18:02 | aaaaaaaaaaaaaaaa<br />
|  3 | 2009-03-30 22:18:02 | aaaaaaaaaaaaaaaaaaaaaaa<br />
|  4 | 2009-03-30 22:18:02 | aaaaaaaaaaa<br />
|  5 | 2009-03-30 22:18:02 | aaa<br />
+&#8212;-+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;</p>
<p>Nous voici donc avec un jeu d&#8217;essai raisonnable, prêt à en découdre avec cette requête&#8230;</p>
<p>mysql&gt; explain select sql_no_cache count(*) from t where length(str) between 20 and 70;</p>
<p>*************************** 1. row ***************************<br />
id: 1<br />
select_type: SIMPLE<br />
table: t<br />
type: ALL<br />
possible_keys: NULL<br />
key: NULL<br />
key_len: NULL<br />
ref: NULL<br />
rows: 2015427<br />
Extra: Using where<br />
1 row in set (0.00 sec)</p>
<p>&#8230; Au temps d&#8217;exécution pitoyable. Sur la machine perso, j&#8217;obtiens en moyenne :</p>
<p>mysql&gt; select sql_no_cache count(*) from t where length(str) between 20 and 70;<br />
+&#8212;&#8212;&#8212;-+<br />
| count(*) |<br />
+&#8212;&#8212;&#8212;-+<br />
|  1019970 |<br />
+&#8212;&#8212;&#8212;-+<br />
1 row in set (1.83 sec)</p>
<p>Sur le serveur pro :</p>
<p>mysql&gt; select sql_no_cache count(*) from t where length(str) between 20 and 70;<br />
+&#8212;&#8212;&#8212;-+<br />
| count(*) |<br />
+&#8212;&#8212;&#8212;-+<br />
|  1020008 |<br />
+&#8212;&#8212;&#8212;-+<br />
1 row in set (0.82 sec)</p>
<p>L&#8217;innodb_buffer_pool_size de la machine perso est complètement sous-dimensionné pour contenir toutes les données de ma table. L&#8217;intérêt ici est de tester ce qui se passe quand les données ne sont pas montées en RAM (comportement de MyISAM qui charge les datas dans le cache OS).</p>
<p>Constatons les &laquo;&nbsp;dégats&nbsp;&raquo; sur la machine perso :</p>
<p>mysql&gt; show variables like &#8216;innodb_buffer_pool_size&#8217;;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+&#8212;&#8212;&#8212;+<br />
| Variable_name           | Value   |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+&#8212;&#8212;&#8212;+<br />
| innodb_buffer_pool_size | 8 388 608<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+&#8212;&#8212;&#8212;+</p>
<p>&#8230; le jour et la nuit avec :</p>
<p>mysql&gt; show variables like &#8216;innodb_buffer_pool_size&#8217;;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+&#8212;&#8212;&#8212;&#8212;+<br />
| Variable_name           | Value      |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+&#8212;&#8212;&#8212;&#8212;+<br />
| innodb_buffer_pool_size | 3 984 588 800<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+&#8212;&#8212;&#8212;&#8212;+</p>
<p>Voyons si un index peut changer la donne ? Plaçons-le sur la colonne &laquo;&nbsp;str&nbsp;&raquo; :</p>
<p>mysql&gt; create index idx_str on t (str);<br />
Query OK, 2000000 rows affected (1 min 1.95 sec)<br />
Records: 2000000  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; create index idx_str on t (str);<br />
Query OK, 2000000 rows affected (24.95 sec)<br />
Records: 2000000  Duplicates: 0  Warnings: 0</p>
<p>Le nouveau plan d&#8217;exécution est le suivant :</p>
<p>mysql&gt; explain select sql_no_cache count(*) from t where length(str) between 20 and 70;<br />
*************************** 1. row ***************************<br />
id: 1<br />
select_type: SIMPLE<br />
table: t<br />
type: index<br />
possible_keys: NULL<br />
key: idx_str<br />
key_len: 102<br />
ref: NULL<br />
rows: 1957767<br />
Extra: Using where; Using index<br />
1 row in set (0.00 sec)</p>
<p>Nous sommes dans le cas d&#8217;un <a href="http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/" target="_blank">covering index</a> mais celui-ci n&#8217;est pas hélas pas d&#8217;une grande utilité sur la machine perso :</p>
<p>mysql&gt; select sql_no_cache count(*) from t where length(str) between 20 and 70;<br />
+&#8212;&#8212;&#8212;-+<br />
| count(*) |<br />
+&#8212;&#8212;&#8212;-+<br />
|  1019970 |<br />
+&#8212;&#8212;&#8212;-+<br />
1 row in set (1.98 sec)</p>
<p>On frôle les 2 secondes.</p>
<p>Sur la machine de prod c&#8217;est en revanche légèrement plus rapide :</p>
<p>mysql&gt; select sql_no_cache count(*) from t where length(str) between 20 and 70;<br />
+&#8212;&#8212;&#8212;-+<br />
| count(*) |<br />
+&#8212;&#8212;&#8212;-+<br />
|  1020008 |<br />
+&#8212;&#8212;&#8212;-+<br />
1 row in set (0.73 sec)</p>
<p>Stratégie : rajouter une colonne &laquo;&nbsp;str_nb&nbsp;&raquo; à ma table t et mettre à jour cette colonne selon la longueur de &laquo;&nbsp;str&nbsp;&raquo;.</p>
<p><strong>Machine Perso :</strong><br />
mysql&gt; ALTER table t ADD str_nb tinyint unsigned, ADD index idx_str_nb(str_nb);<br />
Query OK, 2000000 rows affected (1 min 9.22 sec)<br />
Records: 2000000  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; UPDATE t SET str_nb = length(str);<br />
Query OK, 2000000 rows affected (2 min 3.97 sec)<br />
Rows matched: 2000000  Changed: 2000000  Warnings: 0</p>
<p><strong>Serveur Pro :</strong><br />
mysql&gt; ALTER table t ADD str_nb tinyint unsigned, ADD index idx_str_nb(str_nb);<br />
Query OK, 2000000 rows affected (30.91 sec)<br />
Records: 2000000  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; UPDATE t SET str_nb = length(str);</p>
<p>Query OK, 2000000 rows affected (1 min 22.54 sec)<br />
Rows matched: 2000000  Changed: 2000000  Warnings: 0</p>
<p>mysql&gt; explain select sql_no_cache count(*) from t where str_nb between 20 and 70\G</p>
<p>*************************** 1. row ***************************<br />
id: 1<br />
select_type: SIMPLE<br />
table: t<br />
type: range<br />
possible_keys: idx_str_nb<br />
key: idx_str_nb<br />
key_len: 2<br />
ref: NULL<br />
rows: 914077<br />
Extra: Using where; Using index<br />
1 row in set (0.00 sec)</p>
<p>Notre requête modifiée tire parti du nouvel index :</p>
<p>mysql&gt; select sql_no_cache count(*) from t where str_nb between 20 and 70;<br />
+&#8212;&#8212;&#8212;-+<br />
| count(*) |<br />
+&#8212;&#8212;&#8212;-+<br />
|  1019970 |<br />
+&#8212;&#8212;&#8212;-+<br />
1 row in set (0.99 sec)</p>
<p>Sur la machine de prod :</p>
<p>mysql&gt; select sql_no_cache count(*) from t where str_nb between 20 and 70;<br />
+&#8212;&#8212;&#8212;-+<br />
| count(*) |<br />
+&#8212;&#8212;&#8212;-+<br />
|  1020008 |<br />
+&#8212;&#8212;&#8212;-+<br />
1 row in set (0.37 sec)</p>
<p>Résultat de ces tests : tirer parti d&#8217;une colonne &laquo;&nbsp;précalculée&nbsp;&raquo; permet de <strong>diviser par deux</strong> le temps d&#8217;exécution sur les deux machines.</p>
<p>Comment maintenir à jour cette colonne ?</p>
<p>C&#8217;est là que les <strong>triggers</strong> rentrent en jeu, on en crée deux :</p>
<p>Le premier concerne les <strong>INSERT</strong>&#8230;</p>
<p><code>DELIMITER |<br />
CREATE TRIGGER maj_str_nb_ins BEFORE INSERT ON t<br />
FOR EACH ROW BEGIN<br />
SET NEW.str_nb = length(NEW.str);<br />
END;<br />
|<br />
DELIMITER ;</code></p>
<p>&#8230; Et le second les <strong>UPDATE</strong> :</p>
<p><code>DELIMITER |<br />
CREATE TRIGGER maj_str_nb_upd BEFORE UPDATE ON t<br />
FOR EACH ROW BEGIN<br />
SET NEW.str_nb = length(NEW.str);<br />
END;<br />
|<br />
DELIMITER ;</code></p>
<p>Le mot clé &laquo;&nbsp;NEW&nbsp;&raquo; permet de faire référence à la nouvelle donnée qui est insérée. Dans le cadre d&#8217;un UPDATE il existe également le mot clé &laquo;&nbsp;OLD&nbsp;&raquo; qui permet de manipuler l&#8217;ancienne donnée, celle qui va être mise à jour.</p>
<p>Une fois insérés, ces triggers s&#8217;occuperont pour nous de mettre à jour automatiquement la colonne &laquo;&nbsp;str_nb&nbsp;&raquo;. Quelques exemples :</p>
<p>mysql&gt; insert into t (str) values (&#8216;dbnewz&#8217;);<br />
Query OK, 1 row affected (0.45 sec)</p>
<p>mysql&gt; select * from t where id = 2000001;<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8211;+&#8212;&#8212;&#8211;+<br />
| id      | date                | str    | <strong>str_nb</strong> |<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8211;+&#8212;&#8212;&#8211;+<br />
| 2000001 | 2009-04-01 00:35:58 | dbnewz |      <strong>6</strong> |<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8211;+&#8212;&#8212;&#8211;+</p>
<p>mysql&gt; update t set date = &#8216;2009-04-01 03:33:33&#8242; where id = 2000001;<br />
Query OK, 1 row affected (0.35 sec)<br />
Rows matched: 1  Changed: 1  Warnings: 0</p>
<p>mysql&gt; select * from t where id = 2000001;<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8211;+&#8212;&#8212;&#8211;+<br />
| id      | date                | str    | str_nb |<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8211;+&#8212;&#8212;&#8211;+<br />
| 2000001 | 2009-04-01 03:33:33 | dbnewz |      6 |<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8211;+&#8212;&#8212;&#8211;+<br />
1 row in set (0.00 sec)</p>
<p>mysql&gt; update t set str = &#8216;MySQL&#8217; where id = 2000001;<br />
Query OK, 1 row affected (0.00 sec)<br />
Rows matched: 1  Changed: 1  Warnings: 0</p>
<p>mysql&gt; select * from t where id = 2000001;<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+&#8212;&#8212;&#8211;+<br />
| id      | date                | str   | str_nb |<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+&#8212;&#8212;&#8211;+<br />
| 2000001 | 2009-04-01 00:38:18 | MySQL |      5 |<br />
+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+&#8212;&#8212;&#8211;+<br />
1 row in set (0.00 sec)</p>
<p>A vous de tester si ces triggers peuvent correspondre à un cas que vous cherchez à résoudre, dans ce cas précis, ils fonctionnent.</p>
<p>Merci à <a href="http://www.cybersite.com.au/blog" target="_blank">Jonathon Coombes</a> avec qui j&#8217;ai échangé autour de ce sujet.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/04/01/le-trigger-au-secours-des-function-based-index-fbi/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Les covering index, de la théorie à la pratique avec MyISAM et InnoDB</title>
		<link>http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/</link>
		<comments>http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/#comments</comments>
		<pubDate>Wed, 19 Nov 2008 23:56:59 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=134</guid>
		<description><![CDATA[Pour faire suite au dernier schéma sur les structures comparées d&#8217;un index MyISAM et InnoDB, ce billet a pour but de détailler une optimisation nommée covering index.
On appelle ainsi un index lorsqu&#8217;il &#171;&#160;couvre&#160;&#187; l&#8217;intégralité des données recherchées et évite ainsi un parcours des enregistrements souvent basé sur des random I/O, spécialement couteux sur disque.
A propos [...]]]></description>
			<content:encoded><![CDATA[<p>Pour faire suite au dernier <a href="http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/" target="_blank">schéma</a> sur les structures comparées d&#8217;un index MyISAM et InnoDB, ce billet a pour but de détailler une optimisation nommée covering index.<br />
On appelle ainsi un index lorsqu&#8217;il &laquo;&nbsp;couvre&nbsp;&raquo; l&#8217;intégralité des données recherchées et évite ainsi un parcours des enregistrements souvent basé sur des random I/O, spécialement couteux sur disque.</p>
<p>A propos des random I/O, voici un court extrait d&#8217;un <a href="http://www.dbnewz.com/2008/05/13/les-ssd-solid-state-drive-une-technologie-davenir-pour-nos-sgbd/" target="_blank">billet précédent sur les SSD</a> :</p>
<p><em>Sur un disque classique un “random read” entraîne (du plus couteux au moins couteux) :<br />
- le déplacement de la tête de lecture/écriture sur la bonne piste (”seek time”)<br />
- une fois la tête sur la bonne piste, il faut encore repérer sur celle-ci le bloc secteur demandé (”rotational latency”)<br />
- la lecture et la transmission de la donnée vers le système.</em></p>
<p>Les quelques chiffres suivants permettent de mesurer les conséquences du paragraphe précédent et ainsi de mieux visualiser les enjeux soulevés par les covering index.</p>
<p>On se base ici sur la base d&#8217;un enregistrement de 100 bytes. Notez que ces valeurs sont des ordres de grandeur :</p>
<p><strong>En mémoire : </strong><br />
Sequential read : 5 000 000 read/s (500 Mb /s)<br />
Random read : 250 000 read/s (25 Mb /s)</p>
<p>=&gt; En mémoire, un coefficient x20 existe entre les performances en sequentiel et en lecture aléatoire.</p>
<p><strong>Sur disque : </strong><br />
Sequential read : 500 000 r/s (50 Mb /s)<br />
Random read : 100 r/s (10 Kb /s)</p>
<p>=&gt; Sur disque, un coefficient x5000 (!) existe entre les performances en sequentiel et en lecture aléatoire.</p>
<p><span id="more-134"></span></p>
<p>(Source : <a href="http://www.dbnewz.com/2008/09/10/lincontournable-ouvrage-de-la-rentree-mysql-high-performance-2nd-edition/" target="_blank">MySQL High Performance 2nd Edition</a>)</p>
<p>On note qu&#8217;entre un parcours séquentiel sur disque (full scan table) et le même parcours en mémoire, il existe un facteur 10&#8230; A comparer avec un facteur 2500 pour du random I/O entre un parcours mémoire et son homologue sur disque.<br />
Pas de chance, c&#8217;est le parcours basé sur des random I/O qui en général permet de retrouver un enregistrement à partir d&#8217;un index (la clé primaire &laquo;&nbsp;clustered&nbsp;&raquo; d&#8217;InnoDB mise à part : raccrochez-vous au <a href="http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/" target="_blank">schéma </a>pour comprendre pourquoi).</p>
<p>Nous avons donc tout intérêt à tenter de minimiser ce type de parcours grâce aux covering index.</p>
<p><strong>Les covering index : de la théorie&#8230;</strong></p>
<p>Un covering index doit rassembler deux qualités :<br />
- Etre l&#8217;index choisi par MySQL pour résoudre votre requête<br />
- Contenir l&#8217;intégralité des champs recherchés (ceux du SELECT)</p>
<p>Si vous avez sous les yeux le fameux <a href="http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/" target="_blank">schéma</a>, vous voyez que dans le cas de MyISAM par exemple, il existe en effet un &laquo;&nbsp;saut&nbsp;&raquo; à effectuer entre la valeur indexée (peu importe que l&#8217;index soit primaire ou secondaire) et l&#8217;enregistrement. Dans le cas d&#8217;un covering index il n&#8217;y a plus besoin d&#8217;effectuer ce saut puisque la donnée recherchée se trouve déjà intégralement dans l&#8217;index.</p>
<p>Nous allons le voir maintenant, c&#8217;est au développeur affuté de rechercher ce type d&#8217;optimisations. Pour cela une bonne connaissance des caractéristiques de chaque moteur de stockage utilisé est nécessaire.</p>
<p>Si vous maîtrisez le principe du <a href="http://www.dbnewz.com/2008/06/27/les-index-mysql-types-placements-efficacite/" target="_blank">leftmost prefixing</a> et si en plus vous tirez parti d&#8217;une table InnoDB à la clé primaire ajoutée derrière chaque index secondaire, voilà qui devrait vous éviter de créer des index inutiles en profitant au maximum des forces en présence !</p>
<p><strong>&#8230; à la pratique</strong></p>
<p>Conçu autour de la base <a href="http://dev.mysql.com/doc/" target="_blank">world</a>, voici un exemple concret des différences entre MyISAM et InnoDB dont vous devez tenir compte pour vos stratégies d&#8217;indexation.</p>
<p>Nous partons d&#8217;une table MyISAM qui possède deux index, sa clé primaire (ID) et un index que j&#8217;ai crée sur le &laquo;&nbsp;countrycode&nbsp;&raquo; :</p>
<p>mysql&gt; <strong>SHOW INDEX FROM city\G</strong><br />
***** 1. row ****<br />
Key_name: PRIMARY<br />
Seq_in_index: 1<br />
Column_name: ID<br />
Cardinality: 4321<br />
[...]<br />
**** 2. row ****<br />
Key_name: idx_countrycode<br />
Seq_in_index: 1<br />
Column_name: CountryCode<br />
Cardinality: 720<br />
[...]</p>
<p>Testons un premier EXPLAIN :</p>
<p>mysql&gt; <strong>EXPLAIN SELECT countrycode, id FROM city WHERE countrycode=&#8217;fra&#8217; ORDER BY id ASC\G</strong><br />
***** 1. row ****<br />
id: 1<br />
select_type: SIMPLE<br />
table: city<br />
type: ref<br />
possible_keys: idx_countrycode<br />
<strong>key: idx_countrycode</strong><br />
key_len: 3<br />
ref: const<br />
rows: 39<br />
Extra: <strong>Using where; Using filesort</strong><br />
1 row in set (0.00 sec)</p>
<p>Cette table sera parcourue selon son index &laquo;&nbsp;idx_countrycode&nbsp;&raquo;, cependant vu que la colonne &laquo;&nbsp;id&nbsp;&raquo; est également nécessaire pour les résultats, il y&#8217;a un saut systématique à chaque entrée de l&#8217;index vers les enregistrements correspondants (c&#8217;est le &laquo;&nbsp;saut&nbsp;&raquo; entre le fichier .MYI et le .MYD représenté sur le <a href="http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/" target="_blank">schéma</a>).</p>
<p>A noter que nous avons ici un filesort c&#8217;est à dire un tri supplémentaire effectué par le serveur MySQL pour classer nos résultats. Certes, l&#8217;index contient des données ordonnées mais il s&#8217;agit ici des valeurs de &laquo;&nbsp;countrycode&nbsp;&raquo;, pas de celles concernant la colonne &laquo;&nbsp;id&nbsp;&raquo;.</p>
<p>Tentons maintenant une modification du moteur de stockage de la table suivie du même EXPLAIN :</p>
<p>mysql&gt; <strong>ALTER TABLE city</strong> <strong>ENGINE=InnoDB</strong>;<br />
Query OK, 4079 rows affected (0.45 sec)<br />
Records: 4079  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; <strong>EXPLAIN SELECT countrycode, id FROM city WHERE countrycode=&#8217;fra&#8217; ORDER BY id ASC\G</strong><br />
***** 1. row ****<br />
id: 1<br />
select_type: SIMPLE<br />
table: city<br />
type: ref<br />
possible_keys: idx_countrycode<br />
<strong>key: idx_countrycode</strong><br />
key_len: 3<br />
ref: const<br />
rows: 40<br />
Extra: <strong>Using where; Using index</strong><br />
1 row in set (0.02 sec)</p>
<p>Explain nous gratifie d&#8217;un &laquo;&nbsp;USING INDEX&nbsp;&raquo;, cela signifie que nous sommes dans le cas d&#8217;un &laquo;&nbsp;covering index&nbsp;&raquo;. Ne pas confondre en effet le libellé &laquo;&nbsp;index&nbsp;&raquo; dans la colonne &laquo;&nbsp;type&nbsp;&raquo; de EXPLAIN, qui signifie que tout l&#8217;index est parcouru, avec le &laquo;&nbsp;Using index&nbsp;&raquo; de la colonne extra qui indique un covering index.</p>
<p>Pour en revenir au <a href="http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/" target="_blank">schéma</a>, puisque c&#8217;est le support de ce billet, nous sommes passés du côté gauche du dessin (MyISAM), au côté droit (InnoDB). Remarquez comment est formé l&#8217;index secondaire sous InnoDB&#8230; Il contient la valeur de la clé primaire, c&#8217;est une caractéristique très importante puisque c&#8217;est comme si notre index placé simplement sur &laquo;&nbsp;countrycode&nbsp;&raquo; était en fait un index composé des champs (countrycode, id).</p>
<p>Nous sommes donc bien dans le cas d&#8217;un covering index puisque cette fois l&#8217;intégralité des données recherchées (le countrycode et &laquo;&nbsp;id&nbsp;&raquo;) sont contenues dans l&#8217;index, aucun &laquo;&nbsp;saut&nbsp;&raquo; supplémentaire vers les enregistrements n&#8217;est nécessaire, autant de random I/O de gagnés.</p>
<p>Nous aurions pu obtenir la même chose avec MyISAM mais en spécifiant cette fois un index composé, pour cela on repasse le moteur de stockage en MyISAM puis on crée l&#8217;index :</p>
<p>mysql&gt; <strong>ALTER TABLE city ENGINE=myisam;</strong><br />
Query OK, 4079 rows affected (0.23 sec)<br />
Records: 4079  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; <strong>CREATE INDEX idx_countrycode_id ON city(countrycode,id);</strong><br />
Query OK, 4079 rows affected (0.25 sec)<br />
Records: 4079  Duplicates: 0  Warnings: 0</p>
<p>mysql&gt; <strong>EXPLAIN SELECT id,countrycode FROM city WHERE countrycode=&#8217;fra&#8217; ORDER BY id ASC\G</strong><br />
***** 1. row ****<br />
table: city<br />
type: ref<br />
possible_keys: idx_countrycode,idx_countrycode_id<br />
key: idx_countrycode_id<br />
key_len: 3<br />
ref: const<br />
rows: 40<br />
Extra: Using where; Using index<br />
1 row in set (0.00 sec)</p>
<p>Inutile en revanche de créer ce même index composé sous InnoDB puisqu&#8217;il existe en fait déjà&#8230; Nous ne rajouterions dans ce cas qu&#8217;une possibilité supplémentaire à étudier inutilement pour l&#8217;optimiseur MySQL sans parler du temps de perdu à mettre à jour un index redondant.</p>
<p>&laquo;&nbsp;Combien d&#8217;index inutiles ai-je donc dans ma base à l&#8217;heure actuelle ?&nbsp;&raquo; est peut-être la question que vous vous posez désormais ?</p>
<p>Le tips dbnewz : allez jeter un oeil du côté du &laquo;&nbsp;<a href="http://www.maatkit.org/doc/mk-duplicate-key-checker.html" target="_blank">maatkit</a>&laquo;&nbsp;, cet ensemble de scripts perl contient justement un outil, le &laquo;&nbsp;<a href="http://www.maatkit.org/doc/mk-duplicate-key-checker.html" target="_blank">mk duplicate key checker</a>&nbsp;&raquo; qui a justement pour but de vous aider à détecter les doublons.</p>
<p>Notez que dans certains cas particuliers, un index peut volontairement avoir été crée bien que redondant avec un autre index existant. En effet il est parfois préférable de ne pas &laquo;&nbsp;alourdir&nbsp;&raquo; un index déjà composé de plusieurs champs, exemple un covering index, ou bien encore un index numérique spécialement impliqué dans des jointures qui verrait d&#8217;un mauvais oeil ce VARCHAR que vous souhaitez lui ajouter&#8230; Cela pourrait engendrer un ralentissement sur les requêtes précedemment basées sur les index que vous souhaitez &laquo;&nbsp;enrichir&nbsp;&raquo;.</p>
<p>Le &laquo;&nbsp;mk duplicate key checker&nbsp;&raquo; vous apporte les candidats sur un plateau, à vous d&#8217;apporter la valeur ajoutée finale <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' /> </p>
<p><strong>A retenir :</strong></p>
<p>- Les covering index constituent une optimisation très intéressante pour éviter de couteux random I/O, notamment sur disque. Ayez à l&#8217;esprit qu&#8217;ils existent et profitez des caractéristiques propres à InnoDB dans ce domaine (présence de la clé primaire derrière chaque index secondaire) pour optimiser vos requêtes sans créer d&#8217;index redondants.</p>
<p>- Dans certains cas un covering index peut également vous aider à faire disparaître un disgracieux &laquo;&nbsp;Using filesort&nbsp;&raquo; dans la colonne &laquo;&nbsp;Extra&nbsp;&raquo; (la preuve dans notre exemple).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Dessine-moi MySQL : structure d&#8217;un index MyISAM et InnoDB</title>
		<link>http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/</link>
		<comments>http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/#comments</comments>
		<pubDate>Thu, 23 Oct 2008 22:41:43 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[dessine-moi MySQL]]></category>
		<category><![CDATA[index]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=94</guid>
		<description><![CDATA[Deuxième exemplaire (traduire &#171;&#160;2ème essai&#160;&#187;) concernant ces schémas &#171;&#160;à main levée&#160;&#187;, au menu du jour une comparaison entre la structure d&#8217;un index MyISAM et celle d&#8217;un index InnoDB :
 
J&#8217;ai tenté de tenir compte de vos commentaires précédents, notamment sur les majuscules, est-ce plus lisible ?
Selon les &#171;&#160;règles&#160;&#187; établies pour cette série &#171;&#160;Dessine-moi MySQL&#160;&#187;, les [...]]]></description>
			<content:encoded><![CDATA[<p>Deuxième exemplaire (traduire &laquo;&nbsp;2ème essai&nbsp;&raquo;) concernant ces schémas &laquo;&nbsp;à main levée&nbsp;&raquo;, au menu du jour une comparaison entre la structure d&#8217;un index MyISAM et celle d&#8217;un index InnoDB :</p>
<p style="text-align: center;"><a href="http://www.dbnewz.com/wp-content/uploads/2008/11/index_structure_myisam_innodb_clustered.jpg"><img class="aligncenter size-medium wp-image-128" title="index_structure_myisam_innodb_clustered" src="http://www.dbnewz.com/wp-content/uploads/2008/11/index_structure_myisam_innodb_clustered-300x197.jpg" alt="" width="300" height="197" /></a><a href="http://www.dbnewz.com/wp-content/uploads/2008/11/index_structure_myisam_innodb_clustered1.jpg"> </a></p>
<p>J&#8217;ai tenté de tenir compte de vos <a href="http://www.dbnewz.com/2008/09/18/dessine-moi-mysql-la-replication-master-slave/" target="_blank">commentaires précédents</a>, notamment sur les majuscules, est-ce plus lisible ?</p>
<p>Selon les &laquo;&nbsp;règles&nbsp;&raquo; établies pour cette série &laquo;&nbsp;Dessine-moi MySQL&nbsp;&raquo;, les schémas doivent être suffisamment explicites pour ne pas nécessiter d&#8217;explications supplémentaires&#8230;<br />
Cependant, vu que je n&#8217;ai pas 10 ans d&#8217;école d&#8217;arts graphiques derrière moi (ça s&#8217;est vu ?), et que chaque règle a son exception, je rajouterai tout de même ceci :</p>
<p><span id="more-94"></span></p>
<p>Sous <strong>MyISAM</strong>, il n&#8217;y a pas de différence entre une clé primaire et un index &laquo;&nbsp;classique&nbsp;&raquo; du point de vue de leur structure, ils sont implémentés de la même façon : ils sont triés et pointent vers le ou les enregistrements correspondants. A noter que MyISAM stocke les enregistrements dans l&#8217;ordre où ils sont insérés.</p>
<p>En revanche sous <strong>InnoDB</strong>, une clé primaire est un index dit &laquo;&nbsp;<strong>clustered</strong>&laquo;&nbsp;. L&#8217;index est dans ce cas &laquo;&nbsp;accolé&nbsp;&raquo; aux données, il n&#8217;y a pas de &laquo;&nbsp;saut&nbsp;&raquo; supplémentaire à effectuer pour aller chercher l&#8217;enregistrement une fois qu&#8217;on est positionné sur la valeur recherchée de la clé primaire.<br />
Avec InnoDB, les enregistrements sont stockés non pas dans l&#8217;ordre d&#8217;insertion mais triés selon la clé primaire.</p>
<p>Rajoutons pour terminer que sous InnoDB, un index secondaire contient systématiquement la clé primaire. C&#8217;est en effet la clé primaire qui permet de retrouver l&#8217;enregistrement recherché.<br />
Deux conséquences :<br />
- il faut bien choisir sa clé primaire (concise de préférence puisque celle-ci sera présente dans chaque index secondaire).<br />
- le fait que la clé primaire soit présente sur chaque index secondaire permet &laquo;&nbsp;d&#8217;émuler&nbsp;&raquo; la présence d&#8217;un index supplémenaire sur votre table, autrement dit il existe des stratégies d&#8217;optimisation au rayon des covering index à ce sujet&#8230;</p>
<p>Ce dernier point sonne comme un proverbe chinois ? Alors ne manquez pas le prochain épisode sur les covering index, le support de cours est justement sous vos yeux <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' /> </p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/10/24/dessine-moi-mysql-structure-dun-index-myisam-et-innodb/feed/</wfw:commentRss>
		<slash:comments>4</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>Générer un jeu de données : shell, mysqlslap, et procédure stockée</title>
		<link>http://www.dbnewz.com/2008/08/19/generer-un-jeu-de-donnees-shell-mysqlslap-et-procedure-stockee/</link>
		<comments>http://www.dbnewz.com/2008/08/19/generer-un-jeu-de-donnees-shell-mysqlslap-et-procedure-stockee/#comments</comments>
		<pubDate>Mon, 18 Aug 2008 22:39:46 +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[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=61</guid>
		<description><![CDATA[Le prochain article de notre série consacrée aux index MySQL approche et j&#8217;ai besoin pour ce prochain épisode de générer une table de test de la forme suivante :  CREATE TABLE `t` (
`id` mediumint(8) unsigned NOT NULL auto_increment,
`date` timestamp NOT NULL,
`flag` tinyint(4) NOT NULL default '0',
PRIMARY KEY  (`id`),
KEY `flag` (`flag`)
) ENGINE=MyISAM;La structure est définie, [...]]]></description>
			<content:encoded><![CDATA[<p>Le prochain article de <a href="http://www.dbnewz.com/tag/index/" target="_blank">notre série consacrée aux index</a> MySQL approche et j&#8217;ai besoin pour ce prochain épisode de générer une table de test de la forme suivante :<br id="d7d0" /> <br id="zkp0" /> <code>CREATE TABLE `t` (<br />
`id` mediumint(8) unsigned NOT NULL auto_increment,<br />
`date` timestamp NOT NULL,<br />
`flag` tinyint(4) NOT NULL default '0',<br />
PRIMARY KEY  (`id`),<br />
KEY `flag` (`flag`)<br />
) ENGINE=MyISAM;</code><br id="yr-u" /><br id="yr-u0" />La structure est définie, reste à alimenter la table, disons 1 million d&#8217;enregistrements.<br id="ix.u" /> <br id="ix.u0" /> La valeur du champ &laquo;&nbsp;flag&nbsp;&raquo; est importante pour nos tests ultérieurs, sa valeur doit pour le moment être comprise entre 0 et 1, le tout à peu près uniformément distribué.<br id="g-s4" /> <br id="g-s40" /> La requête sera la suivante :<br id="ef7s" /> <br id="g-s41" /> <code>INSERT INTO test.t (flag) VALUES (ROUND(RAND()))</code>;<br id="d0-j" /> <br id="d0-j0" /> Il faut maintenant exécuter celle-ci 1 million de fois.<br id="gx_b" /> Voyons ce que cela donne en utilisant le shell, mysqlslap ou bien encore une procédure stockée.</p>
<p><span id="more-61"></span></p>
<p><strong>Le shell</strong></p>
<p>L&#8217;idée : générer un fichier texte des données à insérer puis exécuter le fichier de sortie comme entrée du client mysql.</p>
<p>Cette méthode n&#8217;est pas la plus efficace du lot, surtout pour 1 million de lignes, mais on peut néanmoins arriver à ses fins :</p>
<p><code>debian:/tmp# (for i in `seq 1 1000000`; do echo "insert into test.t(flag) values(round(rand()));"; done;) &gt; /tmp/insertions.txt</code></p>
<p>Puis :</p>
<p><code>debian:/tmp# mysql test &lt; /tmp/insertions.txt</code></p>
<p>Notre million de lignes est inséré.</p>
<p><strong>mysqlslap</strong></p>
<p>mysqlslap est un outil de benchmark que <a href="http://www.dbnewz.com/2008/07/07/mysqlslap-et-supersmack-deux-outils-de-benchmark-pour-mysql/" target="_blank">nous avons étudié récemment</a>. Nous détournons ici son utilisation première pour finalement exécuter notre million de requêtes :</p>
<p><code>debian:/usr/local/mysql51# ./bin/mysqlslap --user=root --socket=/tmp/mysql.sock --concurrency=1 --iterations=1000000 --query="insert into test.t (flag) values (round(rand()))"</code></p>
<p>Benchmark<br id="bel10" /> Average number of seconds to run all queries: 0.000 seconds<br id="bel11" /> Minimum number of seconds to run all queries: 0.000 seconds<br id="bel12" /> Maximum number of seconds to run all queries: 0.609 seconds<br id="bel13" /> Number of clients running queries: 1<br id="bel14" /> Average number of queries per client: 1</p>
<p>Cette solution est plus rapide que la précédente, aussi si vous disposez de mysqlslap, n&#8217;hésitez pas.</p>
<p><strong>La procédure stockée</strong> : REPEAT&#8230; UNTIL</p>
<p>Dès lors qu&#8217;on envisage d&#8217;insérer 1 million de lignes dans une table, la notion de &laquo;&nbsp;boucle&nbsp;&raquo; n&#8217;est jamais très loin, et finalement la procédure stockée non plus. Celle-ci est en effet un moyen simple et rapide d&#8217;implémenter un traitement répétitif.</p>
<p>Basique et à peine paramétrable (seul le nombre d&#8217;enregistrements à insérer est dynamique), voici à quoi ressemble la procédure stockée permettant d&#8217;insérer notre million de lignes :</p>
<p><code>delimiter //<br id="bfwc" />CREATE PROCEDURE fill_table(nb_rows INT)<br id="bfwc0" />BEGIN<br id="bfwc1" /> DECLARE i INT DEFAULT 0;<br id="bfwc2" /> REPEAT<br id="bfwc3" /> SET i = i + 1;<br id="bfwc4" /> INSERT INTO t (flag) VALUES(round(rand()));<br id="bfwc5" /> UNTIL i &gt;= nb_rows<br id="bfwc6" /> END REPEAT;<br id="bfwc7" />END;<br id="bfwc8" />//<br id="bfwc9" />delimiter ;</code><br id="bfwc10" /><br id="bfwc11" />Pas vraiment besoin de commenter le code, on note simplement les &laquo;&nbsp;delimiter&nbsp;&raquo; qui permettent de définir une autre terminaison que &laquo;&nbsp;;&nbsp;&raquo; le temps d&#8217;écrire la procédure dans le client mysql.</p>
<p>Pour appeler celle-ci :<br id="z6fd" /><br id="z6fd0" /><code>mysql&gt; CALL fill_table(1000000);</code><br id="z6fd1" />Query OK, 1 row affected (1 min 37.98 sec)</p>
<p>Cette solution est la plus rapide des trois présentées ici.</p>
<p>Mission accomplie ? Presque&#8230;</p>
<p>Avant de lancer la procédure stockée, la commande SHOW INDEX affichait une cardinalité de 0 pour la clé primaire et de NULL pour notre index situé sur &laquo;&nbsp;flag&nbsp;&raquo;.</p>
<p>Une fois la procédure terminée, un SHOW INDEX indique cette fois une cardinalité mise à jour pour la clé primaire (1 000 000) mais celle-ci est toujours à NULL pour l&#8217;index &laquo;&nbsp;flag&nbsp;&raquo;.</p>
<p>La commande <a href="http://dev.mysql.com/doc/refman/5.1/en/analyze-table.html" target="_blank">ANALYZE </a>permet de laver cet affront :</p>
<p><code>mysql&gt; ANALYZE TABLE t;<br />
+---------+---------+----------+----------+<br />
| Table   | Op      | Msg_type | Msg_text |<br />
+---------+---------+----------+----------+<br />
| world.t | analyze | status   | OK       |<br />
+---------+---------+----------+----------+</code></p>
<p>Les statistiques de notre index sont désormais à jour et la commande SHOW INDEX indique désormais une cardinalité de 2 pour l&#8217;index &laquo;&nbsp;flag&nbsp;&raquo;, le champ indexé ne comporte effectivement que des 0 ou des 1.</p>
<p>Nous voilà face à un index de cardinalité 2&#8230; Peu sélectif ? Inutile ? Mieux que rien ? Les paris sont ouverts.</p>
<p>Réponse au prochain numéro, stay tuned.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/08/19/generer-un-jeu-de-donnees-shell-mysqlslap-et-procedure-stockee/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
	</channel>
</rss>

