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..)

 

Commentaires fermés sur Full table scan vs Full index scan part1-2

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
Commentaires fermés sur Présentations des Viadeo Tech Days 2012

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/

Commentaires fermés sur Viadeo Tech Days

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

Commentaires fermés sur Le mystère du CONCAT

ERROR 1690 (22003): BIGINT UNSIGNED value is out of range

juin 13, 2012

Le problème du jour, une table qui contient (entre autre) 2 colonnes de type entier non signé ( NombPart & NombInscr).

 

CREATE TABLE VoteInfo (
NombPart int unsigned DEFAULT NULL,
NombInscr int unsigned DEFAULT NULL,
) ENGINE=InnoDB

 

 

Une requête, qui fait la soustraction entre NombInscr et NombPart, pour remplir une autre table à la volée :

 

CREATE TABLE …
SELECT … NombInscr – NombPart as NombAbs …
FROM … ;

 

 

Et là, c’est le drame…

ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in ‘(NombInscr – NombPart)’

 

 

Après quelques tests, le problème est identifié, il vient de la soustraction : SELECT … NombInscr – NombPart .

Pour faire simple, on a donc un entier non signé qui est retranché d’un autre entier non signé et le tout ne rentre pas dans un entier long non signé… ça ressemble à un problème de signes 🙂

 

Un coup d’œil dans la table montre rapidement que les données ne sont pas cohérentes ie NombInscr < NombPart ce qui implique un résultat négatif qui ne peut être géré dans une colonne non signée ie le BIGINT UNSIGNED du message d’erreur.

En effet, les 2 colonnes étant de type entier non signé, le résultat de l’opération est donc géré avec le plus grand type d’entier (BIGINT) mais en mode non signé (sic) d’où mon problème.

Qu’à cela ne tienne, il suffit juste de s’assurer que le résultat est toujours positif et le problème est réglé (?).

Essayons la fonction ABS par exemple

 

SELECT … ABS(NombInscr – NombPart) as NombAbs …
FROM … ;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in ‘(NombInscr – NombPart)’

 

 

pas mieux 🙁

La fonction ABS, s’appliquant après la soustraction, le problème reste donc le même.

 

 

Quelles solutions ?

 

Dans mon contexte, plusieurs contournements du problème s’offrent à moi

 

  • Corriger les problèmes d’incohérence dans la table (malheureusement pas si simple)

 

  • Ne faire le calcul que lorsque NombInscr est supérieur à NombPart (et tant pis pour les données non cohérentes) :
SELECT … NombInscr – NombPart as NombAbs
FROM …
WHERE NombInscr >= NombPart ;

 

  • Faire le calcul que lorsque NombInscr est supérieur à NombPart et afficher NULL (ou autre chose) sinon :
SELECT … IF(NombInscr >= NombPart, NombInscr – NombPart, NULL) as NombAbs
FROM … ;

 

  • Faire le calcul « dans le bon sens » {update @Cyril}:
SELECT … IF(NombInscr >= NombPart, NombInscr – NombPart, – (NombPartNombInscr)) as NombAbs
FROM … ;

 

 

Il est aussi possible de s’attaquer de manière frontale au problème, en jouant avec le transtypage :

 

  • Avec la fonction CAST
SELECT … CAST(NombInscr AS signed) – CAST(NombPart AS signed) AS NombAbs
FROM … ;

 

  • Avec du transtypage implicite
SELECT … (NombInscr+0.0 – NombPart+0.0) as NombAbs
FROM … ;

 

  • Avec du transtypage explicite {update @armenweb}
SELECT … (CAST(NombInscr AS DECIMAL(20, 0)) – CAST(NombPart AS DECIMAL(20, 0))) as NombAbs
FROM … ;

 

Une autre manière de faire (plus classe?) est de gérer ce problème au niveau serveur, en initialisant la variable SQL_MODE (qui permet de modifier le comportement du serveur) avec la valeur  NO_UNSIGNED_SUBTRACTION

 

 

SET SESSION sql_mode = ‘NO_UNSIGNED_SUBTRACTION’;
SELECT … NombInscr – NombPart as NombAbs
FROM … ;

 

Dans ce mode, le résultat est géré avec un entier signé. Cependant, le souci potentiel est alors de dépasser l’intervalle de validité du BIGINT signé et d’avoir l’erreur suivante :

ERROR 1690 (22003): BIGINT value is out of range in ‘(NombInscr – NombPart)’

 

Pas de méthodes miracles donc…

Pour plus d’infos, RTFM 🙂

 

 

P.S. Dédicace à Clem, à qui je cède tout les droits relatifs à cet article 😉

 

8

PHP DAY le 04 juin 2012

juin 4, 2012

Ce lundi 04 juin 2012, SUP’INTERNET et EPITECH organisent le PHP DAY, avec en guest star Rasmus Lerdorf,

au CAMPUS TECHNOLOGIQUE DE IONIS EPITECH-SUP’INTERNET 14-16 rue Voltaire 94276 Kremlin Bicêtre Cedex

LE PROGRAMME 

