Utiliser une sous-requête c’est mal ?

avril 23, 2013

Jusqu’en MySQL 5.5 inclus, l’utilisation de sous-requêtes peut, dans certain cas, être la cause de problèmes de performances (l’optimiseur est bien meilleur en MySQL 5.6, MariaDB 5.5 et MariaDB 10).

Récemment j’ai eu un souci en prod, après une MEP, avec une requête qui durait en moyenne plus de 1000 secondes

Inutile de préciser que dans un environnement OLTP, application web plus précisément, ce n’est pas jouable, elle a donc été virée très rapidement (mais pas le développeur qui l’a créée…)

 

Alors passons sur le fait que ce genre de problème devrait être identifié avant d’arriver sur la prod, et voyons à quoi ressemble une requête qui peut prendre 45 minutes:


SELECT DISTINCT OtherID
FROM MyTable
WHERE Status = 1
   AND Stage = 4
   AND Invisible = 0
   AND MainID IN
       (
       SELECT MainID
       FROM MyTable
       WHERE OtherID = 2779262
           AND Stage = 0
           AND Invisible = 0
       )

 

7 rows in set (44 min 56.66 sec)

 

On a donc une belle sous-requête dans la clause IN de la requête externe. La table est en InnoDB, elle contient 310 millions de lignes pour une taille de 62Go.

 

Qu’en dit le QEP ?

 

*************************** 1. row ***************************
          id: 1
 select_type: PRIMARY
       table: MyTable
        type: ref
possible_keys: StageMainIDOtherID
         key: StageMainIDOtherID
     key_len: 1
         ref: const
        rows: 155316386
       Extra: Using where; Using temporary
*************************** 2. row ***************************
          id: 2
 select_type: DEPENDENT SUBQUERY
       table: MyTable
        type: index_subquery
possible_keys: MainIDOtherIDStatus, autre index…
         key: MainIDOtherIDStatus
     key_len: 8
         ref: func,const
        rows: 1
       Extra: Using where

 

Malgré la présence d’index (pertinent ?), l’optimiseur analyse la moitié des lignes de la table (155316386), ce qui sur une grosse table peut prendre pas mal de temps, vous en conviendrez.

Niveau status variables

 

| Created_tmp_disk_tables                  | 0             |
| Created_tmp_files                        | 0             |
| Created_tmp_tables                       | 1             |

| Handler_read_first                       | 0             |
| Handler_read_key                         | 44383976      |
| Handler_read_last                        | 0             |
| Handler_read_next                        | 151576920     |
| Handler_read_prev                        | 0             |
| Handler_read_rnd                         | 0             |
| Handler_read_rnd_next                    | 8             |
| Handler_write                            | 7             |

 

Cette vision un peu plus bas niveau montre un nombre élevé de lecture lors du parcours transverse de l’index (Handler_read_next), en clair ce n’est pas vraiment bon pour les performances.

A noter que la clause DISTINCT génère la création d’une table temporaire, son remplissage et évidemment sa lecture (Created_tmp_tables, Handler_write, Handler_read_rnd_next) .

Temps d’exécution unitaire (w/ mysqlslap)

 

En mode unitaire, sur un serveur en configuration prod mais sans charge, mysqlslap nous donne:

    Minimum number of seconds to run all queries: 478.936 seconds.

Comment faire pour rendre cette requête utilisable ?

Plusieurs choix, le plus simple est de la réécrire (pas de modification de la table ou des index). Une sous-requête de ce type se converti facilement en jointure:

 

SELECT DISTINCT r1.OtherID
FROM MyTable r1 INNER JOIN MyTable r2 USING (MainID)
WHERE r1.Status = 1
   AND r1.Stage = 4
   AND r1.Invisible = 0
   AND r2.OtherID = 2779262
   AND r2.Stage = 0
   AND r2.Invisible = 0

Qu’en dit le QEP ?

 
*************************** 1. row ***************************
          id: 1
 select_type: SIMPLE
       table: r2
        type: ref
possible_keys: OtherIDStageStatusInvisibleMainID, autre index…
         key: OtherIDStageStatusInvisibleMainID
     key_len: 5
         ref: const,const
        rows: 234
       Extra: Using where; Using index; Using temporary
*************************** 2. row ***************************
          id: 1
 select_type: SIMPLE
       table: r1
        type: ref
possible_keys: MainIDOtherIDStatus,StageMainIDOtherID
         key: MainIDOtherIDStatus
     key_len: 4
         ref: viaduc.r2.MainID
        rows: 1
       Extra: Using where

 

234 lignes à analyser au lieu de 155316386. Ça à l’air meilleur :)

Un index couvrant en prime (Extra: Using index), c’est toujours bon à prendre.

Niveau status variables

 

| Created_tmp_disk_tables                  | 0             |
| Created_tmp_files                        | 0             |
| Created_tmp_tables                       | 1             |

| Handler_read_first                       | 0             |
| Handler_read_key                         | 236           |
| Handler_read_last                        | 0             |
| Handler_read_next                        | 705           |
| Handler_read_prev                        | 0             |
| Handler_read_rnd                         | 0             |
| Handler_read_rnd_next                    | 8             |
| Handler_write                            | 7             |

 

