Retour sur le MySQL techday

octobre 14, 2013

Jeudi 10 octobre à eu lieu, dans les locaux d’Oracle, le MySQL techday organisé par Oracle et le MySQL User Group francophone.

Dans la continuité de MySQL Connect, les speakers nous ont offert des sessions de très grandes qualités sur les thèmes de la performance et de l’optimisation relative aux versions 5.6 et 5.7 de MySQL.

Les slides de Dimitri sont disponibles sur son blog: http://dimitrik.free.fr/blog/archives/2013/10/my-files-for-mysql-tech-day-paris-today.html

Ceux d’Arnaud Adant, sur le site de MySQL connect:

50 Tips for Boosting MySQL Performance

Enhancing Productivity with MySQL 5.6 New Features

 

Bonne dégustation !

 

A noter que c’est la 1ère fois en France (à ma connaissance) que les 3 grandes tendances de MySQL étaient représentées à un événement, à savoir SkySQL/MariaDB, Percona et bien sûr Oracle.
Prochain pari, des confs avec ces 3 tendances…
Commentaires fermés sur Retour sur le MySQL techday

MySQL TechDay, le programme

octobre 1, 2013

Le 10 octobre 2013, Le MySQL User Group Francophone (lemug.fr) et Oracle MySQL vous invitent au MySQL Tech Day à Paris.

Début des réjouissances 10h30, avec un programme exceptionnel !!! :

  • 10h30 – Overview: MySQL Innovation @Oracle @MySQL Connect
    Xavier GERARD / Olivier ZEMRAG (MySQL France)
  • 11h00 – MySQL Performance: Latest improvements in MySQL 5.6 & 5.7
    Dimitri KRAVTCHUK (MySQL Performance Engineering leader)
  • 13h45 – MySQL Optimizer: What is new? what is coming?
    Guilhem BICHOT (MySQL Optimizer Engineering team)
  • 15h15 – 50 tips to boost MySQL Performance
    Arnaud ADANT (MySQL Support team)
  • 17h00 – MySQL Performance Schema: Overview / HOWTO
    • MySQL Performance Schema: Hands-on (live lab)
      Dimitri KRAVTCHUK (MySQL Performance Engineering leader)

 
Infos & inscriptions: http://www.lemug.fr/2013/mysql-tech-day-paris/

see you there 🙂

2

MySQL TechDay, le 10 Octobre 2013

août 26, 2013

Le 10 octobre 2013, Oracle et Le MySQL User Group Francophone (lemug.fr) vous invitent au MySQL Tech Day à Paris.

Une journée technique bien pleine où les différentes équipes MySQL chez Oracle vont nous présenter les améliorations présentes et futures des versions 5.6 et 5.7 de MySQL.

 

Au programme:

  • Overview: MySQL Innovation @Oracle

  • MySQL Performance: Latest improvements in MySQL 5.6 & 5.7

  • MySQL Optimizer: What is new, what is coming

  • 50 tips to boost MySQL Performance

  • MySQL Performance Schema: Overview / HOWTO

  • MySQL Performance Schema: Hands-on (live lab)

 

Bref du lourd, du très lourd !!!

 

Last but not least, c’est gratuit, mais les places sont limités…

 

Inscription et renseignements, veuillez contacter Alexis Bensa:

  • par email: alexis.bensa(@)oracle.com

  • téléphone: 01.57.60.27.96

Quand ?

Jeudi 10 octobre 2013, toute la journée

?

Cette rencontre aura lieu dans les locaux d’Oracle au

Portes de La Defense 15, boulevard Charles de Gaulle

Prix ?

Entrée libre

 

update: les horaires

Le communiqué officiel:


MySQL Tech Day @Paris


We’re happy to announce you that MySQL Tech Day will take place in Paris on Oct 10, 2013 in Oracle main office. It’ll be a full day event giving you an occasion to listen directly from Oracle developers about most of the improvements made recently within MySQL 5.6 and 5.7 development.

 

The agenda is the following :

  • Overview: MySQL Innovation @Oracle

  • MySQL Performance: Latest improvements in MySQL 5.6 & 5.7

  • MySQL Optimizer: What is new, what is coming

  • 50 tips to boost MySQL Performance

  • MySQL Performance Schema: Overview / HOWTO

  • MySQL Performance Schema: Hands-on (live lab)

 

