Plans et Hints MySQL pour les DBA Oracle

MySQL, comme Oracle, permet de visualiser les plans des requêtes et offre, à travers des hints, des moyens pour les influencer. Passer d’Oracle à MySQL est donc ultra-simple ! En théorie d’accord mais en pratique ?

Cet article fait un petit parallèle entre Oracle et MySQL s’agissant de ces 2 outils que sont la commande explain et les hints…

Schéma d’exemple

Pour les besoins de notre test, nous allons créer un schéma d’exemple dans MySQL et dans Oracle ; Pour cela, on créé un table nommée X contiendra 3 colonnes col1, col2 et col3 ; nous la remplirons de 10000 lignes à l’aide du script load.sql généré ci-dessous :

for i in {1..10000}; do
echo "insert into X values ($i, 1, 'XXXXXXXXXXXXXXXXXXX');"
>> load.sql;
done

Voici la commande pour créer notre table, la charger et collecter les statiques sous Oracle :

create table X (
col1 number primary key,
col2 number not null,
col3 varchar2(20));

@load

commit;

create index xidx on x(col2);

exec dbms_stats.gather_table_stats(user,'X');

La syntaxe équivalence sous MySQL:

create table X (
col1 int primary key,
col2 int not null,
col3 varchar(20)) engine INNODB;

source load.sql

create index XIDX on X(col2);

analyze table X;

Lire un plan

Pour obtenir un plan il suffit d’utiliser la commande explain dans les 2 cas. Avec Oracle, vous utiliserez probablement également le package dbms_xplan comme ci-dessous :

explain plan for 
select count(col3) from X where col2=2;

select * from table(dbms_xplan.display(format=>'basic +cost +rows'));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------
Plan hash value: 910516349

------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)|
------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2 (0) |
| 1 | SORT AGGREGATE | | 1 | |
| 2 | TABLE ACCESS BY INDEX ROWID| X | 1 | 2 (0) |
| 3 | INDEX RANGE SCAN | XIDX | 1 | 1 (0) |
------------------------------------------------------------------

Dans le cas de MySQL, utilisez explain tout simplement :

mysql> explain extended 
select count(col3) from X where col2=2;

+----+-------------+-------+------+---------------+------+---------+-------+------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows |
+----+-------------+-------+------+---------------+------+---------+-------+------+
| 1 | SIMPLE | X | ref | XIDX | XIDX | 4 | const | 1 |
+----+-------------+-------+------+---------------+------+---------+-------+------+

1 row in set, 1 warning (0.00 sec)

mysql> show status like '%cost%';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| Last_query_cost | 1.199000 |
+-----------------+----------+
1 row in set (0.00 sec)

Si les commandes sont similaires le résultat est très différent. Ainsi dans le cas de MySQL, regardez comment lire un plan d’exécution dans la documentation, vous voyez que l’accès à la table X est effectué via l’index XIDX (colonne « key ») à partir d’une constante. Contrairement à Oracle, vous ne voyez pas le détail de chaque opération (qui dépend du moteur) ni le coût pour chacune de ces étapes. MySQL ne vous donne simplement que le coût global.

Cette complexité est due au fait que le calcul d’un plan d’exécution est en fait issue de MySQL mais également du moteur de stockage (InnoDB dans mon cas). Par conséquent le détail de chaque opération dépendant des algorithmes de ce moteur, MySQL n’est pas (encore ?) capable de l’afficher directement. De la même manière, le détail des calculs n’est pas accessible pour les adeptes de la 10053.

MySQL loin d’être bête !

Cela étant, ne sous-estimez pas non plus les capacités de MySQL ; par exemple en version 5.5.10, la plugin InnoDB arrive bien à voir quand un index n’est pas pertinent :

mysql> explain extended 
-> select count(col3) from X where col2=1;
+----+-------------+-------+------+---------------+------+-------+-------------+
| id | select_type | table | type | possible_keys | key | rows | Extra |
+----+-------------+-------+------+---------------+------+-------+-------------+
| 1 | SIMPLE | X | ALL | XIDX | NULL | 10241 | Using where |
+----+-------------+-------+------+---------------+------+-------+-------------+
1 row in set, 1 warning (0.00 sec)

Le résultat peut être différent selon les versions et les paramètres. On voit bien ici que, dans mon cas, si l’index XIDX peut-être utilisé, il ne l’est pas ; pour cela InnoDB n’utilise pas des histogrammes de distribution mais un algorithme correspondant plutôt à du dynamique sampling comme c’est expliqué dans cette section de la documentation. C’est loin d’être riducule !

Utiliser des hints

Continuons l’exemple précédent ; avec MySQL, comme pour Oracle, il est possible d’influer sur le plan d’une requête avec des hints. La syntaxe de ces hints est directement appliquée sur les tables de l’ordre SQL et non pas sous la forme d’un commentaire spécial comme pour Oracle ; voici un exemple d’utilisation du hint force index; le premier exemple n’utilise pas le hint alors que le second si :