Avec beaucoup moins de ligne analysées, le nombre de Handler_read_next est extrêmement bas par rapport à la la requête précédente: 705 au lieu de 151576920. On a donc un faible nombre de lecture lors parcours transverse de l’arbre d’index. En clair, c’est plutôt une bonne chose.

 

Le temps d’exécution de la requête à froid est: 7 rows in set (1.17 sec)

A mettre en perspective avec les 45 minutes de la première version…

Temps d’exécution unitaire (w/ mysqlslap)

 

    Minimum number of seconds to run all queries: 0.001 seconds

 

Le faisceau d’indice valide bien un “meilleur” (c’est un euphémisme) temps d’exécution minimal unitaire (0.001 s vs 478.936 s)

 

Je pense que l’on va garder cette version de la requête pour la prod :) .

 

Alors que faut il en tirer comme conclusion ?

 

Il est légitime de se poser la question de proscrire ou pas les sous-requêtes si on n’est pas en MySQL 5.6+ ou MariaDB 5.5+

Je conseillerai surtout de toujours tester ses requêtes avant de les pousser en prod… notamment en la passant au révélateur EXPLAIN.

En effet dans le cas de la 1ère requête, rows: 155316386, est un bon indicateur de “ça risque de partir en couille !”.

La requête peut également être réécrite de la manière suivante:

 

SELECT DISTINCT OtherID
FROM
   (
   SELECT MainID
   FROM MyTable
   WHERE Invisible = 0
       AND Stage = 0
       AND OtherID = 2779262
   ) dt
     INNER JOIN MyTable r
       WHERE r.MainID = dt.MainID
           AND r.Invisible = 0
           AND r.Stage = 4
           AND r.Status = 1

oh la belle sous requête ! (ou table dérivée pour les puristes)

Qu’en dit le QEP ?

 

*************************** 1. row ***************************
          id: 1
 select_type: PRIMARY
       table: <derived2>
        type: ALL
possible_keys: NULL
         key: NULL
     key_len: NULL
         ref: NULL
        rows: 235
       Extra: Using temporary
*************************** 2. row ***************************
          id: 1
 select_type: PRIMARY
       table: r
        type: ref
possible_keys: MainIDOtherIDStatus,StageMainIDOtherID
         key: MainIDOtherIDStatus
     key_len: 4
         ref: dt.MainID
        rows: 1
       Extra: Using where
*************************** 3. row ***************************
          id: 2
 select_type: DERIVED
       table: MyTable
        type: ref
possible_keys: OtherIDStageStatusInvisibleMainID, autre index…
         key: OtherIDStageStatusInvisibleMainID
     key_len: 5
         ref:
        rows: 234
       Extra: Using where; Using index
 
 

54990 (235 x 1 x 234) lignes à analyser + index couvrant. Ça n’a pas l’air trop mal tout ça !

Cette requête combine sous-requête + jointure et jusqu’ici… même pas peur.

Niveau status variables

 

| Created_tmp_disk_tables                  | 0             |
| Created_tmp_files                        | 0             |
| Created_tmp_tables                       | 2             |

| Handler_read_first                       | 0             |
| Handler_read_key                         | 236           |
| Handler_read_last                        | 0             |
| Handler_read_next                        | 705           |
| Handler_read_prev                        | 0             |
| Handler_read_rnd                         | 0             |
| Handler_read_rnd_next                    | 244           |
| Handler_write                            | 242           |

 

Les indications bas niveau fournit par les handlers sont assez proche de celles fournit par l’exécution de la version jointure seule, de la requête.

Seules les informations (Created_tmp_tables, Handler_read_rnd_next,  Handler_write) liées aux 2 tables temporaires créées (1 pour la table dérivée + 1 pour le DISTINCT) sont différentes.

 

En(un peu plus) clair: Handler_write = 235 lignes écrites dans la table dérivée + 7 lignes du résultat final après dédoublonnage (DISTINCT) = 242

 

Ces 2 tables étant suffisamment petite pour tenir entièrement en mémoire (Created_tmp_disk_tables=0) le surcoût devrait être négligeable par rapport à la la version jointure seule de la requête.

 

Le temps d’exécution de cette requête à froid est: 7 rows in set (1.23 sec)

Il est équivalent à la jointure.

 

Temps d’exécution unitaire (w/ mysqlslap)

 

    Minimum number of seconds to run all queries: 0.001 seconds

 

On retrouve bien des temps d’exécution équivalent au millième près. CQFD

Il y a souvent plusieurs façon d’écrire une requête, mais elles sont rarement équivalentes en matière de temps d’exécution. La commande EXPLAIN fait partie du package minimal que tout développeur doit connaître. Ajouter à cela un environnement de test et un zeste de bon sens et il devient alors possible d’éliminer une partie des soucis de prod et d’éviter de perdre du temps inutilement.



 

Divers

--------------
mysql  Ver 14.14 Distrib 5.5.24, for Linux (x86_64) using readline 5.1

Server version:        5.5.24-55-log Percona Server (GPL), Release 26.0
Protocol version:    10
Connection:        Localhost via UNIX socket
Server characterset:    utf8
Db     characterset:    utf8
Client characterset:    utf8
Conn.  characterset:    utf8
--------------

 