Don’t miss such an opportunity to get most of the hot news about MySQL improvement directly from the source! It’ll be just two weeks after MySQL Connect, so there will be really a lot of news! So, book this date in your agenda right now and then don’t forget to register yourself for the event – attending is totally free, but places are limited, so registration is mandatory to secure enough seats.

 

To register : please, contact Alexis Bensa:

  • by email: alexis.bensa(@)oracle.com

  • or directly by phone: 01.57.60.27.96

 

Oracle main office address: Portes de La Defense 15, boulevard Charles de Gaulle.

 

Further details will be communicated over a time.

4

LeMug.fr à la radio

juillet 10, 2013
Tags: ,

L’association LeMug.fr (MySQL User Group Francophone) fut l’invité à l’émission Cyberculture de la radio Ici et Maintenant samedi 6 juillet pour parler des bases de données webs et principalement du sgbdr le plus utilisée sur internet: MySQL.

Le podcast de l’émission est disponible ici : Les bases de données webs

Source lemug.fr

 

Commentaires fermés sur LeMug.fr à la radio

#DBHangOpsFr du 20/06/2013, les backups avec MySQL

juin 25, 2013
Tags:

Si vous avez manqué 1er #DBHangOpsFr, voici la session de rattrapage.

Un grand merci à Cédric.

 

Commentaires fermés sur #DBHangOpsFr du 20/06/2013, les backups avec MySQL

Utiliser une sous requête c’est mal ? (suite) part 1-3

mai 27, 2013

Comme promit, voici la suite de l’article Utiliser une sous-requête c’est mal ?

L’idée ici est de répondre aux interrogations de svar et d’en profiter pour explorer les nouvelles possibilités de la variante stable de MySQL qui possède l’optimiseur le plus avancé, c’est à dire MariaDB 5.5.

 

Préambule

En pré-requis, je vous invite à explorer la doc officielle de MySQL au sujet de la variable optimizer_switch, qui permet de sélectionner les algorithmes d’optimisation, ainsi que les différentes subtilités implémentées dans l’optimiseur de MariaDB.

 

La valeur par défaut sur MariaDB 5.5.30 de la variable optimizer_switch est:

 

mariadb5.5> SHOW VARIABLES LIKE ‘optimizer_switch’\G
*************************** 1. row ***************************
Variable_name: optimizer_switch
      Value: index_merge=on,index_merge_union=on,index_merge_sort_union=on,
index_merge_intersection=on,index_merge_sort_intersection=off,
engine_condition_pushdown=off,index_condition_pushdown=on,
derived_merge=on,derived_with_keys=on,firstmatch=on,loosescan=on,
materialization=on,in_to_exists=on,semijoin=on,partial_match_rowid_merge=on,
partial_match_table_scan=on,subquery_cache=on,mrr=off,mrr_cost_based=off,
mrr_sort_keys=off,outer_join_with_cache=on,semijoin_with_cache=on,
join_cache_incremental=on,join_cache_hashed=on,join_cache_bka=on,
optimize_join_buffer_size=off,table_elimination=on,extended_keys=off

Le truc super lisible en fait !

Mais comme je suis un mec super sympa, j’ai mis en gras les directives qui vont nous intéresser dans cet article.

 

Les patterns des tests

 

Pour atteindre les objectifs fixés, j’ai effectué 2 niveaux de tests. Le premier niveau va me fournir des données “objectives”. Il consiste à exécuter la requête avec différentes directives d’optimisation et d’analyser les “status” ainsi que les plans d’exécution associés.

Niveau stats

  1. SET optimizer_switch=’default’ : réinit des directives d’optimisation

  2. SET optimizer_switch=’valeur adéquate : choix des directives d’optimisation

  3. EXPLAIN EXTENDED : QEP et infos de réécritures de la requête

  4. SHOW WARNINGS : pour savoir comment l’optimiseur à réécrit la requête

  5. FLUSH STATUS : réinit des stats à 0

  6. exécution de la requête

  7. SHOW STATUS : pour récupérer les variables d’états, Handler* & Created_tmp

Avec le second niveau de tests, je vais récupérer une info “subjective”, le temps. Cela consiste à récupérer la durée minimale d’exécution de la requête avec les différentes directives d’optimisations à l’aide de l’outil mysqlslap.

Niveau temps

  1. SET GLOBAL optimizer_switch=’default’

  2. SET GLOBAL optimizer_switch=’valeur adéquate

  3. mysqlslap  -c1 -i200 -q »la requête«  : requête unitaire jouée plusieurs fois ie buffers chaud

 