mysql> explain extended select count(col3) from X where col2=1;
+----+-------------+-------+------+---------------+------+---------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows |
+----+-------------+-------+------+---------------+------+---------+------+-------+
| 1 | SIMPLE | X | ALL | XIDX | NULL | NULL | NULL | 10241 |
+----+-------------+-------+------+---------------+------+---------+------+-------+

1 row in set, 1 warning (0.00 sec)

mysql> show status like '%cost%';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 2081.199000 |
+-----------------+-------------+
1 row in set (0.00 sec)


explain extended select count(col3) from X force index (XIDX) where col2=1;
+----+-------------+-------+------+---------------+------+---------+-------+------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows |
+----+-------------+-------+------+---------------+------+---------+-------+------+
| 1 | SIMPLE | X | ref | XIDX | XIDX | 4 | const | 9999 |
+----+-------------+-------+------+---------------+------+---------+-------+------+

1 row in set, 1 warning (0.00 sec)

mysql> show status like '%cost%';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 2098.799000 |
+-----------------+-------------+
1 row in set (0.00 sec)

Du fait du caractère figé de la syntaxe du hint dans MySQL, peut faire échouer la requête :

mysql> drop index XIDX on X;

mysql> select count(col3) from X force index (XIDX) where col2=1;
ER ROR 1176 (42000): Key 'XIDX' doesn't exist in table 'X'

Dans Oracle, le hint n’est simplement pas appliqué dans ce type de cas :

explain plan for 
select /*+ index(X XIDX) */ count(col3)
from X where col2=1;

select * from table(dbms_xplan.display(format=>'basic +cost +rows'));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------
Plan hash value: 910516349

------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)|
------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 65 (0)|
| 1 | SORT AGGREGATE | | 1 | |
| 2 | TABLE ACCESS BY INDEX ROWID| X | 10000 | 65 (0)|
| 3 | INDEX RANGE SCAN | XIDX | 10000 | 20 (0)|
------------------------------------------------------------------

drop index XIDX;

explain plan for
select /*+ index(X XIDX) */ count(col3)
from X where col2=1;

select * from table(dbms_xplan.display(format=>'basic +cost +rows'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------
Plan hash value: 989401810

--------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)|
--------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 14 (0)|
| 1 | SORT AGGREGATE | | 1 | |
| 2 | TABLE ACCESS FULL| X | 10000 | 14 (0)|
--------------------------------------------------------

Mais pourquoi ça s’appelle un hint alors ?

Si la syntaxe et l’existence des structures liées au hints sont vérifiées, le hint reste pourtant un hint, c’est à dire une indication qui doit être suivi si c’est possible. Ainsi si votre moteur ne peut pas suivre l’indication, elle sera malgré tout ignorée sans que pour autant cela ne déclenche une erreur. Je ne pouvais quand même pas manquer l’opportunité de montrer un exemple où Oracle est capable de prendre un plan que l’on force :

create index xidx on x(col2);

explain plan for
select /*+ index(X XIDX) */ count(col3)
from X
where col2 in (select max(col1) from X);

select * from table(dbms_xplan.display(format=>'basic +cost +rows'));

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------
Plan hash value: 1286095740

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)|
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 67 (0) |
| 1 | SORT AGGREGATE | | 1 | |
| 2 | TABLE ACCESS BY INDEX ROWID | X | 10000 | 65 (0) |
| 3 | INDEX RANGE SCAN | XIDX | 10000 | 20 (0) |
| 4 | SORT AGGREGATE | | 1 | |
| 5 | INDEX FULL SCAN (MIN/MAX)| SYS_C0011832 | 1 | 2 (0) |
---------------------------------------------------------------------------

Et que MySQL 5.5.10 sans patch et malgré le hint n’arrive pas à reproduire :

mysql> explain select count(col3) from X force index (XIDX) where col2 in (select max(col1) from X);
+----+--------------------+-------+------+---------------+------+-------+
| id | select_type | table | type | possible_keys | key | rows |
+----+--------------------+-------+------+---------------+------+-------+
| 1 | PRIMARY | X | ALL | NULL | NULL | 10241 |
| 2 | DEPENDENT SUBQUERY | NULL | NULL | NULL | NULL | NULL |
+----+--------------------+-------+------+---------------+------+-------+
2 rows in set (0.00 sec)

Evidemment tout ça est très général. Les algorithmes, le placement et les structures des données sont très différents. Le jeu des parallèles est donc très limité et s’arrête assez vite. Disons simplement que, si vous avez diagnostiqué un problèmes lié à l’exécution d’une requête avec MySQL ou Oracle, vous avez des outils similaires à votre portée, quoique !