Début du cycle de conférences à partir de 14h

 

  • 14h : Sécurité PHP MySQL

Conférencier : Damien Seguy – auteur du livre « Sécurité PHP » et membre du PHPGroup

 

  • 15h : Performances MySQL

Conférencier : Olivier Dasini – auteur des livres « Audit et optimisation MySQL 5 (Eyrolles) » et « MySQL 5, Administration et optimisation (ENI) » et responsable de la communauté MySQL en France

 

  • 16h30 : Performances Web

Conférencier : Paul Rouget (Mozilla)

 

  • 17h30 : Performances PHP

Conférenciers :

– Guillaume Plessis – co auteur du livre « Performances PHP » et mainteneur des paquets Debian optimisés (dotdeb)

– Cyril PIERRE de GEYER – co auteur des livres « PHP 5 avancé » et « Performances PHP »

– Julien PAULI – co auteur des livres « Performances PHP » et « Zend Framework »

 

  • 19h : Rasmus LERDORF – créateur de PHP

 

Commentaires fermés sur PHP DAY le 04 juin 2012

Optimiser un ALTER

mai 30, 2012

Lors d’une discussion à la cafèt, la question suivante fut posée : « Faire un ALTER TABLE avec plusieurs instructions est il plus rapide qu’un ALTER TABLE par instruction » ?

Les 2 protagonistes n’étant pas d’accord entre eux, c’est tout naturellement que je fus invité à donner mon avis.

La réponse me semble assez intuitive, mais comme des chiffres valent mieux qu’un longs discours…

 

 

Le contexte :

  • MySQL Community Server 5.5.22
  • Une table InnoDB de 8 539 238 enregistrements
  • 17 colonnes avec INT, CHAR, VARCHAR, TEXT, DATETIME, …

 

 

Ajout de 3 index (en une seule commande)

 