CREATE TABLE MyTable (
 MyTableID int(11) NOT NULL AUTO_INCREMENT,
 MainID int(11) NOT NULL DEFAULT '0',
 OtherID int(11) NOT NULL DEFAULT '0',
 Stage tinyint(4) NOT NULL DEFAULT '0',
 Status int(11) DEFAULT '0',
 Invisible tinyint(1) NOT NULL DEFAULT '0',
 PRIMARY KEY (MyTableID),
 KEY OtherIDStageStatusInvisible (OtherID,Stage,Status,Invisible),
 KEY OtherIDStatusInvisible (OtherID,Status,Invisible),
 KEY MainIDOtherIDStatus (MainID,OtherID,Status),
 KEY OtherIDStageStatusInvisibleMainID (OtherID,Stage,Status,Invisible,MainID),
 KEY StageMainIDOtherID (Stage,MainID,OtherID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci

 

*************************** 1. row ***************************
          Name: MyTable
        Engine: InnoDB
       Version: 10
    Row_format: Compact
          Rows: 310632909
Avg_row_length: 52
    Data_length: 16334700544
  Index_length: 49877909504
     Data_free: 7340032
     Collation: utf8_swedish_ci
8

Mon premier entretien

avril 3, 2013
Tags: ,

 

Il y a quelques jours j’ai passé mon premier entretien, coté recruteur, pour un poste de DBA MySQL stagiaire chez Viadeo. Ce fut une expérience particulièrement enrichissante. Pour ne rien vous cacher j’ai eu un peu d’appréhension et une petite dose de stress :) mais beaucoup moins que le candidat reçu de jour là.

Ça m’a rappelé le fait que j’ai passé beaucoup beaucoup beaucoup d’entretiens dans ma vie professionnelle et voici ce que j’en ai tiré:

 

- Stress

C’est normal d’avoir de l’appréhension surtout lorsque l’on débute sa carrière professionnelle (et même plus tard). Différentes méthodes existent pour gérer le stress, la mienne est déjà de l’accepter (que celui qui n’a jamais stressé jette le premier CV). C’est humain et dans certaines situations il peut sauver notre vie. J’essaye néanmoins de le contenir en diminuant la part de la part d’incertitude sur laquelle j’ai une influence (la cause principale du stress).

Comment diminuer cette part de part d’incertitude ?

 

  • Préparer l’entretien en se renseignant sur l’entreprise et sur le poste à pourvoir. Cela permet de montrer son l’intérêt pour l’entreprise et pour le poste. Ça permet aussi de s’auto-rassurer et d’avoir des choses à dire qui ne sont pas que du registre “moi je”

  • Maîtriser l’histoire de son parcours scolaire et/ou professionnel. C’est à dire pouvoir notamment expliquer les éventuelles “aspérités” de son parcours et en donner une image positive.

 

La meilleure façon que j’ai trouvé pour améliorer tout ça a été de passer beaucoup d’entretiens, notamment pour des postes qui ne m’intéressait pas :) . L’avantage est de roder son discours dans un cadre où la pression est moindre. Et ça reste un superbe moyen de s’entraîner en condition réelle. Je le recommande fortement !

 

- Sur un pied d’égalité

La chose la plus important que j’ai comprise dans le monde de l’entreprise et notamment lors du processus d’entretien, c’est que l’entreprise ne vous donne pas un boulot pour vous faire plaisir mais qu’elle veut acheter une compétence, une expertise voir un potentiel car elle va lui (r)apporter quelque chose. Alors comme tout processus d’achat, l’idée est d’acheter à un moindre prix et d’en tirer un maximum, c’est le jeu, mais la chose importante à garder en tête est que l’état d’esprit à avoir est “échange de  compétence / potentiel / expertise contre rémunération” et surtout pas celui de “subalterne à supérieur”. Surtout que, pour la petite histoire, il y a des chances que le recruteur en face de vous soit dans un statut précaire avec un salaire inférieur ou égale à celui de vos prétentions… ça remet les choses en perspective !

 

- Salaire

Combien je vaux (dans le monde de l’entreprise) ???  c’est une question que je me suis souvent posée et je me suis rendu compte qu’il est difficile d’y répondre, en tout cas dans mon domaine,  mais je ne pense pas être le seul. Il y a pas mal de ressources sur internet et dans les magasines spécialisés mais je me demande où ils trouvent leurs chiffres car ils n’ont jamais correspondu à mon salaire. Autre difficulté, la question du salaire est tabou en France, ce n’est pas évident de savoir combien gagne quelqu’un à poste et niveau équivalent.

Le salaire est important mais il ne dit pas tout (productivité, efficacité,…). De plus ce n’est pas le seul paramètre à prendre en compte: les avantages en nature, l’expérience que l’on va en tirer (surtout pour les jeunes diplômés), le CV (une belle expérience ou une grande entreprise peuvent permettre de faire de grands bonds niveau salaire par la suite), le lieu géographique, les perspectives d’évolutions,… sont autant de variables qui peuvent (doivent) entrer en compte dans le choix du job.

Je n’ai pas de méthodes miracles (si vous en avez faite tourner :D ), mais là encore je n’ai pas trouvé mieux que de passer des entretiens. Ça donne une bonne idée de ce que le marché peut payer pour un poste précis. Et puis si vous n’êtes pas satisfait par votre salaire (ou autre) rien ne vous empêche d’aller voir ailleurs par la suite.

 

- Réponse négative