A noter que le nombre d’itération (200) à été réduit à 5 lorsque la requête dure de l’ordre de la 10aine de minutes (bah oui, j’ai une vie aussi).

Contexte

 

Pour rappel, le problème initiale est une sous requête problématique en MySQL (Percona server) 5.5 :

 

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
     )
+———-+
| OtherID |
+———-+
7 rows in set (44 min 56.66 sec) <= A froid

L’idée ici n’est pas de comparer le temps d’exécution de la requête sur MariaDB par rapport à  Percona server (les serveurs et les paramètres sont différents), mais bien de voir et de comprendre les (nouveaux) choix de l’optimiseur de MariaDB 5.5.

Traditional behavior (IN to EXISTS strategy)

 

TEST 1

Traditionnellement l’optimiseur converti la sous-requête IN en EXIST. Pour simuler ce comportement il faut désactiver la matérialisation et la semi-jointure.

 

SET optimizer_switch=’materialization=off,semijoin=off’;

 

QEP

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

Intuitivement on peut penser que la requête interne est exécutée en premier, puis qu’elle génère une liste d’ID (MainID) qui est ensuite passée dans la clause IN, du genre :

… MainID IN (123, 456, 789).

 

Eh ben non !

Comme convenu, le QEP est semblable à celui utilisé avec MySQL / percona server 5.5.

En fait, l’optimiseur transforme le clause IN en EXISTS. La table externe est lue la première sur environ 50% de ses lignes (rows: 153455020), ce qui dans notre cas fait beaucoup et est jointe avec la table interne.

La sous-requête dépend alors des données de la requête externe (select_type: DEPENDENT SUBQUERY).

La requête réécrite par l’optimiseur ressemble alors à peu près à :

 

SELECT DISTINCT OtherID
FROM MyTable AS MyExtTable
WHERE Status = 1
 AND Stage = 4
 AND Invisible = 0
 AND EXISTS
     (
     SELECT 1
     FROM MyTable AS MyInnTable
     WHERE OtherID = 2779262
         AND Stage = 0
         AND Invisible = 0
         AND MyExtTable.MainID = MyInnTable.MainID
     )
 
 

Optimizer rewriting

select distinct OtherID AS OtherID from MyTable
where ((Status = 1) and (Stage = 4) and (Invisible = 0) and <expr_cache><MainID>(<in_optimizer>(MainID,<exists>(<index_lookup>(<cache>(MainID) in MyTable on MainIDOtherIDStatus
where (((OtherID = 2779262) and (Stage = 0) and (Invisible = 0)) and (<cache>(MainID) = MainID) and (OtherID = 2779262) and (Stage = 0) and (Invisible = 0)))))))

 

Confirmation que l’on a bien une transformation IN to EXISTS.

Status

+—————————+———–+
| Variable_name             | Value     |
+—————————+———–+
| Created_tmp_disk_tables     | 0     |
| Created_tmp_tables            | 2     |
| Handler_mrr_key_refills     | 0         |
| Handler_mrr_rowid_refills   | 0         |
| Handler_prepare           | 0         |
| Handler_read_first        | 0         |
| Handler_read_key          | 44655686  |
| Handler_read_last         | 0         |
| Handler_read_next         | 153049748 |
| Handler_read_prev         | 0         |
| Handler_read_rnd          | 0         |
| Handler_read_rnd_deleted    | 0         |
| Handler_read_rnd_next     | 8         |
| Handler_tmp_write         | 206       |
| Handler_write             | 0         |
+—————————+———–+

 

Beaucoup de lecture dans l’index (Handler_read_key | 44655686). Le problème ici est le nombre élevé de lectures transverse de l’index (Handler_read_next | 153049748) qui est très coûteux, surtout si les IO sont sur disque, et c’est encore pire s’ils sont aléatoires ie random IO disk.

 

Contrairement à MySQL 5.5 où il n’y a qu’une seule table temporaire créée, ici il y en a 2 (Created_tmp_table | 2), l’une pour le dédoublonnage lié à la clause DISTINCT et l’autre… ben je ne sais pas :/

En profilant la requête avec la commande SHOW PROFILE, il y est bien noté que 2 tables temporaires sont supprimées, mais dans le même temps, une seule est créée…

Si quelqu’un à une explication, je suis preneur.

Bench

daz@sql:~/mariadb5.5.30$ mysqlslap –create-schema=test -c1 -i5 -q »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  ); »

Benchmark

  Minimum number of seconds to run all queries: 550.444 seconds

 

