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

<channel>
	<title>dbnewz &#187; index</title>
	<atom:link href="http://www.dbnewz.com/tag/index/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.dbnewz.com</link>
	<description>le blog français sur les SGBD - MySQL, Oracle et plus...</description>
	<lastBuildDate>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>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>Les covering index, de la théorie à la pratique avec MyISAM et InnoDB</title>
		<link>http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/</link>
		<comments>http://www.dbnewz.com/2008/11/20/les-covering-index-de-la-theorie-a-la-pratique-avec-myisam-et-innodb/#comments</comments>
		<pubDate>Wed, 19 Nov 2008 23:56:59 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[pratique]]></category>

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

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

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

		<guid isPermaLink="false">http://www.dbnewz.com/2008/06/05/les-index-mysql-la-serie-dont-vous-etes-le-heros/</guid>
		<description><![CDATA[Un titre sans doute bien étrange pour certains et qui rappelera des souvenirs à d&#8217;autres, surtout à ceux qui ont déjà parcouru un de ces livres &#171;&#160;dont vous êtes le héros&#171;&#160;&#8230;
Afin que les choses soient claires pour tout le monde, je vous propose en fait de participer à la conception du sommaire de la future [...]]]></description>
			<content:encoded><![CDATA[<p>Un titre sans doute bien étrange pour certains et qui rappelera des souvenirs à d&#8217;autres, surtout à ceux qui ont déjà parcouru un de ces livres &laquo;&nbsp;<a href="http://fr.wikipedia.org/wiki/Livre-jeu" target="_blank">dont vous êtes le héros</a>&laquo;&nbsp;&#8230;</p>
<p>Afin que les choses soient claires pour tout le monde, je vous propose en fait de participer à la conception du sommaire de la future série d&#8217;articles sur les index qui sera publiée prochainement sur dbnewz.</p>
<p>L&#8217;indexation est en effet un thème auquel il faut <strong>absolument</strong> s&#8217;intéresser, tout d&#8217;abord pour éviter des catastrophes et bien sûr pour optimiser les performances !</p>
<p><span id="more-43"></span></p>
<p>Les index sont une arme redoutable, à double tranchant : oubliez-les et ils se rappeleront violemment à votre bon souvenir &laquo;&nbsp;ça ne fait pas 5 min que nous sommes en production et la base ne répond plus, la sonde cpu est à 100%, on ne peut même plus se connecter à la base (too many connections), qu&#8217;est ce qui se passe ?!&nbsp;&raquo; Dans le même genre d&#8217;extrêmes, invitez-les partout et vous compliquerez la tâche de l&#8217;optimiseur MySQL (chargé de concevoir le meilleur plan d&#8217;exécution possible pour vos requêtes), ralentirez vos mises à jour et augmenterez la taille de vos bases de données, générant d&#8217;autres problèmes&#8230;<br />
En revanche, une bonne stratégie d&#8217;indexation permet parfois d&#8217;obtenir des gains en performance très importants, parmi les plus importants qu&#8217;on puisse obtenir en agissant sur les réglages d&#8217;une configuration MySQL &laquo;&nbsp;classique&nbsp;&raquo;.</p>
<p>Ce que je vous propose donc, c&#8217;est non pas de m&#8217;envoyer des copies de vos plans d&#8217;exécution ou les schémas de vos tables, mais plutôt de poster ici vos attentes, les zones d&#8217;ombre que vous souhaiteriez éclaircir concernant les index.<br />
Exemples :<br />
&laquo;&nbsp;Pourquoi un index disponible n&#8217;est pas pris en compte ?&nbsp;&raquo;<br />
&laquo;&nbsp;En quoi indexer peut améliorer les performances ?&nbsp;&raquo;<br />
&laquo;&nbsp;Quels sont les champs à indexer ?&nbsp;&raquo;, etc, etc.</p>
<p>Profitez de l&#8217;occasion qui vous est donnée pour poster dans les commentaires de ce billet vos problématiques, vos souhaits, j&#8217;en tiendrai compte lors de la conception des différents chapitres que comportera cette série qui deviendra du &laquo;&nbsp;cousu main&nbsp;&raquo; pour tous les participants, intéressant non ?</p>
<p>A vos claviers <img src='http://www.dbnewz.com/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' /> </p>
]]></content:encoded>
			<wfw:commentRss>http://www.dbnewz.com/2008/06/05/les-index-mysql-la-serie-dont-vous-etes-le-heros/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Choisir l&#8217;implémentation de ses index : &#171;&#160;B-TREE&#160;&#187; ou &#171;&#160;HASH&#160;&#187;, quelles différences ?</title>
		<link>http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/</link>
		<comments>http://www.dbnewz.com/2008/05/19/choisir-limplementation-de-ses-index-b-tree-ou-hash-quelles-differences/#comments</comments>
		<pubDate>Sun, 18 May 2008 23:12:15 +0000</pubDate>
		<dc:creator>arnaud</dc:creator>
				<category><![CDATA[4.1]]></category>
		<category><![CDATA[5.0]]></category>
		<category><![CDATA[5.1]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[tuning]]></category>

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