Rien de personnel ! Inadéquation entre le poste et le candidat ou un autre candidat semble tout simplement mieux adapté. La réponse négative (ou la non réponse) fait partie intégrante du processus de recherche d’emploi. Eh ben oui, on ne peut pas plaire à tout le monde !

C’est comme ça et il faut l’accepter. Cet entretien aura servi d’entraînement pour être meilleur pour le prochain.

Lâche pas l’affaire !

 

A une dernière chose, le poste de stagiaire VIADEO : DBA MySQL (H/F) est toujours ouvert :)

2

Offre de stage VIADEO : DBA MySQL (H/F)

mars 6, 2013
Tags: ,

Une fois n’est pas coutume, un article pour vous parler d’une offre de stage (avec possibilité d’embauche) en tant que DBA MySQL.

Je vais faire simple, Viadeo (réseau social professionnel) cherche à étoffer son équipe MySQL. On cherche donc un DBA MySQL (en devenir). Si  tu souhaites rejoindre une boite en plein expansion, où il fait bon travailler et où il y a plein de challenges technologiques à relever, tente ta chance :)

A oui, j’ai failli oublier, tu vas travailler avec moi;)

Informations et blabla RH  :

http://www.viadeo.com/fr/jobs/offer/?jobOfferId=00212vkp60fz97pw

 

3

Meetup les bases de données relationnelles avec PHP

février 25, 2013

L’AFUP (Association Française des Utilisateurs de PHP) vous donne rendez-vous le 28 Février chez Linagora pour une soirée ayant pour thème: Les bases de données relationnelles avec PHP.

Les deux conférenciers qui animeront cette soirée sont à la hauteur du sujet choisi puisqu’il s’agit de Olivier Dasini, expert MySQL chez Viadeo qui présentera une méthodologie pour optimiser les performances des requêtes d’une application et Thomas Reiss, expert PostgreSQL à Dalibo, nous détaillera les fonctionnalités de PostgreSQL, en s’appuyant sur différents retours d’expérience pour mieux nous en faire découvrir les capacités.

Pour clôturer la soirée, Serge Frezefond, Cloud Solutions Architect chez SkySQL, présentera le driver MySQL sous licence PHP.

Comme toujours le RDV est gratuit et ouvert à tous, n’hésitez donc pas en parler autour de vous !

Le rendez-vous se tiendra le jeudi 28 février dans les locaux de chez Linagora, à partir de 18h30 (ouverture des portes), 80 Rue Roque de Fillol 92800 Puteaux

Début des sessions à 19h00 et nombre de places limités

Pour vous inscrire : http://afup.org/pages/rendezvous/index.php?id=18

A jeudi

 

0

MySQL 5.6

février 21, 2013

Cela fait quelques jours maintenant que MySQL 5.6 est disponible pour la production. Un impressionnant travail a été effectué par les équipe d’Oracle, voici un petit résumé des principales évolution vu par Peter Zaitsev.

L’événement dans l’événement, c’est la « polémique » sur les performances de la 5.6, par rapport à MySQL 5.5 mais surtout par rapport à MariaDB 5.5.

Dimitri (Oracle), montre que MySQL 5.6 écrasent MySQL 5.5 et MariaDB 5.5. Axel (MariaDB), de son coté montre que la 5.6 est moins performante que MariaDB 5.5, ainsi que MySQL 5.5. Peter (Percona), de son coté montre que les perfs de MySQL 5.6 sont en retrait par rapport à celles de MySQL 5.5. Au passage, je note que Percona server 5.5 n’est pas testé… Last but not least, Mark (Facebook) , montre que sa version maison 5.1.63 patchée met tout le monde d’accord. Il montre aussi que le coût du performance schemas n’est pas négligeable.

 

Mon avis

MySQL 5.6 est indéniablement la meilleur version MySQL à ce jour, mais si je devais la mettre en prod, j’attendrai septembre. Après en ce qui concerne la comparaison technique avec MariaDB 5.5, Percona server 5.5, MySQL 5.5… je suis persuadé que 80% des utilisateurs ne verront pas de différences notables (niveau perf). Pour les 20% qui reste, un seul conseil testez !

L’autre critère de comparaison est plus de nature philosophique (voir politique), les modèles proposés par Oracle, Monty et Percona ne sont pas vraiment identiques :/

Autre point, il faut toujours garder à l’esprit que les benchs ne donnent que le résultat de ce qu’on test, en d’autres termes, ils sont pas toujours objectif et souvent pas représentatif de votre workload. Un seul conseil testez !

Merci à Oracle, Monty program et Percona de se tirer la bourre. Continuez à tirer la base de données open source la plus utilisée vers le haut :) (ainsi que Facebook, Twitter, Google … et le reste de la communauté)

 

Liens

http://www.mysql.com/downloads/mysql/

https://blogs.oracle.com/supportingmysql/entry/server_defaults_changes_in_mysql

http://www.mysqlperformanceblog.com/2013/01/27/mysql-5-6-improvements-in-the-nutshell/

http://dimitrik.free.fr/blog/archives/2013/02/mysql-performance-mysql-56-vs-mysql-55-vs-mariadb-55.html

http://dimitrik.free.fr/blog/archives/02-01-2013_02-28-2013.html