Les meilleures performances de cette requête, dans ces conditions, sont de l’ordre de la 10aine de minutes, ce qui est rarement acceptable en OLTP.

Pour simuler l’”intelligence” de l’optimiseur de MySQL / Percona Server 5.5, il a fallu désactiver 2 directives d’optimisation (la matérialisation et la semi-jointure). Voyons ce qu’il en est en ne désactivant qu’une seule d’entre elles, à tour de rôle.

MATERIALIZATION strategy (disable semi-join)

 

TEST 2

Pour désactiver la semi-jointure, il suffit de mettre la directive suivante à OFF:

SET optimizer_switch=’semijoin=off’;

 

La stratégie d’optimisation utilisée par l’optimiseur est alors la matérialisation. En 2 mots, le résultat de la sous-requête est stocké dans une table temporaire qui contient un index unique. Cet index sert à dédoublonner les valeurs insérée et à récupérer les infos de manière performante (comme un index classique), qui vont alimenter la requête externe.

QEP

*************************** 1. row ***************************
         id: 1
select_type: PRIMARY
      table: MyTable
       type: ref
possible_keys: StageMainIDOtherID, …
        key: StageMainIDOtherID
    key_len: 1
        ref: const
       rows: 153455020
    filtered: 100.00
      Extra: Using where; Using temporary
*************************** 2. row ***************************
         id: 2
select_type: MATERIALIZED
      table: MyTable
       type: ref
possible_keys: MainIDOtherIDStatus,OtherIDStageStatusInvisibleMainID,StageMainIDOtherID, …
        key: OtherIDStageStatusInvisibleMainID
    key_len: 5
        ref: const,const
       rows: 235
    filtered: 100.00
      Extra: Using where; Using index

 

Là encore, selon le QEP la table externe est lue la première sur environ 50% de ses lignes  (rows: 153455020). Ce qui change là, c’est que la stratégie d’optimisation retenue est la matérialisation.

Autres différences avec le QEP originel, un covering index (Extra: Using where; Using index) sur les 235 (une estimation) enregistrements (rows: 235) de la table matérialisée.

Optimizer rewriting

select distinct OtherID AS OtherID from MyTable
where ((Status = 1) and (Stage = 4) and (Invisible = 0) and <expr_cache><MainID>(<in_optimizer>(MainID,MainID in
( <materialize> (select MainID from MyTable
where ((OtherID = 2779262) and (Stage = 0) and (Invisible = 0)) ), <primary_index_lookup>(MainID in <temporary table> on distinct_key where ((MainID = <subquery2>.MainID)))))))

 

Confirmation que l’on à bien la matérialisation de la sous-requête sous la forme d’une table temporaire indexée(<primary_index_lookup>MainID in <temporary table> on distinct_key).

A noter que l’index à également servi au dédoublonnage.

Status

+—————————+———–+
| Variable_name             | Value     |
+—————————+———–+
| Created_tmp_disk_tables     | 0     |
| Created_tmp_tables            | 3     |
| Handler_mrr_key_refills     | 0         |
| Handler_mrr_rowid_refills   | 0         |
| Handler_prepare           | 0         |
| Handler_read_first        | 0         |
| Handler_read_key          | 44655687  |
| Handler_read_last         | 0         |
| Handler_read_next         | 153049981 |
| Handler_read_prev         | 0         |
| Handler_read_rnd          | 0         |
| Handler_read_rnd_deleted    | 0         |
| Handler_read_rnd_next     | 8         |
| Handler_tmp_write         | 442       |
| Handler_write             | 0         |
+—————————+———–+

Niveau table temporaires, on a 1 table temporaire pour le DISTINCT + 1 table pour la matérialisation + 1 table temporaire mystère (Created_tmp_tables | 3).

Avec une table temporaire supplémentaire, il y a alors plus d’écriture (Handler_tmp_write | 442).

442 (Handler_tmp_write) – 235 (MATERIALIZED rows) = 207 (à rapporter au 206 du Handler_tmp_write du QEP originel)

 

A l’aide de toutes ces informations, lequel des 2 modes est le plus performant ?

D’un point de vu status, il y a 1 table temporaire de plus pour T2, mais les tables étant créées entièrement en mémoire, le coût reste donc faible. De plus le nombre de Handler_read_next est sensiblement égale. Même s’il n’est pas réparti de la même manière.

Bref, niveau status, exaequo entre T1 & T2

