<?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; pratique</title>
	<atom:link href="http://www.dbnewz.com/tag/pratique/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.dbnewz.com</link>
	<description>le blog français sur les SGBD - MySQL, Oracle et plus...</description>
	<lastBuildDate>Wed, 28 Jul 2010 14:01:15 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Outils d’analyse de requêtes lentes – mysqlsla</title>
		<link>http://www.dbnewz.com/2010/07/28/outils-d%e2%80%99analyse-de-requetes-lentes-%e2%80%93-mysqlsla/</link>
		<comments>http://www.dbnewz.com/2010/07/28/outils-d%e2%80%99analyse-de-requetes-lentes-%e2%80%93-mysqlsla/#comments</comments>
		<pubDate>Wed, 28 Jul 2010 14:01:15 +0000</pubDate>
		<dc:creator>stephane</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[outils]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=622</guid>
		<description><![CDATA[Pour ce second volet de notre série consacrée aux outils d&#8217;analyse de requêtes lentes, nous allons nous pencher aujourd&#8217;hui sur mysqlsla, qui est un script Perl disposant de nombreuses options d&#8217;agrégation et de filtrage.
Commençons par l&#8217;installation du script. Rien de plus simple, il vous suffit pour commencer de télécharger et de décompresser une archive de [...]]]></description>
			<content:encoded><![CDATA[<p>Pour ce second volet de notre série consacrée aux outils d&#8217;analyse de requêtes lentes, nous allons nous pencher aujourd&#8217;hui sur <code>mysqlsla</code>, qui est un script Perl disposant de nombreuses options d&#8217;agrégation et de filtrage.<span id="more-622"></span></p>
<p>Commençons par l&#8217;installation du script. Rien de plus simple, il vous suffit pour commencer de télécharger et de décompresser une archive de l&#8217;outil, disponible <a href="http://hackmysql.com/scripts/mysqlsla-2.03.tar.gz">ici</a>. Ensuite, des classiques<br />
<code><br />
$ perl Makefile.PL<br />
$ make<br />
# make install<br />
</code><br />
vous permettent d&#8217;installer le script. Notez, point agréable, qu&#8217;une page de <code>man</code> est intégrée. Si vous cherchez la syntaxe d&#8217;une option, un <code>man mysqlsla</code> vous dispensera donc bien souvent d&#8217;aller faire un tour sur le site du projet.</p>
<p><code>mysqlsla</code> est plus généraliste que <code>mysqldumpslow</code> dans le sens où il est capable de traiter tout type de journal (requêtes lentes, mais aussi journal binaire ou journal général, ou encore journal défini par l&#8217;utilisateur). Les principes que nous allons voir dans cet article pour les requêtes lentes sont aussi valables pour les autres types de journaux.</p>
<p>L&#8217;idée de base de <code>mysqlsla</code> est de pouvoir analyser des fichiers journaux en leur appliquant éventuellement des filtres afin de ne garder que certains événements et d&#8217;émettre un rapport personnalisable présentant les résultats de cette analyse. Dans cet article nous allons seulement regarder les capacités de filtrage de <code>mysqlsla</code> et pas la manière de produire un rapport personnalisé, puisque le rapport par défaut nous convient très bien.</p>
<p>Pour un premier essai, lançons <code>mysqlsla</code> sans paramètre particulier à part l&#8217;option <code>lt</code> (comme log type) qui indique au script que le fichier passé est un journal de requêtes lentes. Cette option n&#8217;est en réalité pas indispensable car <code>mysqlsla</code> sait détecter automatiquement le type de journal :<br />
<code><br />
$ mysqlsla -lt slow msandbox-slow.log<br />
Report for slow logs: msandbox-slow.log<br />
4 queries total, 2 unique<br />
Sorted by 't_sum'<br />
Grand Totals: Time 1 s, Lock 0 s, Rows sent 1.80k, Rows Examined 64.18k<br />
</code><code><br />
________________________________________________ 001 ___<br />
Count         : 1  (25.00%)<br />
Time          : 718.344 ms total, 718.344 ms avg, 718.344 ms to 718.344 ms max  (79.40%)<br />
Lock Time (s) : 259 s total, 259 s avg, 259 s to 259 s max  (52.11%)<br />
Rows sent     : 0 avg, 0 to 0 max  (0.00%)<br />
Rows examined : 16.04k avg, 16.04k to 16.04k max  (25.00%)<br />
Database      : sakila<br />
Users         :<br />
	msandbox@localhost  : 100.00% (1) of query, 100.00% (4) of all users<br />
</code><code><br />
Query abstract:<br />
SET timestamp=N; INSERT INTO rental2 SELECT * FROM rental;<br />
</code><code><br />
Query sample:<br />
SET timestamp=1278504762;<br />
INSERT INTO rental2 SELECT * FROM rental;<br />
</code><code><br />
________________________________________________ 002 ___<br />
Count         : 3  (75.00%)<br />
Time          : 186.368 ms total, 62.123 ms avg, 52.575 ms to 74.232 ms max  (20.60%)<br />
Lock Time (s) : 238 s total, 79 s avg, 60 s to 117 s max  (47.89%)<br />
Rows sent     : 599 avg, 599 to 599 max  (100.00%)<br />
Rows examined : 16.04k avg, 16.04k to 16.04k max  (75.00%)<br />
Database      : sakila<br />
Users         :<br />
	msandbox@localhost  : 100.00% (3) of query, 100.00% (4) of all users<br />
</code><code><br />
Query abstract:<br />
SET timestamp=N; SELECT customer_id,COUNT(*) FROM rental WHERE return_date&gt;'S' GROUP BY customer_id;<br />
</code><code><br />
Query sample:<br />
SET timestamp=1278504711;<br />
SELECT customer_id,COUNT(*) FROM rental WHERE return_date&gt;'2005-01-01' GROUP BY customer_id;<br />
</code><br />
A première vue, ce rapport ressemble à celui de <code>mysqldumpslow</code> avec un regroupement des requêtes similaires. Le rapport est cependant plus complet car pour chaque groupe, nous avons les valeurs minimales, moyennes et maximales ainsi qu&#8217;une information très intéressante sur le poids relatif de chaque groupe, ce qui permet de cibler facilement les requêtes qui ont le plus contribué au temps de réponse ou qui ont examiné le plus de lignes.</p>
<p>Autre point à noter : pour chaque groupe de requêtes, <code>mysqlsla</code> nous présente une vue abstraite de la requête, c&#8217;est-à-dire une requête générique où les paramètres qui varient entre les requêtes du groupe sont remplacés par N pour un nombre ou S pour une chaîne de caractères, mais aussi un exemple de requête avec des vrais paramètres. C&#8217;est un bon point par rapport à <code>mysqldumpslow</code> puisqu&#8217;il est facile avec l&#8217;exemple de regarder le plan d&#8217;exécution donné par la commande <code>EXPLAIN</code>.</p>
<p>Intéressons-nous aux 4 questions que nous nous étions posées lors de l&#8217;<a href="http://www.dbnewz.com/2010/07/08/outils-danalyse-de-requetes-lentes-mysqldumpslow">article précédent</a>, qui vont nous permettre de voir comment utiliser les méta-données des requêtes pour trier ou filtrer.</p>
<p>Chaque requête dans le journal possède un certain nombre de méta-données, comme le temps d&#8217;exécution ou le nombre de lignes examinées. Il existe également des méta-données dérivées, comme la moyenne des temps d&#8217;exécution pour un groupe de requêtes. Les méta-données disponibles dépendent du type de journal considéré, et tout est détaillé dans la <a href="http://hackmysql.com/mysqlsla_filters">page de la documentation</a> consacrée aux filtres.</p>
<p><code>mysqlsla</code> nous permet de trier les résultats du rapport selon n&#8217;importe quelle méta-donnée avec l&#8217;option <code>--sort</code>.</p>
<p>Ceci va nous permettre de répondre aux 2 premières questions :</p>
<p>Quel est le groupe de requêtes ayant le plus long temps de réponse ?<br />
<code><br />
$ mysqlsla -lt slow --sort t_sum msandbox-slow.log<br />
</code><br />
Ici l&#8217;affichage est exactement celui que nous avions sans l&#8217;option <code>--sort</code>. C&#8217;est normal car pour les journaux de requêtes lentes, <code>mysqlsla</code> applique par défaut l&#8217;option <code>--sort t_sum</code> !</p>
<p>Quel est le groupe de requêtes ayant le plus grand nombre d&#8217;occurrences ?<br />
<code><br />
$ mysqlsla -lt slow --sort c_sum msandbox-slow.log<br />
Report for slow logs: msandbox-slow.log<br />
4 queries total, 2 unique<br />
Sorted by 'c_sum'<br />
Grand Totals: Time 1 s, Lock 0 s, Rows sent 1.80k, Rows Examined 64.18k<br />
</code><code><br />
_______________________________________________ 001 ___<br />
Count         : 3  (75.00%)<br />
Time          : 186.368 ms total, 62.123 ms avg, 52.575 ms to 74.232 ms max  (20.60%)<br />
Lock Time (s) : 238 s total, 79 s avg, 60 s to 117 s max  (47.89%)<br />
Rows sent     : 599 avg, 599 to 599 max  (100.00%)<br />
Rows examined : 16.04k avg, 16.04k to 16.04k max  (75.00%)<br />
Database      : sakila<br />
Users         :<br />
	msandbox@localhost  : 100.00% (3) of query, 100.00% (4) of all users<br />
</code><code><br />
Query abstract:<br />
SET timestamp=N; SELECT customer_id,COUNT(*) FROM rental WHERE return_date&gt;'S' GROUP BY customer_id;<br />
</code><code><br />
Query sample:<br />
SET timestamp=1278504711;<br />
SELECT customer_id,COUNT(*) FROM rental WHERE return_date&gt;'2005-01-01' GROUP BY customer_id;<br />
</code><code><br />
________________________________________________ 002 ___<br />
Count         : 1  (25.00%)<br />
Time          : 718.344 ms total, 718.344 ms avg, 718.344 ms to 718.344 ms max  (79.40%)<br />
Lock Time (s) : 259 s total, 259 s avg, 259 s to 259 s max  (52.11%)<br />
Rows sent     : 0 avg, 0 to 0 max  (0.00%)<br />
Rows examined : 16.04k avg, 16.04k to 16.04k max  (25.00%)<br />
Database      :<br />
Users         :<br />
	msandbox@localhost  : 100.00% (1) of query, 100.00% (4) of all users<br />
</code><code><br />
Query abstract:<br />
SET timestamp=N; INSERT INTO rental2 SELECT * FROM rental;<br />
</code><code><br />
Query sample:<br />
SET timestamp=1278504762;<br />
INSERT INTO rental2 SELECT * FROM rental;<br />
</code><br />
L&#8217;utilisation des filtres va nous permettre de répondre aux 2 dernières questions. Il faut savoir que <code>mysqlsla</code> dispose de deux types de filtres : le premier type permet de filtrer sur les méta-données des entrées du journal alors que le second permettre de sélectionner ou d&#8217;exclure un ou plusieurs types de requêtes. Ces deux types de filtres peuvent bien sûr être combinés afin de répondre à des questions complexes.</p>
<p>Les filtres sur les méta-données s&#8217;écrivent avec l&#8217;option <code>--meta-filter</code> (ou <code>-mf</code> en abrégé), comme par exemple <code>-mf "db=sakila"</code> pour ne conserver que les requêtes sur la base sakila ou <code>-mf "db=sakila,c_sum&gt;5"</code> pour ne conserver que les requêtes sur la base sakila qui apparaissent au moins 6 fois.</p>
<p>Il est facile avec cette option de répondre à la 3è question :<br />
Quelles sont les requêtes qui prennent plus de x secondes ?<br />
En positionnant x à 0.1s, on obtient le résultat suivant :<br />
<code><br />
$ mysqlsla -lt slow -mf "t&gt;0.1" msandbox-slow.log<br />
Report for slow logs: msandbox-slow.log<br />
1 queries total, 1 unique<br />
Sorted by 't_sum'<br />
Grand Totals: Time 1 s, Lock 0 s, Rows sent 0, Rows Examined 16.04k<br />
</code><code><br />
________________________________________________ 001 ___<br />
Count         : 1  (100.00%)<br />
Time          : 718.344 ms total, 718.344 ms avg, 718.344 ms to 718.344 ms max  (100.00%)<br />
Lock Time (s) : 259 s total, 259 s avg, 259 s to 259 s max  (100.00%)<br />
Rows sent     : 0 avg, 0 to 0 max  (0.00%)<br />
Rows examined : 16.04k avg, 16.04k to 16.04k max  (100.00%)<br />
Database      :<br />
Users         :<br />
	msandbox@localhost  : 100.00% (1) of query, 100.00% (1) of all users<br />
</code><code><br />
Query abstract:<br />
SET timestamp=N; INSERT INTO rental2 SELECT * FROM rental;<br />
</code><code><br />
Query sample:<br />
SET timestamp=1278504762;<br />
INSERT INTO rental2 SELECT * FROM rental;<br />
</code></p>
<p>Enfin nous allons nous servir des filtres sur les requêtes (option <code>--statement-filter</code> ou <code>-sf</code>) pour répondre à la 4è question :<br />
Comment ne conserver que les requêtes <code>SELECT</code> ?<br />
<code><br />
$ mysqlsla -lt slow -sf "+SELECT" msandbox-slow.log<br />
Report for slow logs: msandbox-slow.log<br />
3 queries total, 1 unique<br />
Sorted by 't_sum'<br />
Grand Totals: Time 0 s, Lock 0 s, Rows sent 1.80k, Rows Examined 48.13k<br />
</code><code><br />
________________________________________________ 001 ___<br />
Count         : 3  (100.00%)<br />
Time          : 186.368 ms total, 62.123 ms avg, 52.575 ms to 74.232 ms max  (100.00%)<br />
Lock Time (s) : 238 s total, 79 s avg, 60 s to 117 s max  (100.00%)<br />
Rows sent     : 599 avg, 599 to 599 max  (100.00%)<br />
Rows examined : 16.04k avg, 16.04k to 16.04k max  (100.00%)<br />
Database      : sakila<br />
Users         :<br />
	msandbox@localhost  : 100.00% (3) of query, 100.00% (3) of all users<br />
</code><code><br />
Query abstract:<br />
SELECT customer_id,COUNT(*) FROM rental WHERE return_date&gt;'S' GROUP BY customer_id;<br />
</code><code><br />
Query sample:<br />
SELECT customer_id,COUNT(*) FROM rental WHERE return_date&gt;'2005-01-01' GROUP BY customer_id;<br />
</code><br />
Et voilà, notre (courte) exploration de <code>mysqlsla</code> est terminée ! Ces quelques exemples ont montré que <code>mysqlsla</code> est extrêmement flexible comparé à <code>mysqldumpslow</code>, ce qui en fait un très bon choix en tant outil d&#8217;analyse de journaux MySQL. Quels sont les inconvénients de <code>mysqlsla</code> ? En fait, je n&#8217;en vois qu&#8217;un seul : Daniel, le développeur de <code>mysqlsla</code>, a annoncé au printemps que l&#8217;outil ne serait plus maintenu. La raison est simple : Daniel fait maitenant partie de l&#8217;équipe de développement de Maatkit, qui propose un outil remplaçant <code>mysqlsla</code>. Mais assez dit, ce sera l&#8217;objet du prochain article de cette série.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2010/07/28/outils-d%e2%80%99analyse-de-requetes-lentes-%e2%80%93-mysqlsla/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Désactiver les clés étrangères</title>
		<link>http://www.dbnewz.com/2009/07/15/desactiver-les-cles-etrangeres/</link>
		<comments>http://www.dbnewz.com/2009/07/15/desactiver-les-cles-etrangeres/#comments</comments>
		<pubDate>Wed, 15 Jul 2009 21:00:29 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=321</guid>
		<description><![CDATA[Une petite &#171;&#160;astuce&#160;&#187; pour se remettre dans le bain du blogging&#8230; Comme vous avez pu le constater la fréquence de mise à jour a un peu diminuée depuis notre retour de la MySQL Conf, corrigeons cela avec cette petite remise en jambe à classer dans la catégorie &#171;&#160;pratique&#160;&#187;.
La désactivation du contrôle des clés étrangères est [...]]]></description>
			<content:encoded><![CDATA[<p>Une petite &laquo;&nbsp;astuce&nbsp;&raquo; pour se remettre dans le bain du blogging&#8230; Comme vous avez pu le constater la fréquence de mise à jour a un peu diminuée depuis notre retour de la MySQL Conf, corrigeons cela avec cette petite remise en jambe à classer dans la catégorie &laquo;&nbsp;pratique&nbsp;&raquo;.</p>
<p>La désactivation du contrôle des clés étrangères est intéressante lorsque vous devez exécuter sur votre serveur MySQL un script de création de tables par exemple. Il se peut dans ce cas que l&#8217;ordre de création des différentes tables ne soit pas &laquo;&nbsp;logique&nbsp;&raquo;.</p>
<p>J&#8217;entends par là qu&#8217;une table A peut contenir une contrainte basée sur une clé étrangère référençant le champ d&#8217;une table B&#8230; qui n&#8217;existe pas encore ! La &laquo;&nbsp;logique&nbsp;&raquo; voudrait que les tables soient inscrites dans le script selon les liens qui existent entre elles mais cela n&#8217;est pas toujours le cas.</p>
<p>Cette opération de classement pouvant s&#8217;avérer fastidieuse à effectuer manuellement, vous pouvez vous affranchir des vérifications effectuées par le SGBD concernant les clés étrangères en utilisant la commande suivante :</p>
<p><code>mysql&gt; <strong>SET foreign_key_checks = 0;</strong></code></p>
<p>Vous êtes ainsi tranquille le temps de créer vos tables. Réactiver ce contrôle n&#8217;est pas plus compliqué :</p>
<p><code>mysql&gt; <strong>SET foreign_key_checks = 1;</strong></code></p>
<p><a href="http://dev.mysql.com/doc/refman/5.1/en/innodb-foreign-key-constraints.html" target="_blank">Le chapitre de la documentation</a> correspondant aux foreign keys nous indique que c&#8217;est d&#8217;ailleurs la technique employée par mysqldump.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/07/15/desactiver-les-cles-etrangeres/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Sauvegarder ses procédures stockées avec mysqldump</title>
		<link>http://www.dbnewz.com/2009/04/07/sauvegarder-ses-procedures-stockees-avec-mysqldump/</link>
		<comments>http://www.dbnewz.com/2009/04/07/sauvegarder-ses-procedures-stockees-avec-mysqldump/#comments</comments>
		<pubDate>Tue, 07 Apr 2009 19:48:04 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=246</guid>
		<description><![CDATA[Une fois n&#8217;est pas coutume, un billet qui se lit en 10s :
Pour exporter vos procédures stockées grâce à mysqldump, n&#8217;oubliez pas l&#8217;option -R. Sans elle votre prochaine restauration risque de vous apporter quelques surprises. Si les triggers sont bien sauvegardés par défaut, il n&#8217;en va pas de même pour les procédures stockées.
Ainsi :
mysqldump -umy_user [...]]]></description>
			<content:encoded><![CDATA[<p>Une fois n&#8217;est pas coutume, un billet qui se lit en 10s :</p>
<p>Pour exporter vos procédures stockées grâce à <a href="http://dev.mysql.com/doc/refman/5.1/en/mysqldump.html" target="_blank">mysqldump</a>, n&#8217;oubliez pas l&#8217;option -R. Sans elle votre prochaine restauration risque de vous apporter quelques surprises. Si les triggers sont bien sauvegardés par défaut, il n&#8217;en va pas de même pour les procédures stockées.</p>
<p>Ainsi :<br />
<code>mysqldump -umy_user -p MY_DB MY_TABLE1 MY_TABLE2 &gt; /tmp/dump_my_db.sql</code></p>
<p>&#8230; Sauvegardera bien les tables MY_TABLE1 et MY_TABLE2 de la base MY_DB, mais pas les procédures stockées&#8230;</p>
<p>L&#8217;option -R permet de sauvegarder cette fois vos bases, procédures stockées comprises :</p>
<p><code>mysqldump -umy_user -p <strong>-R</strong> MY_DB MY_TABLE1 MY_TABLE2 &gt; /tmp/dump_my_db.sql</code></p>
<p>Pour ne sauvegarder que vos procédures stockées, utilisez par exemple :</p>
<p><code>mysqldump –umy_user -p <strong>-R</strong> --all-databases --no-data --no-create-db --no-create-info &gt; /tmp/dump_proc.sql</code></p>
<p>mysqldump est riche en options, <a href="http://dev.mysql.com/doc/refman/5.1/en/mysqldump.html" target="_blank">un petit coup d&#8217;oeil sur la doc</a> de temps en temps permet d&#8217;exploiter de nouvelles idées.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/04/07/sauvegarder-ses-procedures-stockees-avec-mysqldump/feed/</wfw:commentRss>
		<slash:comments>2</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>1</slash:comments>
		</item>
		<item>
		<title>surveillez vos my.cnf : can’t find message file share/english/errmsg.sys</title>
		<link>http://www.dbnewz.com/2009/03/24/surveillez-vos-mycnf-can%e2%80%99t-find-message-file-shareenglisherrmsgsys/</link>
		<comments>http://www.dbnewz.com/2009/03/24/surveillez-vos-mycnf-can%e2%80%99t-find-message-file-shareenglisherrmsgsys/#comments</comments>
		<pubDate>Mon, 23 Mar 2009 23:22:50 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=218</guid>
		<description><![CDATA[Oui je me suis fait avoir, oui je suis parti en mode R&#38;D pour comprendre d&#8217;où sortait cette erreur sans cause apparente, oui j&#8217;ai tenté de bidouiller mon my.cnf pour rajouter la variable &#171;&#160;language&#160;&#187; et définir ainsi le path vers ce fameux &#171;&#160;errmsg.sys&#160;&#187; inconnu au bataillon de mon serveur MySQL&#8230;
Faute avouée est à demi-pardonnée, alors [...]]]></description>
			<content:encoded><![CDATA[<p>Oui je me suis fait avoir, oui je suis parti en mode R&amp;D pour comprendre d&#8217;où sortait cette erreur sans cause apparente, oui j&#8217;ai tenté de bidouiller mon my.cnf pour rajouter la variable &laquo;&nbsp;language&nbsp;&raquo; et définir ainsi le path vers ce fameux &laquo;&nbsp;errmsg.sys&nbsp;&raquo; inconnu au bataillon de mon serveur MySQL&#8230;</p>
<p>Faute avouée est à demi-pardonnée, alors autant dire que publiée sur un blog elle est carrément excusée <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' /> </p>
<p>&laquo;&nbsp;L&#8217;astuce&nbsp;&raquo; est donc la suivante :</p>
<p>Vérifiez que vos &laquo;&nbsp;clients&nbsp;&raquo; MySQL lisent bien le <strong>bon </strong>my.cnf&#8230; La doc ne fait pourtant pas mystère de la façon dont les fichiers de configuration (my.cnf en tête de gondole) <a href="http://dev.mysql.com/doc/refman/5.1/en/option-files.html" target="_blank">sont lus.</a></p>
<p>&laquo;&nbsp;If multiple instances of a given option are found, the last         instance takes <strong>precedence</strong>.&nbsp;&raquo; Autrement dit, le dernier qui parle à raison et mon my.cnf (/etc/my.cnf) était écrasé par d&#8217;autres copies de my.cnf dont je n&#8217;avais pas vérifié l&#8217;existence cette fois-ci.</p>
<p>Pour vérifier les fichiers lus par défaut (et dans quel ordre) :</p>
<p><code>&gt; mysql --help | grep 'my.cnf'</code></p>
<p>On obtient sur cette machine :<br />
<code><br />
order of preference, my.cnf, $MYSQL_TCP_PORT,<br />
/etc/my.cnf /etc/mysql/my.cnf /usr/local/mysql/etc/my.cnf ~/.my.cnf</code></p>
<p>Si vous ne souhaitez pas que le /etc/mysql/my.cnf écrase ou redéfinisse certaines de vos variables (&laquo;&nbsp;datadir&nbsp;&raquo; et &laquo;&nbsp;basedir&nbsp;&raquo; par exemple) du /etc/my.cnf précédent, un renommage du my.cnf superflu résoudra le problème.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/03/24/surveillez-vos-mycnf-can%e2%80%99t-find-message-file-shareenglisherrmsgsys/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Liens symboliques et fichiers temporaires sous MyISAM</title>
		<link>http://www.dbnewz.com/2009/02/15/liens-symboliques-et-fichiers-temporaires-sous-myisam/</link>
		<comments>http://www.dbnewz.com/2009/02/15/liens-symboliques-et-fichiers-temporaires-sous-myisam/#comments</comments>
		<pubDate>Sun, 15 Feb 2009 15:44:11 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=203</guid>
		<description><![CDATA[Récemment confronté à des problèmes ponctuels d&#8217;espace disque sur un serveur gros consommateur de ce type de ressources (datawarehouse), j&#8217;ai dû temporairement jongler entre différentes partitions afin de permettre au serveur MySQL de continuer à fonctionner.
Le problème :
La partition accueillant le répertoire d&#8217;installation standard de notre MySQL arrivant à saturation, il devenait impossible de passer [...]]]></description>
			<content:encoded><![CDATA[<p>Récemment confronté à des problèmes ponctuels d&#8217;espace disque sur un serveur gros consommateur de ce type de ressources (datawarehouse), j&#8217;ai dû temporairement jongler entre différentes partitions afin de permettre au serveur MySQL de continuer à fonctionner.</p>
<p>Le problème :<br />
La partition accueillant le répertoire d&#8217;installation standard de notre MySQL arrivant à saturation, il devenait impossible de passer certaines commandes dont le <strong>ALTER TABLE</strong>. Celle-ci nécessite en effet la plupart du temps (pour MyISAM) la création de fichiers temporaires (.MYD, .MYI, .frm) dont les tailles sont semblables à celles des fichiers initiaux (aux modifications de structure près). Le hic : la taille restante sur cette partition ne permettait pas la création de tels fichiers.</p>
<p>Sur une table de plusieurs centaines de millions de lignes, pesant quelques dizaines de Go, un ALTER TABLE prend souvent plusieurs heures&#8230; Je vous conseille donc de vérifier <strong>dès le départ</strong> si vous disposez sur votre partition d&#8217;un espace au moins équivalent à la somme des .MYD + .MYI de votre table si vous souhaitez éviter de saturer totalement l&#8217;espace disque.</p>
<p>Si malgré tout vous poussez le vice jusqu&#8217;à défier toutes vos sondes de monitoring et obtenez un &laquo;&nbsp;Use 100%&nbsp;&raquo;  sur un &laquo;&nbsp;df -h&nbsp;&raquo;, sachez que le <strong>.TMD</strong> susceptible de faire son apparition est là pour tenter de réparer les fichiers temporaires (au moins le .MYD) crées lors du ALTER TABLE. Cette étape supplémentaire rallonge fortement l&#8217;opération initiale à tel point qu&#8217;il est parfois plus judicieux de l&#8217;interrompre, supprimer les fichiers temporaires initialement crées par le ALTER, ainsi que le .TMD, et de tout reprendre à zéro avec cette fois un espace disque suffisant.</p>
<p>Pour éviter d&#8217;en arriver là, et si l&#8217;espace disque vient à manquer sur la partition de votre MySQL, les liens symboliques peuvent vous sauver la mise. Ils permettent de déplacer bases et tables MyISAM sur <strong>différentes partitions / disques</strong>, soit pour des raisons de performance (on peut alors allouer une base/table à un disque) soit comme ici surtout pour des raisons de place.</p>
<p>Le jonglage à base de liens symboliques permet-il d&#8217;éviter tous les problèmes ? Quel rôle joue la variable TMPDIR ?</p>
<p><span id="more-203"></span>Autant être clair dès maintenant, la variable TMPDIR n&#8217;intervient pas dans le cadre d&#8217;un ALTER TABLE. Si celle-ci désigne bien l&#8217;emplacement où MySQL stocke ses fichiers temporaires, un ALTER TABLE crée une table temporaire dans le même répertoire que la table originale.</p>
<p>En effet :</p>
<p>mysql&gt; show variables like &#8216;tmpdir&#8217;;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+<br />
| Variable_name | Value |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+<br />
| tmpdir        | /tmp  |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+<br />
1 row in set (0.00 sec)</p>
<p>debian:~/sandboxes/msb_5_1_28/data/test# ls -l</p>
<p>-rw-rw&#8212;- 1 root root     8644 2009-02-08 02:52 t.frm<br />
-rw-rw&#8212;- 1 root root 20000000 2009-02-08 02:52 t.MYD<br />
-rw-rw&#8212;- 1 root root 17444864 2009-02-08 02:52 t.MYI</p>
<p>Pendant l&#8217;exécution du ALTER TABLE on obtient :</p>
<p>debian:~/sandboxes/msb_5_1_28/data/test# ls -l</p>
<p>-rw-rw&#8212;- 1 root root     8616 2009-02-08 08:00 #sql-e1c_1.frm<br />
-rw-rw&#8212;- 1 root root  1966080 2009-02-08 08:00 #sql-e1c_1.MYD<br />
-rw-rw&#8212;- 1 root root     1024 2009-02-08 08:00 #sql-e1c_1.MYI<br />
-rw-rw&#8212;- 1 root root     8644 2009-02-08 02:52 t.frm<br />
-rw-rw&#8212;- 1 root root 20000000 2009-02-08 02:52 t.MYD<br />
-rw-rw&#8212;- 1 root root 17444864 2009-02-08 02:52 t.MYI</p>
<p>Après l&#8217;opération :</p>
<p>debian:~/sandboxes/msb_5_1_28/data/test# ls -l</p>
<p>-rw-rw&#8212;- 1 root root     8616 2009-02-08 08:00 t.frm<br />
-rw-rw&#8212;- 1 root root  9000000 2009-02-08 08:00 t.MYD<br />
-rw-rw&#8212;- 1 root root 17422336 2009-02-08 08:01 t.MYI</p>
<p>Les fichiers temporaires liés à la reconstruction de la nouvelle table (#&#8230;) sont bien situés dans le répertoire de la table d&#8217;origine, et non pas dans /tmp.<br />
Dans le cadre d&#8217;un GROUP BY ou ORDER BY, MySQL peut avoir besoin de créer des fichiers temporaires dont l&#8217;emplacement sera cette fois défini par la variable TMPDIR.</p>
<p>La création des fichiers temporaires au sein même du répertoire d&#8217;origine de la table n&#8217;est pas vraiment souhaitable si l&#8217;on manque d&#8217;espace disque, comment régler le problème ?</p>
<p>Tentons de déplacer la table incriminée par des liens symboliques.</p>
<p><strong>Deux remarques importantes :</strong></p>
<ol>
<li> &#8211; <strong>le .frm doit rester dans le répertoire initial</strong>, il ne peut pas être transformé en lien symbolique, en revanche le .MYD et le .MYI peuvent être déplacés où bon vous semble, il est notamment possible de les stocker chacun dans un répertoire différent.</li>
<li>- La documentation stipule que la manipulation d&#8217;une table MyISAM par liens symboliques doit s&#8217;effectuer serveur MySQL éteint. La &laquo;&nbsp;procédure&nbsp;&raquo; fonctionne serveur allumé mais vous vous exposez à différents problèmes : accès aux tables concernées pendant la procédure de déplacement de vos tables et <strong>résultats aléatoires</strong>.</li>
</ol>
<p>J&#8217;ai effectué de nombreux tests avec un serveur MySQL non coupé, les résultats varient selon les exécutions, les versions de MySQL&#8230; Difficile d&#8217;établir une conclusion concernant un comportement de MySQL lorsque le serveur fonctionne et qu&#8217;on utilise les liens symboliques pour déplacer une table, voici différents comportements que j&#8217;ai observé :</p>
<ul>
<li> Tout se passe bien : vos tables pointent vers un autre répertoire, vous appliquez le ALTER TABLE et les fichiers temporaires suivent les liens symboliques pour être finalement crées sur le répertoire de destination, tout est ok.</li>
<li> MySQL est susceptible de supprimer vos liens symboliques en fin d&#8217;ALTER TABLE, alors que vos .MYD et .MYI du répertoire cible sont supprimés pour être finalement recrées dans le répertoire initial.</li>
<li> MySQL semble avoir du mal avec les liens symboliques détenus par root&#8230; J&#8217;ai remarqué qu&#8217;avec un chown -h mysql:root par exemple, le serveur MySQL s&#8217;en sortait mieux. A noter que la 5.1.30 semble faire effectuer ce &laquo;&nbsp;changement de propriétaire&nbsp;&raquo; d&#8217;elle même : en fin d&#8217;ALTER TABLE mes liens symboliques sont passés de root:root à mysql:mysql.</li>
</ul>
<p>Bref, d&#8217;une façon générale avant de vous lancer dans le symlinking d&#8217;une très lourde table, testez d&#8217;abord la manipulation sur une table plus modeste afin de vérifier comment se comporte votre serveur MySQL, ceci tout particulièrement si vous comptez effectuer l&#8217;opération serveur allumé (encore une fois non recommandé, attention aux accès potentiels sur vos tables&#8230;).</p>
<p>La procédure à suivre pour vos tests est simple, voici la mienne :</p>
<p>J&#8217;ai repris la structure de la table d&#8217;un billet précédent (<a href="http://www.dbnewz.com/2008/08/19/generer-un-jeu-de-donnees-shell-mysqlslap-et-procedure-stockee/" target="_blank">générer un jeu de données</a><span>)</span>, ainsi que la procédure stockée nécessaire pour la renseigner, souvent 1M de lignes pour mes tests (à varier selon votre configuration, il faut que les ALTER TABLE que vous passerez soient suffisamment longs pour vous laisser le temps d&#8217;effectuer au moins un &laquo;&nbsp;ls -l&nbsp;&raquo; <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> </p>
<p>J&#8217;ai initialement mes 3 fichiers de ma table MyISAM &laquo;&nbsp;t&nbsp;&raquo; :</p>
<p>debian:~/sandboxes/msb_5_1_28/data/test# ls -l<br />
-rw-rw&#8212;- 1 root root 8616 2009-02-08 13:34 t.frm<br />
-rw-rw&#8212;- 1 root root 4500000 2009-02-08 13:34 t.MYD<br />
-rw-rw&#8212;- 1 root root 8177664 2009-02-08 13:34 t.MYI</p>
<p>On déplace les fichiers .MYD et .MYI vers un répertoire cible :<br />
mv t.M* /symlink_test</p>
<p>On ajoute les liens symboliques :</p>
<p>ln -s /symlink_test/t.MYD t.MYD<br />
ln -s /symlink_test/t.MYI t.MYI</p>
<p>debian:~/sandboxes/msb_5_1_28/data/test# ls -l<br />
-rw-rw&#8212;- 1 root root &#8230; 13:34 t.frm<br />
lrwxrwxrwx 1 root root  &#8230; 13:49 t.MYD -&gt; /symlink_test/t.MYD<br />
lrwxrwxrwx 1 root root  &#8230; 13:49 t.MYI -&gt; /symlink_test/t.MYI</p>
<p>Les fichiers .MYD et .MYI sont situés dans /symlink_test et on applique un ALTER TABLE à notre table.<br />
Voici quelques résultats obtenus pendant le ALTER TABLE&#8230;</p>
<p><strong>Cas 1 : MySQL ne suit pas les liens symboliques :</strong></p>
<p>/var/database/mysql50/tmp# ls -l</p>
<p>-rw-rw&#8212;- 1 mysql mysql   &#8230;  23:34 #sql-fe6_6e715a.frm<br />
-rw-rw&#8212;- 1 mysql mysql &#8230;   23:34 #sql-fe6_6e715a.MYD<br />
-rw-rw&#8212;- 1 mysql mysql   &#8230;  23:34 #sql-fe6_6e715a.MYI<br />
-rw-rw&#8212;- 1 mysql mysql   &#8230;  23:31 t.frm<br />
lrwxrwxrwx 1 root  root      &#8230;  23:33 t.MYD -&gt; /sym/t.MYD<br />
lrwxrwxrwx 1 root  root      &#8230;  23:33 t.MYI -&gt; /sym/t.MYI<br />
&#8230; et finalement les supprime en fin d&#8217;ALTER TABLE :</p>
<p>/var/database/mysql50/tmp# ls -l</p>
<p>-rw-rw&#8212;- 1 mysql mysql  &#8230;  23:34 t.frm<br />
-rw-rw&#8212;- 1 mysql mysql &#8230;  23:34 t.MYD<br />
-rw-rw&#8212;- 1 mysql mysql &#8230;  23:34 t.MYI</p>
<p>A tout hasard vérifiez la variable &#8216;have_symlink&#8217; dans votre configuration, elle est normalement activée par défaut :<br />
mysql&gt; show variables like &#8216;have_symlink&#8217;;</p>
<p>+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+<br />
| Variable_name | Value |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;-+<br />
| have_symlink  | YES   |</p>
<p><strong>Cas 2 : MySQL suit les liens symboliques :</strong></p>
<p>-rw-rw&#8212;- 1 root root 8644 2009-02-08 14:01 #sql-e1c_1.frm<br />
lrwxrwxrwx 1 root root  &#8230; #sql-e1c_1.MYD -&gt; /symlink_test/test/#sql-e1c_1.MYD<br />
lrwxrwxrwx 1 root root  &#8230; #sql-e1c_1.MYI -&gt; /symlink_test/test/#sql-e1c_1.MYI<br />
-rw-rw&#8212;- 1 root root  &#8230; t.frm<br />
lrwxrwxrwx 1 root root   &#8230; t.MYD -&gt; /symlink_test/test/t.MYD<br />
lrwxrwxrwx 1 root root   &#8230; t.MYI -&gt; /symlink_test/test/t.MYI</p>
<p>&#8230; sur le répertoire cible on constate la présence des fichiers temporaires :<br />
debian:/symlink_test/test# ls -l<br />
-rw-rw&#8212;- 1 root root 20000000 2009-02-08 14:01 #sql-e1c_1.MYD<br />
-rw-rw&#8212;- 1 root root  9265152 2009-02-08 14:01 #sql-e1c_1.MYI<br />
-rw-rw&#8212;- 1 root root 10000000 2009-02-08 13:58 t.MYD<br />
-rw-rw&#8212;- 1 root root 18443264 2009-02-08 13:59 t.MYI</p>
<p>Si vous en avez la possibilité, suivez les préconisations de la doc MySQL et établissez un lien symbolique <strong>sur la base entière</strong> plutôt que sur une table en particulier.<br />
Ex :</p>
<p>debian:~/sandboxes/msb_5_1_28/data/test# cd ..<br />
debian:~/sandboxes/msb_5_1_28/data# mv test /symlink_test/<br />
debian:~/sandboxes/msb_5_1_28/data# ln -s /symlink_test/test test<br />
debian:~/sandboxes/msb_5_1_28/data# ls -l<br />
lrwxrwxrwx 1 root root &#8230; 16:08 test -&gt; /symlink_test/test</p>
<p>Et dans le répertoire cible, pendant l&#8217;exécution du ALTER TABLE :</p>
<p>debian:/symlink_test/test# ls -l<br />
-rw-rw&#8212;- 1 root root     &#8230; 16:08 #sql-1efe_1.frm<br />
-rw-rw&#8212;- 1 root root     &#8230; 16:08 #sql-1efe_1.MYD<br />
-rw-rw&#8212;- 1 root root     &#8230; 16:08 #sql-1efe_1.MYI<br />
-rw-rw&#8212;- 1 root root     &#8230; 15:58 t.frm<br />
-rw-rw&#8212;- 1 root root     &#8230; 15:58 t.MYD<br />
-rw-rw&#8212;- 1 root root     &#8230; 15:58 t.MYI</p>
<p>&#8230; les fichiers temporaires ont bien suivi le lien symbolique, c&#8217;est la méthode recommandée par MySQL : le déplacement d&#8217;une base entière.</p>
<p>A noter qu&#8217;il est également possible d&#8217;établir de faire pointer un .MYD et un .MYI vers un répertoire précis via respectivement DATA DIRECTORY et INDEX DIRECTORY, deux options disponibles pour un CREATE TABLE. Malheureusement un ALTER TABLE ne peut pas modifier ces valeurs.</p>
<p>Les liens relatifs à notre thème du jour sur la doc MySQL :<br />
<span><a href="http://dev.mysql.com/doc/refman/5.1/en/temporary-files.html" target="_blank">http://dev.mysql.com/doc/refman/5.1/en/temporary-files.html</a><br />
</span><a href="http://dev.mysql.com/doc/refman/5.1/en/server-options.html#option_mysqld_tmpdir" target="_blank"><span>http://dev.mysql.com/doc/refman/5.1/en/server-options.html#option_mysqld_tmpdir</span></a><br />
<a href="http://dev.mysql.com/doc/refman/5.1/en/symbolic-links-to-tables.html" target="_blank"><span>http://dev.mysql.com/doc/refman/5.1/en/symbolic-links-to-tables.html</span></a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/02/15/liens-symboliques-et-fichiers-temporaires-sous-myisam/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Encore une question réccurente&#8230;</title>
		<link>http://www.dbnewz.com/2009/01/06/encore-une-question-reccurante/</link>
		<comments>http://www.dbnewz.com/2009/01/06/encore-une-question-reccurante/#comments</comments>
		<pubDate>Tue, 06 Jan 2009 17:19:04 +0000</pubDate>
		<dc:creator>pébé</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[pratique]]></category>

		<guid isPermaLink="false">http://www.dbnewz.com/?p=188</guid>
		<description><![CDATA[Bonjour à tous, c&#8217;est mon 1er post de l&#8217;année 2009, donc je vous souhaite à tous mes meilleurs voeux pour cette nouvelle année&#8230; Après 15 jours sans accés au réseau ( oui cela m&#8217;arrive une fois l&#8217;an ) me voila de retour en ligne. Comme le signalait arnaud, j&#8217;espère rencontrer le maximum de personne à [...]]]></description>
			<content:encoded><![CDATA[<p>Bonjour à tous, c&#8217;est mon 1er post de l&#8217;année 2009, donc je vous souhaite à tous mes meilleurs voeux pour cette nouvelle année&#8230; Après 15 jours sans accés au réseau ( oui cela m&#8217;arrive une fois l&#8217;an ) me voila de retour en ligne. Comme le signalait arnaud, j&#8217;espère rencontrer le maximum de personne à Paris pour la <a href="http://fr.sun.com/sunnews/events/2009/jan/soiree_open_source/">soirée Open Source</a>. En attendant, faisons un peu de MySQL!<br />
La question du jour m&#8217;a été posée par mon ami Geoff et c&#8217;est une question assez simple. Comment mettre les differentes valeurs d&#8217;une colonne sur une ligne?<br />
<span id="more-188"></span></p>
<p>Sans rentrer dans les détails, nous avons une simple table qui stocke différents métrics mesurés sur plusieurs serveurs. Nous avons à faire une application de monitoring.</p>
<p>mysql&gt; desc default_summary;<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;+&#8212;&#8211;+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;-+<br />
| Field         | Type             | Null | Key | Default | Extra          |<br />
+&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+&#8212;&#8212;+&#8212;&#8211;+&#8212;&#8212;&#8212;+&#8212;&#8212;&#8212;&#8212;&#8212;-+<br />
| id            | int(10) unsigned | NO   | PRI | NULL    | auto_increment |<br />
| host          | char(25)         | NO   | MUL |         |                |<br />
| date          | datetime         | NO   | MUL |         |                |<br />
| metric1 | float            | YES  |     | NULL    |                |<br />
| metric2       | float            | YES  |     | NULL    |                |</p>
<p>mysql&gt; select host,date, metric1 from default_summary limit 5;<br />
| host1 | 2009-11-06 17:00:00 |            5 |<br />
| host2 | 2009-11-06 17:00:00 |            6 |<br />
| host3 | 2009-11-06 17:00:00 |            7 |</p>
<p>Ce que mon ami aimerait avoir est:<br />
| 2009-11-06 17:00:00 |            5 |            6 |            7 |</p>
<p>Donc comment arriver à ce résultat sans passer pas des tables temporaires, qui est aussi une solution dans certains cas?</p>
<p>select date<br />
sum( case host when &#8216;host1&#8242; then metric1 else 0 end ) as host1,<br />
sum( case host when &#8216;host2&#8242; then metric1 else 0 end ) as host2,<br />
sum( case host when &#8216;host3&#8242; then metric1 else 0 end ) as host3,<br />
&#8230;.<br />
from default_summary<br />
group by date;</p>
<p>Il faut évidemment connaitre tous les valeurs de host dés le début&#8230; humm je sens que je vais bientôt coder une petite fonction <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' /> </p>
<p>Il y a plusieurs solutions possibles sous d&#8217;autre SGBD comme Oracle ( PL/SQL, SYS_CONNECT_BY_PATH, Oracle Cross Join ) mais ceci est une autre histoire!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2009/01/06/encore-une-question-reccurante/feed/</wfw:commentRss>
		<slash:comments>0</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>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>