http://blog.mariadb.org/sysbench-oltp-mysql-5-6-vs-mariadb-10-0/

http://www.mysqlperformanceblog.com/2013/02/18/is-mysql-5-6-slower-than-mysql-5-5/

https://www.facebook.com/notes/mysql-at-facebook/my-mysql-is-faster-than-your-mysql/10151250402570933

 

 

 

 

1

Full table scan vs Full index scan part2-2

janvier 30, 2013

2/ FTS ou FIS

(Aller à la 1ère partie)
Avant de répondre explicitement à la question, un petit zoom sur l’une des nombreuses nouveautés de MySQL 5.6. La commande EXPLAIN s’est enrichie de la clause format=json. Elle permet d’avoir une version un peu plus détaillée que l’EXPLAIN classique.

Query 1/ EXPLAIN format=json  SELECT d,avg(price) FROM bills GROUP BY d\G
*************************** 1. row ***************************
EXPLAIN: {
« query_block »: {
« select_id »: 1,
« grouping_operation »: {
« using_filesort »: false,
« table »: {
« table_name »: « bills »,
« access_type »: « index »,
« key »: « d »,
« used_key_parts »: [
"d"
],
« key_length »: « 5″,
« rows »: 176268,
« filtered »: 100
}
}
}
}

Query 4/ EXPLAIN format=json  SELECT d,avg(price) FROM bills IGNORE INDEX(d) GROUP BY d\G
*************************** 1. row ***************************
EXPLAIN: {
« query_block »: {
« select_id »: 1,
« grouping_operation »: {
« using_temporary_table »: true,
« using_filesort »: true,
« table »: {
« table_name »: « bills »,
« access_type »: « ALL »,
« rows »: 176268,
« filtered »: 100
}
}
}
}

Sans surprise, on retrouve les informations de la version classique de l’EXPLAIN, a savoir:
FIS d’un coté (choix de l’optimiseur) contre FTS + création d’une table temporaire + tri de l’autre. Néanmoins, pour certains types de requêtes, les informations générées par la clause format=json peuvent être plus pertinentes.

Comme nous l’avons également vu dans la première partie de cet article, le GROUP BY de MySQL génère systématiquement un tri (« using_filesort »: true,). Ce tri n’étant pas toujours nécessaire, il y a une astuce pour ne pas le générer. Ajouter la clause ORDER BY NULL

Query 5/ EXPLAIN format=json  SELECT d,avg(price) FROM bills IGNORE INDEX(d) GROUP BY d ORDER BY NULL\G
*************************** 1. row ***************************
EXPLAIN: {
« query_block »: {
« select_id »: 1,
« ordering_operation »: {
     « using_filesort »: false,
« grouping_operation »: {
       « using_temporary_table »: true,
       « using_filesort »: false,
« table »: {
« table_name »: « bills »,
« access_type »: « ALL »,
« rows »: 176268,
« filtered »: 100
}
}
}
}
}

Query 5/ EXPLAIN  SELECT d,avg(price) FROM bills IGNORE INDEX(d) GROUP BY d ORDER BY NULL\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: bills
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 176268
       Extra: Using temporary

SHOW STATUS LIKE ‘Last_query_cost’;
+—————–+————–+
| Variable_name   | Value        |
+—————–+————–+
| Last_query_cost | 35861.599000 |
+—————–+————–+

Il y a une incidence notable sur le coût de la requête, 35861.599 au lieu de 212129.599.
Pas vraiment surprenant !

Niveau performance ça donne quoi ?

Quick bench (au meilleur des 4 tests ie 4 fois 100 runs):

Server version:        5.6.9-rc MySQL Community Server (GPL)

Taille: environs 16 Mo ((Données + Index))

Configuration de l’instance:
Innodb buffer pool size = 32 Go
max_heap_table_size = tmp_table_size = 512 Mo

Query 1:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills  GROUP BY d ; »
Benchmark
Average number of seconds to run all queries: 0.203 seconds
   Minimum number of seconds to run all queries: 0.199 seconds
Maximum number of seconds to run all queries: 0.224 seconds

Query 2:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills USE INDEX(d) GROUP BY d ; »
Benchmark
Average number of seconds to run all queries: 0.206 seconds
   Minimum number of seconds to run all queries: 0.198 seconds
Maximum number of seconds to run all queries: 0.217 seconds

Query 3:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills FORCE INDEX(d) GROUP BY d ; »
Benchmark
Average number of seconds to run all queries: 0.203 seconds
   Minimum number of seconds to run all queries: 0.199 seconds
Maximum number of seconds to run all queries: 0.219 seconds

Le temps d’exécution des requêtes 1, 2 et 3 est le même ce qui est logique car le plan d’exécution est le même, en d’autres termes, forcer l’utilisation de l’index ou non n’a pas d’incidence sur la durée des requêtes.
De plus ce résultat confirme que le coût bizarre du FORCE INDEX est bien un bug (cf la première partie de cet article).

Query 4:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills IGNORE INDEX(d) GROUP BY d ; »
Benchmark
Average number of seconds to run all queries: 0.168 seconds
   Minimum number of seconds to run all queries: 0.166 seconds
Maximum number of seconds to run all queries: 0.172 seconds