Avec explain, le nombre de rows de T2 est 235 fois plus important que pour T1. Cependant, T2 fournit une matérialisation et index covering index alors qu’avec T1 on est dans de la sous-requête corrélée !

Bref niveau explain, c’est pas clair non plus 😀 mais la matérialisation se faisant en mémoire, elle devrait être moins coûteuse.

Bench

daz@sql:~/mariadb5.5.30$ mysqlslap –create-schema=test -c1 -i5 -q »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  ); »
Benchmark
  Minimum number of seconds to run all queries: 430.712 seconds

 

T2 s’exécute avec 2 minutes de moins que T1.

SEMIJOIN strategy (disable materialization)

 

TEST 3

Pour désactiver la matérialisation, il suffit de mettre la directive suivante à OFF:

SET optimizer_switch=’materialization=off’;

 

La stratégie d’optimisation utilisée par l’optimiseur est alors la semi-jointure (une sorte de jointure interne où seule une occurrence des valeurs jointes est récupérée).

QEP

*************************** 1. row ***************************
         id: 1
select_type: PRIMARY
      table: MyTable
       type: index
possible_keys: MainIDOtherIDStatus,OtherIDStageStatusInvisibleMainID,StageMainIDOtherID
        key: OtherIDStageStatusInvisibleMainID
    key_len: 15
        ref: NULL
       rows: 306910041
    filtered: 0.00
      Extra: Using where; Using index; Using temporary; LooseScan
*************************** 2. row ***************************
         id: 1
select_type: PRIMARY
      table: MyTable
       type: ref
possible_keys: MainIDOtherIDStatus,StageMainIDOtherID
        key: MainIDOtherIDStatus
    key_len: 4
        ref: MyTable.MainID
       rows: 1
    filtered: 100.00
      Extra: Using index condition; Using where

Le QEP obtenu est un full index scan aka FIS (type: index) avec covering index (Extra: Using index;). En général un FIS est plutôt une mauvaise nouvelle pour les perfs, sauf dans le cas d’un index covering.

L’algorithme utilisé par l’optimiseur pour la semi-jointure est le  LooseScan (Extra: LooseScan).

A noter l’utilisation de l’optimisation index condition pushdown ou ICP (Extra: Using index condition). On en y revient plus tard.

Optimizer rewriting

select distinct OtherID from MyTable semi join (MyTable)
where ((Status = 1) and (Stage = 4) and (Invisible = 0) and (OtherID = 2779262) and (Stage = 0) and (Invisible = 0)
and (MainID = MainID))

 

Confirmation que l’on a bien une transformation en semi-jointure.

Status

+—————————+———–+
| Variable_name             | Value     |
+—————————+———–+
| Created_tmp_disk_tables | 0     |
| Created_tmp_files       | 0     |
| Created_tmp_tables      | 1     |
| Handler_mrr_key_refills   | 0         |
| Handler_mrr_rowid_refills | 0         |
| Handler_prepare           | 0         |
| Handler_read_first        | 1         |
| Handler_read_key          | 236       |
| Handler_read_last         | 0         |
| Handler_read_next         | 306909498 |
| Handler_read_prev         | 0         |
| Handler_read_rnd          | 0         |
| Handler_read_rnd_deleted  | 0         |
| Handler_read_rnd_next     | 8         |
| Handler_tmp_write         | 7         |
| Handler_write             | 0         |
+—————————+———–+

 

Les informations d’état sont assez claires (si si vous allez voir):

  • Le FIS est un parcours transverse complet de l’arbre d’index: (Handler_read_next | 306909498 et  Handler_read_first | 1)

  • La sous-requête elle, est responsable de: (Handler_read_key | 236)

  • Le distinct est responsable de la création de la table temporaire: (Created_tmp_tables | 1)

  • Remplissage de la table temporaire: (Handler_tmp_write | 7)

  • Lecture des données de la table temporaire: (Handler_read_rnd_next | 8)

 

T2 vs T3 ça donne quoi ?

Là encore ce n’est pas trivial de comparer les QEP et status des 2 requêtes. On peut cependant raisonnablement penser que le FIS avec covering index de T3 va générer moins de random IO que T2.

 

 

Bench

daz@sql:~/mariadb5.5.30$ mysqlslap –create-schema=test -c1 -i200 -q »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  ); »
Benchmark
  Minimum number of seconds to run all queries: 130.531 seconds

 

T3 s’exécute avec 3m20 de moins que T2.

T3 s’exécute avec 5m20 de moins que T1.

Optimizer choice

 

TEST 4