mysql5.5> ALTER TABLE lien
-> ADD KEY `IdxIdMembre` (`IdMembre`),
-> ADD KEY `IdxCreationTitre` (`Creation`,`Titre`(100)),
-> ADD KEY `IdxPaysLangueCreation` (`Pays`,`Langue`,`Creation`);
Query OK, 0 rows affected (2 min 26.43 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

Ce qui fait un total de 146.43 sec

 

 

Suppression de index (en une seule commande)

 

mysql5.5> ALTER TABLE lien
-> DROP KEY `IdxIdMembre`,
-> DROP KEY `IdxCreationTitre`,
-> DROP KEY `IdxPaysLangueCreation`;
Query OK, 0 rows affected (1.78 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

Au passage on peut remarquer que depuis MySQL 5.5, supprimer des index secondaires (non clé primaire) ne coûte rien.

 

 

 

Ajout de 3 index (en 3 commandes)

 

mysql5.5> ALTER TABLE lien ADD KEY `IdxIdMembre` (`IdMembre`);
Query OK, 0 rows affected (26.34 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

mysql5.5> ALTER TABLE lien ADD KEY `IdxCreationTitre` (`Creation`,`Titre`(100));
Query OK, 0 rows affected (57.58 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

mysql5.5> ALTER TABLE lien ADD KEY `IdxPaysLangueCreation` (`Pays`,`Langue`,`Creation`);
Query OK, 0 rows affected (1 min 7.24 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

Ce qui fait un total de 151.16 sec (4 % plus lent)

 

Bien entendu, plus le volume de donnée à déplacer est important, plus l’écart entre les 2 façons de faire risque d’être important.

 

Suppression de index (en 3 commandes)

 

mysql5.5> ALTER TABLE lien Drop index `IdxIdMembre`;
Query OK, 0 rows affected (0.29 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

mysql5.5> ALTER TABLE lien DROP KEY `IdxCreationTitre`;
Query OK, 0 rows affected (1.06 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

mysql5.5> ALTER TABLE lien DROP KEY `IdxPaysLangueCreation`;
Query OK, 0 rows affected (0.42 sec)
Records: 0 Duplicates: 0 Warnings: 0

 

Total: 1.77 sec

 

CQFD

 

 

Divers

 

CREATE TABLE `lien` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
...
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci

 

 

show table status like 'lien'\G
*************** 1. row ***************
Name: lien
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 8427138
Avg_row_length: 609
Data_length: 5139070976
Max_data_length: 0
Index_length: 0
Data_free: 4194304
Auto_increment: 8901321
...
Collation: utf8_swedish_ci
Checksum: NULL
Create_options:
Comment:

 

2

Jointure vs sous-requête

mai 15, 2012

MySQL est connu pour ne pas être très performant avec les sous-requêtes. Ce n’est pas faux, et d’ailleurs c’est encore le cas avec MySQL 5.5. Le contournement consiste en général à réécrire la requête, certaines sous-requêtes pouvant être aisément réécrite en jointure.

C’est le cas de
SELECT a FROM T1 WHERE col IN (SELECT col FROM T2…)  qui se transforme en
SELECT distinct a FROM T1 INNER JOIN T2 ON TI.col=T2.col WHERE …
Avec un exemple concret, il est possible de faire les constatations suivantes.

Serveur : MySQL: 5.5.22 Community Server (GPL)

 

Requête 1 :

SELECT count(*) FROM customer
WHERE id_cutomer IN
(
    SELECT id_cutomer FROM customer
    WHERE phone like '06%'
);

 

 

Requête 2 (requête 1 réécrite avec une jointure) :

SELECT count(*) FROM customer c1 INNER JOIN
    customer c2 USING(id_cutomer)
WHERE c2.phone like '06%';

 

P.S. Le critère de l’auto-jointure étant sur toutes les parties de l’index unique (la clé primaire), il n’ est donc pas nécessaire de déboublonner avec DISTINCT (ou GROUP BY).

 

 

Après benchs, l’écart entre ses 2 requêtes est d’environ 7 %. Dans la vraie vie, l’écart peut aller de quelques secondes à une requête qui ne rend jamais la main…

 

La lecture du plan d’exécution (QEP) donné par la commande EXPLAIN, n’explique pas de manière flagrante la différence de performance.

Requête 1 :

*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: customer
type: index
possible_keys: NULL
key: idx_lname
key_len: 50
ref: NULL
rows: 700021
Extra: Using where; Using index
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY

table: customer

type: unique_subquery

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: func

rows: 1

Extra: Using where

 

Requête 2 :

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: c1

type: index

possible_keys: PRIMARY

key: idx_lname

key_len: 50

ref: NULL

rows: 700021

Extra: Using index

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: c2

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: customer.c1.id_cutomer

rows: 1

Extra: Using where

 

 

Qu’en est il de la version 5.6 de MySQL ?

Cette dernière mouture apporte une ribambelle d’amélioration, notamment en ce qui concerne l’optimisation des sous-requêtes…

 

Serveur : MySQL: 5.6.5-m8 MySQL Community Server (GPL)

 

Les QEPs respectifs sont

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: customer

type: index

possible_keys: PRIMARY

key: idx_lname

key_len: 50

ref: NULL

rows: 699628

Extra: Using index

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: customer

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: customer.customer.id_cutomer

rows: 1

Extra: Using where

 

et

 

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: c1

type: index

possible_keys: PRIMARY

key: idx_lname

key_len: 50

ref: NULL

rows: 699628

Extra: Using index

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: c2

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: customer.c1.id_cutomer

rows: 1

Extra: Using where

 

 

Oui, oui vous avez bien lu, ils sont bien (quasiment) identique !

En détaillant le QEP de la sous-requête (avec EXPLAIN EXTENDED), on constate que l’optimiseur à (enfin) transformé la sous-requête en jointure :

 

 

show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select count(0) AS `count(*)` from `customer`.`customer` join `customer`.`customer` where ((`customer`.`customer`.`id_cutomer` = `customer`.`customer`.`id_cutomer`) and (`customer`.`customer`.`phone` like ‘06%’))

 

 

C’est une bien bonne nouvelle !

Malheureusement MySQL 5.6 n’est pas encore en GA (fin d’année à priori selon mes sources). Mais tout n’est pas perdu ! (ça s’est du suspens :D).

 

 

 

Quelques chiffres

 

Un « quick bench », avec mysqlslap

 

Requête 1 :

mysqlslap –create-schema=test -q »SELECT count(*) FROM customer WHERE id_cutomer IN (SELECT id_cutomer FROM cust WHERE phone like ‘06%’); » -i200

 

Requête 2 :

mysqlslap –create-schema=test -q »SELECT count(*) FROM customer c1 INNER JOIN customer c2 USING(id_cutomer) WHERE c2.phone like ‘06%’; » -i200

 

 

MySQL 5.5

 

Requête 1 :

Minimum number of seconds to run all queries: 0.923 seconds

 

Requête 2 :

Minimum number of seconds to run all queries: 0.844 seconds

 

 

MySQL 5.6

 

Requête 1 :

Minimum number of seconds to run all queries: 0.974 seconds

 

Requête 2 :

Minimum number of seconds to run all queries: 0.979 seconds

 

 

 

Divers

CREATE TABLE `customer` (
`id_cutomer` int(11) NOT NULL AUTO_INCREMENT,
`fname` varchar(16) COLLATE utf8_swedish_ci NOT NULL DEFAULT '',
`lname` varchar(16) COLLATE utf8_swedish_ci NOT NULL DEFAULT '',
`street_1` varchar(20) COLLATE utf8_swedish_ci DEFAULT NULL,
`city` varchar(20) COLLATE utf8_swedish_ci DEFAULT NULL,
`state` char(2) COLLATE utf8_swedish_ci DEFAULT NULL,
`zip` varchar(9) COLLATE utf8_swedish_ci DEFAULT NULL,
`phone` varchar(16) COLLATE utf8_swedish_ci DEFAULT NULL,
`since` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`divers` text COLLATE utf8_swedish_ci,
PRIMARY KEY (`id_cutomer`),
KEY `idx_lname` (`lname`),
KEY `idx_city` (`city`),
KEY `idx_fname` (`fname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci
Commentaires fermés sur Jointure vs sous-requête