Plusieurs indications:

  • Le full table scan donne un meilleur temps d’exécution que le full index scan:
    • 0.166 vs 0.199
    • D’une manière générale, le FTS et plus rapide que le FIS sauf dans le cas d’un index covering (using index)

 

  • Le coût de la création de la table temporaire et du tri n’a pas un impact suffisant pour que le temps d’exécution soit supérieur au FIS:
    • Created_tmp_tables: 1, Created_tmp_disk_tables: 0,  Sort_merge_passes: 0
    • Seules 231 lignes sont à trier (Handler_read_rnd = 231)
    • Ces opérations se déroulent en mémoire (ie pas d’accès disque). C’est essentiellement un coût processeur.

Et avec l’astuce ORDER BY NULL ?

Query 6:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills USE INDEX(d) GROUP BY d ORDER BY NULL; »
Benchmark
Average number of seconds to run all queries: 0.203 seconds
   Minimum number of seconds to run all queries: 0.199 seconds
Maximum number of seconds to run all queries: 0.218 seconds

Pas de tri, pas d’incidence sur les perfs, trivial

Query 5:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills IGNORE INDEX(d) GROUP BY d ORDER BY NULL; »
Benchmark
Average number of seconds to run all queries: 0.168 seconds
   Minimum number of seconds to run all queries: 0.166 seconds
Maximum number of seconds to run all queries: 0.172 seconds

Le coût du tri est faible, négligeable => pas d’incidences sur les perfs

En diminuant la taille des paramètres max_heap_table_size & tmp_table_size :

Taille: environs 16 Mo ((Données + Index))

Configuration de l’instance:
Innodb buffer pool size = 32 Go
max_heap_table_size = tmp_table_size = 16 Ko

Infos sur les variables tmp_table_size & max_heap_table_size

En diminuant max_heap_table_size & tmp_table_size, pour forcer la création de la table temporaire sur le disque:

Query 2:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills USE INDEX(d) GROUP BY d; »
Benchmark
Average number of seconds to run all queries: 0.203 seconds
   Minimum number of seconds to run all queries: 0.199 seconds
Maximum number of seconds to run all queries: 0.221 seconds

Pas d’incidence sur les perfs, car pas de tri donc pas de créations de table temporaire. Trivial !

Query 4:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills IGNORE INDEX(d) GROUP BY d; »
Benchmark
Average number of seconds to run all queries: 0.767 seconds
   Minimum number of seconds to run all queries: 0.757 seconds
Maximum number of seconds to run all queries: 0.778 seconds

Sans surprise, c’est plus long. La table temporaire étant créée sur disque.

Query 5:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills IGNORE INDEX(d) GROUP BY d ORDER BY NULL; »
Benchmark
Average number of seconds to run all queries: 0.765 seconds
   Minimum number of seconds to run all queries: 0.756 seconds
Maximum number of seconds to run all queries: 0.773 seconds

Le tri étant fait en mémoire, les variables max_heap_table_size & tmp_table_size n’ont pas d’incidences sur le tri.

En diminuant la taille du paramètre innodb_buffer_pool_size :

Taille: environs 16 Mo ((Données + Index))

Configuration de l’instance:
Innodb buffer pool size = 8 Mo
max_heap_table_size = tmp_table_size = 512 Mo

Query 2:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills USE INDEX(d) GROUP BY d ; »
Benchmark
Average number of seconds to run all queries: 1.214 seconds
   Minimum number of seconds to run all queries: 1.174 seconds
Maximum number of seconds to run all queries: 1.243 seconds

Query 4:
daz@mysql:~/mysql_5_6_5$ ./mysqlslap –create-schema=test -c1 -i100 -q »SELECT avg(price) FROM test.bills IGNORE INDEX(d) GROUP BY d ; »
Benchmark
Average number of seconds to run all queries: 0.179 seconds
   Minimum number of seconds to run all queries: 0.174 seconds
Maximum number of seconds to run all queries: 0.185 seconds

L’écart de performance est encore plus important avec des données qui ne tiennent (apparemment) pas entièrement en mémoire. Le sur-coût I/O imposé par la double recherche (dans l’index secondaire, puis random I/O dans la clé primaire) n’est pas anodin.

Conclusion
- Le choix de l’optimiseur n’est parfois pas le bon
- Le choix le plus intuitif n’est parfois pas le plus performant
- Toujours valider ses hypothèses par la pratique
- Se méfier des Full Index Scan, qui peuvent être vraiment plus coûteux qu’un Full Table Scan en cas de non covering index

2

Full table scan vs Full index scan part1-2

janvier 8, 2013
Tags:

1/ Le mystère de la variable Last_query_cost

MySQL utilise un optimiseur à base de coûts. Le plan d’exécution de la requête choisit est celui dont le coût est le plus faible. Ce dernier peut être visualisé grâce à la variable Last_query_cost.
Son unité est le coût (encore lui) des accès aléatoires en lecture de pages de 4ko.
Étrangement cette variable est assez peu/mal documentée. Voici ce qu’on retrouve dans la doc officielle de MySQL
Je cite:
Last_query_cost
The total cost of the last compiled query as computed by the query optimizer. This is useful for comparing the cost of different query plans for the same query. The default value of 0 means that no query has been compiled yet. The default value is 0.Last_query_cost has session scope.
TheLast_query_cost value can be computed accurately only for simple “flat” queries, not complex queries such as those with subqueries orUNION. For the latter, the value is set to 0.