Comme je l’ai mentionné en introduction, l’optimiseur de MariaDB est le plus avancé (en matière de possibilités d’optimisation) dans l’univers MySQL. Voyons comment cet optimiseur, avec les réglages par défaut, gère cette requête.

 

La commande suivante permet de réinitialiser, les directives d’optimisation:

SET optimizer_switch=’default’;

QEP

*************************** 1. row ***************************
         id: 1
select_type: PRIMARY
      table: <subquery2>
       type: ALL
possible_keys: distinct_key
        key: NULL
    key_len: NULL
        ref: NULL
       rows: 235
    filtered: 100.00
      Extra: Using temporary
*************************** 2. row ***************************
         id: 1
select_type: PRIMARY
      table: MyTable
       type: ref
possible_keys: MainIDOtherIDStatus,StageMainIDOtherID
        key: MainIDOtherIDStatus
    key_len: 4
        ref: MyTable.MainID
       rows: 1
    filtered: 100.00
      Extra: Using index condition; Using where
*************************** 3. row ***************************
         id: 2
select_type: MATERIALIZED
      table: MyTable
       type: ref
possible_keys: OtherIDStageStatusInvisibleMainID, StageMainIDOtherID, …
        key: OtherIDStageStatusInvisibleMainID
    key_len: 5
        ref: const,const
       rows: 235
    filtered: 100.00
      Extra: Using where; Using index; Distinct

La sous-requête est matérialisée et on a également une semi-jointure entre cette dernière est la table de la requête externe (MyTable).

Optimizer rewriting

select distinct OtherID AS OtherID from MyTable semi join (MyTable)
where ((Status = 1) and (Stage = 4) and (Invisible = 0) and (OtherID = 2779262) and (Stage = 0) and (Invisible = 0)
and (MainID = MainID))

 

L’information sur la semi-join est explicite (MyTable semi join (MyTable)… (MainID = MainID)).

Par contre, pas de trace ici de la matérialisation…

Status

+—————————+——-+
| Variable_name             | Value |
+—————————+——-+
| Created_tmp_disk_tables | 0     |
| Created_tmp_files       | 0     |
| Created_tmp_tables      | 2     |
| Handler_mrr_key_refills   | 0     |
| Handler_mrr_rowid_refills | 0     |
| Handler_prepare           | 0     |
| Handler_read_first        | 0     |
| Handler_read_key          | 237   |
| Handler_read_last         | 0     |
| Handler_read_next         | 243   |
| Handler_read_prev         | 0     |
| Handler_read_rnd          | 0     |
| Handler_read_rnd_deleted  | 0     |
| Handler_read_rnd_next     | 245   |
| Handler_tmp_write         | 243   |
| Handler_write             | 0     |
+—————————+——-+

 

Niveau handler, que des petites valeurs, ça sent bon la perf tout ça !!!

Grosso-modo, ces infos nous disent :

  • 2 tables temporaires pour le distinct et la matérialisation : (Created_tmp_tables | 2)

  • La semi-jointure  : (Handler_read_next | 243)
  • La sous-requête elle, est responsable de : (Handler_read_key | 237)

  • Remplissage + lecture des tables temporaire : (Handler_read_rnd_next | 245 + Handler_tmp_write | 243)

Bench

daz@sql:~/mariadb5.5.30$ mysqlslap –create-schema=test -c1 -i200 -q »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  ); »
Benchmark
  Minimum number of seconds to run all queries: 0.026 seconds

 

T4 est plus rapide que T1 de plus de 9 minutes.

T4 est plus rapide que T2 de plus de 7 minutes.

T4 est plus rapide que T3 de plus de 2 minutes.

En image, en temps relatif par rapport à T1 (la requête la plus longue) ça donne :

Avec les versions de MySQL / Percona server inférieures ou égales à 5.5 et les versions de MariaDB inférieures ou égales à 5.2 (de mémoire), les sous-requêtes peuvent poser de gros problèmes de performances. Il faut donc bien tester ces requêtes avant de les passer en prod.

 

Une bonne raison supplémentaire pour tester (et migrer) vers MariaDB 5.5 ou MySQL 5.6 (qui possède également les optimisations semijoin et materialization).

 

A suivre, dans la deuxième partie de cet article : Index Condition Pushdown et Multi Range Read…

Commentaires fermés sur Utiliser une sous requête c’est mal ? (suite) part 1-3

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: MyTable.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
9

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

 

Commentaires fermés sur Meetup les bases de données relationnelles avec PHP