La valeur de Last_query_cost est parfois déconcertante, même avec MySQL 5.6. Voyez par vous même…

Quelques informations avant d’entrer dans le vif du sujet.
Server version:        5.6.9-rc MySQL Community Server (GPL)

Configuration de l’instance:
Innodb buffer pool size = 32 Go
max_heap_table_size = tmp_table_size = 512 Mo

Table:
CREATE TABLE `bills` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `d` datetime NOT NULL,
 `price` smallint(5) unsigned NOT NULL,
 PRIMARY KEY (`id`),
 KEY `d` (`d`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci

SHOW TABLE STATUS:
*************************** 1. row ***************************
          Name: bills
        Engine: InnoDB
       Version: 10
    Row_format: Compact
          Rows: 176268
Avg_row_length: 56
    Data_length: 9961472
Max_data_length: 0
  Index_length: 4210688
     Data_free: 2097152
                   …
     Collation: utf8_swedish_ci
=> Données + Index:  environs 16 Mo

L’idée est d’exécuter la requête ci-dessous, avec différents “hint” est de voir les plans d’exécution (QEP) générés par MySQL ainsi que leurs coûts respectifs.
SELECT d,avg(price) FROM bills GROUP BY d

2 QEP possibles:
- Faire un full table scan, créer une table temporaire pour regrouper et ordonner (particularité du GROUP BY de MySQL) les données en fonction de la date (d) pour calculer la moyenne des prix (avg(price)).
- Faire un full index scan sur d, ce qui évite le tri et la création de la table temporaire. Cependant le “price” n’étant pas dans l’index, l’info doit être récupéré dans la couche “data”

Query 1/ EXPLAIN  SELECT d,avg(price) FROM bills GROUP BY d\G
*************************** 1. row ***************************
          id: 1
 select_type: SIMPLE
       table: bills
        type: index
possible_keys: NULL
         key: d
     key_len: 5
         ref: NULL
        rows: 176268
       Extra: NULL

Le choix de l’optimiseur est assez intuitif, il se sert du fait que l’index d est trié (type: index = full index scan) pour optimiser le GROUP BY (regroupement + tri), c’est à dire ne pas créer de table temporaire.

Le coût de cette opération est:
SHOW STATUS LIKE ‘Last_query_cost;
+—————–+—————+
| Variable_name   | Value         |
+—————–+—————+
| Last_query_cost | 212129.599000 |
+—————–+—————+

Question de psychopathe, que se passe t’il si je force l’optimiseur à utiliser l’index qu’il dit qu’il va utiliser ? (c’est vicieux je sais :D )

Query 2/ EXPLAIN  SELECT d,avg(price) FROM bills USE INDEX(d) GROUP BY d\G *************************** 1. row ***************************
          id: 1
 select_type: SIMPLE
       table: bills
        type: index
possible_keys: NULL
         key: d
     key_len: 5
         ref: NULL
        rows: 176268
       Extra: NULL

Alors pour être précis, là je ne le force pas vraiment, je l’incite (fortement) à l’utiliser. Mais sans surprise, il l’utilise (ouf !), en d’autre terme le QEP est le même que précédemment.

Et qu’en est il du coût ?

SHOW STATUS LIKE ‘Last_query_cost;
+—————–+—————+
| Variable_name   | Value         |
+—————–+—————+
| Last_query_cost | 212129.599000 |
+—————–+—————+

Pareil que précédemment, circuler il n’y a rien à voir !

Ok est si là je le force vraiment, avec FORCE INDEX (principale différence entre USE INDEX et FORCE INDEX est qu’avec le second l’optimiseur choisira (si c’est possible bien sûr) d’utiliser l’index en lieu est place d’un full table scan)

Query 3/  EXPLAIN  SELECT d,avg(price) FROM bills FORCE INDEX(d) GROUP BY d\G
*************************** 1. row ***************************
          id: 1
 select_type: SIMPLE
       table: bills
        type: index
possible_keys: NULL
         key: d
     key_len: 5
         ref: NULL
        rows: 176268
       Extra: NULL

Là encore, (sans surprise) le QEP est le même que précédemment. Et le coût ?

SHOW STATUS LIKE ‘Last_query_cost;
+—————–+—————+
| Variable_name   | Value         |
+—————–+—————+
| Last_query_cost | 387790.599000 |
+—————–+—————+

… WTF …
Bon là pour être franc, je n’ai pas d’explications rationnelles. Serait ce une feature ? (rationnelle on à dit !!!) ok ok bah un bug alors.
Inutile de préciser que je me suis empressé de tester avec d’autres versions de MySQL (psychopate !) et le résultat est sans appel:
Avec MySQL 5.5, MariaDB 5.5 & Percona server 5.5 le coût du FORCE INDEX est identique aux 3 autres coûts.

J’opterai donc pour une petite régression de l’optimiseur, bref un BUG

Soit ! si j’empêche l’optimiseur d’utiliser l’index:
Query 4/ EXPLAIN  SELECT d,avg(price) FROM bills IGNORE INDEX(d) GROUP BY d\G
*************************** 1. row ***************************
          id: 1
 select_type: SIMPLE
       table: bills
        type: ALL
possible_keys: NULL
         key: NULL
     key_len: NULL
         ref: NULL
        rows: 176268
       Extra: Using temporary; Using filesort

C’est plutôt clair, full table scan (type: ALL), plus création d’un table temporaire (Using temporary) pour regrouper les données et les trier (Using filesort). C’est le prix à payer pour ne pas utiliser l’index. Ça à l’air vachement coûteux tout ça, Voyons ce qu’en pense MySQL

SHOW STATUS LIKE ‘Last_query_cost’;
+—————–+—————+
| Variable_name   | Value         |
+—————–+—————+
| Last_query_cost | 212129.599000 |
+—————–+—————+


Humm pour l’optimiseur, le coût est le même, pas forcément intuitif tout ça Pour rappel (ou pas)  les paramètres pris en compte par l’optimiseur sont (entre autre):

- Pour les coûts I/O: information sur les enregistrements (nombre, taille,), informations sur les index(null, unique, cardinalité…)…
- Pour les coûts CPU: l’évaluation des conditions, comparaison du rapport index/enregistrements

Force est de constater que selon l’optimiseur, le coût du FTS + table temp + tri et équivalent, pour cette requête au FIS.

Bilan de cette première partie:
- Il y a manifestement un bug de l’optimiseur en MySQL 5.6.9 lors du calcul du coût d’une requête avec la clause FORCE INDEX
-  Dans notre exemple, le coût d’un full index scan est le même qu’un full table scan + Using temporary + Using filesort

Mais alors, quel QEP donne t’il le meilleur temps d’exécution ?

Cette question est traitée dans la 2ème partie de cet article

(Aller à la 2ème partie..)

 

0

Présentations des Viadeo Tech Days 2012

décembre 5, 2012
Tags:
  • Une gestion efficace du changement de vos structures de données relationnelles avec Liquibase par Loïc Dias Da Silva
0

Viadeo Tech Days

novembre 12, 2012

Viadeo vous donne rendez-vous les 20, 21 et 22 novembre 2012 à Paris à l’occasion des Tech Days.

Au programme des conférences:
- MySQL avec MHA
- HDFS HA
- Neo4J
- Puppet
- UX
- La BI
- Tetra le framework UI de Viadeo
- Liquidbase logiciel de gestion de structures relationnelles
- Le mobile
- Processus de rollout

Informations & inscription (gratuite): http://techdays.viadeo.com/

0

Le mystère du CONCAT

juillet 19, 2012

Un comportement bizarre m’a été remontée par un collègue.

J’ai une table qui contient des IDs (pas la clé primaire):

SELECT DISTINCT ID FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678

 

J’utilise la fonction CONCAT pour concaténer une chaîne de caractères et les IDs de la table:

 

 

SELECT DISTINCT ID, CONCAT(‘Daz_’,ID) AS Concat FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678
Concat: Daz_1234

 

 

Pour une raison qui m’échappe, l’ID concaténé est tronqué ! Bug ou feature ???

 

Si je rajoute une autre chaîne de caractères à concaténer, le résultat est encore plus surprenant…

 

SELECT DISTINCT ID, CONCAT(‘Daz_’,ID,’abcd’) AS Concat FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678
Concat: Daz_12345678

 

L’ajout de 4 caractères me permet d’avoir la suite de mon nombre ie les 4 chiffres suivants (sic).

 

Si j’agrandis la chaîne, je peux enfin voir, dans le résultat de la concaténation, les premiers caractères de ma chaîne.

 

SELECT DISTINCT ID, CONCAT(‘Daz_’,ID,’abcdef‘) AS Concat FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678
Concat: Daz_12345678ab

 

Bref c’est du grand n’importe quoi !!! A ce stade, je suis persuadé que ce n’est pas une feature :)

 

Alors on fait quoi avec ça ?

 

Avec ces 2 derniers exemples, un fix quick & dirty permet, dans le contexte d’utilisation de mon collègue, de contourner ce problème.

Concaténer avec des espaces, 4 dans son cas :

 

SELECT DISTINCT ID, CONCAT(‘Daz_’,ID,‘ ‘) AS Concat FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678
Concat: Daz_12345678

 

 

Mais si mon ID contient plus de 8 chiffres, ça ne fonctionne pas (bah oui quick & dirty j’ai dis) !

 

Avant d’aller plus loin, il est rassurant de constater que sans DISTINCT, le comportement est plus proche du résultat attendu :

 

SELECT ID, CONCAT(‘Daz_’,ID) AS Concat FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678
Concat: Daz_12345678

 

 

J’ai également testé avec un GROUP BY sur ID et… le comportement est le même. Le problème semble venir de la table temporaire nécessaire pour dédoublonner.

 

La solution est dans le CAST

 

Le contournement est de fixer la taille de la sortie concaténée en utilisant la fonction CAST:

SELECT DISTINCT ID, CAST(CONCAT(‘Daz_’,ID) AS CHAR(255) ) AS Concat FROM Connection WHERE…\G
*************************** 1. row ***************************
ID: 12345678
Concat: Daz_12345678

 

CQFD

 

N.B. Server version: 5.5.12-rel20.3-log Percona Server with XtraDB (GPL), Release rel20.3, Revision 118

 

P.S. Merci à Nico pour m’avoir remonté ce comportement bizarre

P.S2.: ce comportement est à rapprocher du Bug #39543

0