mercredi 2 novembre 2011

Doit-on utiliser le singulier ou le pluriel pour nommer ses tables ?

C'est une question qui revient souvent quand on décide de créer une nouvelle base de données. En effet, on peut avoir oublié la raison pour laquelle on a fait tel choix lors du design d'une ancienne base.Il se peut par exemple que vous utilisiez un ORM et que celui-ci ne supporte par exemple que le singulier sur le nom des tables. Mais dans le cas, où vous vous interrogez simplement sur la bonne marche à suivre je ne connais pas de règle ISO sur le sujet . Sachez cependant qu'Oracle utilise par exemple le pluriel pour les noms des tables et le singulier pour les noms des colonnes. MySQL en fait tout autant dans sa base de données information_schema. Cependant, j'ai trouvé une réponse assez argumentée que j'ai voulu partager.

Pour en savoir plus, allez lire la réponse sur stackoverflow. N'oubliez quand même pas qu'au final vous avez toujours le choix, mais mieux vaut considérer les avantages/inconvénients que peuvent vous apportez d'utiliser ou non le pluriel/singulier et je rajouterais aussi que peuvent vous apporter l'utilisations du CamelCase (passage de Windows à Unix par ex), des minuscules ou majuscules.

lundi 17 octobre 2011

Indexation et booléens

Voilà encore une raison pour laquelle il faut toujours tester vos requêtes!
En effet, il s'avère que l'optimiseur de MySQL 5.5 (vérifié aussi en 5.1.60) ne fait pas le même choix selon qu'un filtre sur la valeur d'un booléen indexé utilise l'opérateur = ou IS. Voici un exemple extrait du rapport de Bug que j'ai posté sur bugs.mysql.com :

mysql> CREATE TABLE t(id INT, b BOOLEAN DEFAULT FALSE);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO t(id) SELECT 1;
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 16 rows affected (0.00 sec)
Records: 16 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 32 rows affected (0.00 sec)
Records: 32 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t;
Query OK, 64 rows affected (0.00 sec)
Records: 64 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t UNION ALL SELECT id FROM t UNION ALL SELECT id FROM t;
Query OK, 387 rows affected (0.00 sec)
Records: 387 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t UNION ALL SELECT id FROM t UNION ALL SELECT id FROM t;
Query OK, 1548 rows affected (0.02 sec)
Records: 1548 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t UNION ALL SELECT id FROM t UNION ALL SELECT id FROM t;
Query OK, 6192 rows affected (0.03 sec)
Records: 6192 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t UNION ALL SELECT id FROM t UNION ALL SELECT id FROM t;
Query OK, 24768 rows affected (0.18 sec)
Records: 24768 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t(id) SELECT id FROM t UNION ALL SELECT id FROM t UNION ALL SELECT id FROM t;
Query OK, 99072 rows affected (0.43 sec)
Records: 99072 Duplicates: 0 Warnings: 0
mysql> alter table t add index(b);
Query OK, 0 rows affected (0.22 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t values(10,TRUE);
Query OK, 1 row affected (0.00 sec)

mysql> EXPLAIN SELECT COUNT(*) FROM t WHERE b IS TRUE\G
******** 1. row ********
id: 1
select_type: SIMPLE
table: t
type: index
possible_keys: NULL
key: b
key_len: 2
ref: NULL
rows: 131783
Extra: Using where; Using index
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT COUNT(*) FROM t WHERE b = TRUE\G
******** 1. row ********
id: 1
select_type: SIMPLE
table: t
type: ref
possible_keys: b
key: b
key_len: 2
ref: const
rows: 1
Extra: Using where; Using index
1 row in set (0.00 sec)

Comme vous pouvez le voir, lorsque l'on utilise l'opérateur "=" , MySQL effectue un accès unique dans l'index b, alors qu'avec l'opérateur "IS" il effectue un parcours total de l'index... Il est donc très important pour l'instant de privilégier l'opérateur d'égalité en attendant la correction du bug.
Lire la suite...

mercredi 12 octobre 2011

Sauvegarder physiquement certaines tables InnoDB

Avec les tables MyISAM, il est possible de sauvegarder directement les fichiers MYD et MYI lorsque le serveur MySQL tourne, en supposant bien sûr que vous avez verrouillé les tables concernées avec la commande LOCK TABLES ou qu'il n'y a aucune requête en cours. Cependant, il n'en est pas de même avec le moteur InnoDB. En effet, à la différence de MyISAM, il y a des threads qui continue à s'exécuter après que les modifications aient été faites et qui modifient les fichiers de données. Seules les journaux de transactions (iblogfiles) sont réellement écrits à chaque fin de transaction (je considère que vous ne jouez pas avec le paramètre innodb_flush_log_at_trx_commit). De plus, il y a un fichier spécial, nommé par défaut ibdata1, qui contient le dictionnaire de données et toutes les tables (et index) InnoDB ayant été créées avec le paramètre innodb_file_per_table désactivé).
C'est la raison pour laquelle il n'est pas possible de copier un fichier innodb d'un serveur à un autre. Il est par contre tout a fait possible de sauvegarder un fichier innodb et de le restaurer plus tard sur le même serveur ou sur un serveur hébergeant une sauvegarde physique, sans avoir à restaurer la base dans son ensemble.
Il faut savoir qu'InnoDB associe un identifiant à chaque fichier innoDB nommé tablespace (oui cela n'a rien à voir avec les tablespaces d'Oracle). Cet id est stocké dans le catalogue InnoDB ainsi que dans le fichier concerné. C'est ce qui empêche de restaurer le fichier sur un autre serveur sur lequel le même tablespace n'existe pas ou a un identifiant différent, ou sur le même serveur sur lequel on aurait fait un TRUNCATE de la table, et donc généré implicitement un nouvel identifiant !

La méthode à suivre est la suivante, détaillée dans la document MySQL :

mysql> USE test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> CREATE TABLE t1(id int);
Query OK, 0 rows affected (0.03 sec)
mysql> SELECT @i:=0;
+-------+
| @id:=0 |
+-------+
| 0 |
+-------+
1 row in set (0.00 sec
mysql> INSERT INTO t1 SELECT @id:=@id+1 FROM information_schema.TABLES;
Query OK, 198 rows affected (0.03 sec)
Records: 198 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t1 SELECT @id:=@id+1 FROM information_schema.TABLES;
Query OK, 198 rows affected (0.00 sec)
Records: 198 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t1 SELECT @id:=@id+1 FROM information_schema.TABLES;
Query OK, 198 rows affected (0.00 sec)
Records: 198 Duplicates: 0 Warnings: 0
mysql> SELECT COUNT(*) FROM t1;
+----------+
| COUNT(*) |
+----------+
| 594 |
+----------+
1 row in set (0.00 sec)
mysql> SHOW ENGINE INNODB STATUS\G ... Main thread process no. 5109, id 140547993949952, state: waiting for server activity
Number of rows inserted 594, updated 0, deleted 0, read 1193
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 12.91 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================
1 row in set (0.00 sec)

Une fois que le statut du moteur InnoDB est bien à "waiting for server activity", On sauvegarde le fichier InnoDB

cp -p /var/lib/mysql/test/t1.ibd /tmp/

La sauvegarde effectuée, on simule la perte de données

mysql> DELETE FROM t1 WHERE id>400;
Query OK, 194 rows affected (0.04 sec)
mysql> SELECT MAX(id) FROM t1;
+---------+
| MAX(id) |
+---------+
| 400 |
+---------+
1 row in set (0.00 sec)

- On tente maintenant de restaurer nos données en prévenant MySQL

mysql> ALTER TABLE t1 DISCARD TABLESPACE;
Query OK, 0 rows affected (0.00 sec)

- On restaure le fichier à sa place d'origine

root@lizzie:~# cp -p /tmp/t1.ibd /var/lib/mysql/test/t1.ibd

- On avertit MySQL qu'il peut utiliser à présent le fichier

mysql> ALTER TABLE t1 IMPORT TABLESPACE;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT MAX(id) FROM t1;
+---------+
| MAX(id) |
+---------+
| 594 |
+---------+
1 row in set (0.02 sec)

Et voilà !
Par contre, comme je vous l'ai dit ça ne fonctionne plus quand l'id du tablespace a été modifié dans le catalogue :

mysql> TRUNCATE TABLE t1;
Query OK, 0 rows affected (0.01 sec)
mysql> ALTER TABLE t1 DISCARD TABLESPACE;
Query OK, 0 rows affected (0.00 sec)
root@lizzie:~# cp -p /tmp/t1.ibd /var/lib/mysql/test/t1.ibd
mysql> ALTER TABLE t1 IMPORT TABLESPACE;
ERROR 1030 (HY000): Got error -1 from storage engine

Vous trouverez un message plus explicite dans le fichier d'erreur :
grep -A 10 "InnoDB: Error" /var/lib/mysql/lizzie.err
111012 18:09:31 InnoDB: Error: tablespace id and flags in file './test/t1.ibd' are 209768 and 0, but in the InnoDB
InnoDB: data dictionary they are 209769 and 0.
InnoDB: Have you moved InnoDB .ibd files around without using the
InnoDB: commands DISCARD TABLESPACE and IMPORT TABLESPACE?
InnoDB: Please refer to
InnoDB: http://dev.mysql.com/doc/refman/5.5/en/innodb-troubleshooting-datadict.html
InnoDB: for how to resolve the issue.
111012 18:09:31 InnoDB: cannot find or open in the database directory the .ibd file of
InnoDB: table `test`.`t1`
InnoDB: in ALTER TABLE ... IMPORT TABLESPACE
Lire la suite...

vendredi 10 juin 2011

Proxychains ou l'art de proxifier

S'il vous est arrivé de vous retrouver dans l'impossibilité de sortir sur certains ports mais que vous avez su mettre en place un proxy socks ou http, il n'est pas forcément aisé de configurer les différents outils que vous utilisez.
Ce fut mon cas par exemple avec git. En effet, il est possible avec git d'utiliser un proxy socks, mais cela nécessite de créer un script basé sur netcat et d'affecter à la variable d'environnement GIT_PROXY_COMMAND le chemin associé à ce script. Bref, quelles que soient les commandes que vous utilisez vous aurez ou non la possibilité de configurer l'utilisation d'un proxy en modifiant un fichier de configuration, une variable de d'environnement etc...
L'intérêt de proxychains est que la configuration ne se fait qu'une seule fois, dans son propre fichier de configuration. Ensuite vous utilisez la syntaxe proxychains <command> <args> et votre commande utilisera le proxy spécifié dans le fichier de configuration de proxychains !

Voyons tout de suite un exemple :

git pull git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git master
git.kernel.org[0: 199.6.1.168]: errno=Connection timed out
git.kernel.org[0: 130.239.17.8]: errno=Connection timed out
git.kernel.org[0: 2001:6b0:e:4017:1994:313:1:2]: errno=Network is unreachable
git.kernel.org[0: 2001:500:60:10:1994:313:1:2]: errno=Network is unreachable
fatal: unable to connect a socket (Network is unreachable)

Maintenant en utilisant un proxy socks.

- Je mets donc en place mon proxy
ssh -fND localhost:10123 my_proxy

- J'installe proxychains et mets en place ma configuration
sudo apt-get install proxychains

- Voici mon fichier de configuration /etc/proxychains
strict_chain

# Quiet mode (no output from library)
quiet_mode

# Proxy DNS requests - no leak for DNS data
#proxy_dns

# Some timeouts in milliseconds
tcp_read_time_out 15000
tcp_connect_time_out 8000

[ProxyList]
socks5 127.0.0.1 10123

- Il ne reste plus qu'à tester proxychains pour proxifier le flux de ma commande git. Pour ce test j'ai commenté l'instruction quiet_mode dans le fichier de configuration de proxychains
proxychains git pull git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git master
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-127.0.0.1:10123-<><>-130.239.17.8:9418-<><>-OK
From git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6
* branch master -> FETCH_HEAD
Already up-to-date.

Comme on peux le voir on passe par le proxy socks local pour atteindre git.eu.kernel.org(130.239.17.8) sur le port git (9418)

Pour ceux qui veulent en savoir plus, le fonctionnement de proxychains est assez simple ainsi que son code (~ 1000 lignes). Il affecte à la variable d'environnement LD_PRELOAD le chemin de sa bibliothèque dynamique (libproxychains.so) surchargeant les fonctions réseaux de base que sont gethostbyaddr, getnameinfo, gethostbyname, connect


Lire la suite...

lundi 30 mai 2011

Télécharger vos patchs Oracle avec wget

Vous l'avez peut être remarqué, mais Oracle vous donne à présent la possibilité de télécharger les archives en utilisant wget. Pour cela, il vous suffit de choisir "WGET options" après avoir cliqué sur download et ensuite de choisir download.sh pour récupérer le script. Enregistrer le sous le nom wget-oracle.sh.

Pour le faire fonctionner il vous reste à :

- entrer vos informations SSO, par exemple :
SSO_USERNAME=cscetbon@lesdatabases.com
SSO_PASSWORD=Waou000p
- commenter les instructions wget qui ne concernent pas les fichiers que vous désirez télécharger
- remplacer le ligne
WGET=/usr/bin/wget
par
WGET='/usr/bin/wget --user-agent="Mozilla/5.0" '
- ajouter en début de script l'instruction
export LANG=C
- rendre exécutable le script
chmod u+x wget-oracle.sh

Maintenant rendez vous dans un répertoire disposant d'assez d'espace pour télécharger vos archives et lancez la commande :
/le/chemin/que/vous/aurez/choisi/wget-oracle.sh

lundi 16 mai 2011

jointures et collate

L'option COLLATE peut être utilisée dans l'ordre de création d'une table pour spécifier le comportement à observer sur les opérations liées à une colonne. Vous pouvez par exemple grâce à ce mot clé effectuer une recherche sur une chaîne de caractères en ne tenant pas compte des accents. Cependant, il est important de savoir que cela peut avoir un impact sur les performances de vos jointures SQL si vous n'utilisez le même COLLATE sur les colonnes servant à effectuer la jointure. Nous allons voir un exemple concret rencontré en production.
Cet exemple fait intervenir 2 tables dont une qui contient plus de 4 millions d'enregistrements :

mysql> SELECT count(*) FROM a1;
+----------+
| count(*) |
+----------+
| 4382124 |
+----------+
1 row in set (0.77 sec)

mysql> SELECT count(*) FROM a2;
+----------+
| count(*) |
+----------+
| 1 |
+----------+
1 row in set (0.00 sec)

La jointure des tables n'est pas très lente mais est exécutée des millions de fois, ce qui fait que le processus global durait une dizaine d'heure. Voici le plan d'exécution utilisé par MySQL :

mysql>EXPLAIN SELECT count(*) FROM a1 JOIN a2 ON (a2.pka2=a1.pka1);
+----+-------------+-------+--------+---------------+---------+---------+------+---------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+------+---------+--------------------------+
| 1 | SIMPLE | a2 | system | PRIMARY | NULL | NULL | NULL | 1 | |
| 1 | SIMPLE | a1 | index | NULL | PRIMARY | 27 | NULL | 4387787 | Using where; Using index |
+----+-------------+-------+--------+---------------+---------+---------+------+---------+--------------------------+
2 rows in set (0.00 sec)

Comme vous pouvez le voir, MySQL effectue un FULL SCAN de l'index (PK) de la table a1 qui contient ~ 4,4 Millions d'enregistrements. L'exécution de la requête dure 130ms.
En fait, ceci est dû au fait que les COLLATE des colonnes a2.pka2 et a1.pka1 sont différents

mysql> SELECT TABLE_NAME,COLUMN_NAME,CHARACTER_SET_NAME,COLLATION_NAME FROM information_schema.columns WHERE (TABLE_NAME='a1' and COLUMN_NAME='pka1') or (TABLE_NAME='a2' and COLUMN_NAME='pka2')\G
*************************** 1. row ***************************
TABLE_NAME: a1
COLUMN_NAME: pka1
CHARACTER_SET_NAME: ascii
COLLATION_NAME: ascii_bin
*************************** 2. row ***************************
TABLE_NAME: a2
COLUMN_NAME: pka2
CHARACTER_SET_NAME: latin1
COLLATION_NAME: latin1_swedish_ci
2 rows in set (0.00 sec)

Si on modifie le COLLATE de la colonne a2.pka2 pour qu'il soit celui de la colonne a1.pka1, MySQL n'a plus besoin de scanner entièrement l'index de la table a1.

mysql> ALTER TABLE a2 CHANGE COLUMN pka2 pka2 VARCHAR(25) NOT NULL COLLATE ascii_bin;
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql>EXPLAIN SELECT count(*) FROM a1 JOIN a2 ON (a2.pka2=a1.pka1);
+----+-------------+-------+--------+---------------+---------+---------+----------------------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+----------------------------+------+-------------+
| 1 | SIMPLE | a2 | index | PRIMARY | PRIMARY | 27 | NULL | 1 | Using index |
| 1 | SIMPLE | a1 | eq_ref | PRIMARY | PRIMARY | 27 | a2.pka2 | 1 | Using index |
+----+-------------+-------+--------+---------------+---------+---------+----------------------------+------+-------------+
2 rows in set (0.00 sec)

Ceci a permis de ramener la durée du processus global à une vingtaine de minutes. Prenez donc garde à utiliser soigneusement la clause COLLATE, surtout quand vous effectuez des jointures !
Lire la suite...

dimanche 27 février 2011

Contraintes uniques et valeurs nulles

Vous risquez d'être surpris si vous utiliser une contrainte d'unicité sur un groupe de colonnes qui peuvent être nulles. En effet, il faut savoir que la contrainte d'unicité différencie par défaut les valeurs nulles. Ainsi si vous créez la table suivante :

mysql> CREATE TABLE t1(id1 int,id2 int,id3 int);
Query OK, 0 rows affected (0.00 sec)

mysql> ALTER TABLE t1 ADD UNIQUE KEY (id1,id2,id3);
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0

Vous pourrez ajouter les triplets (1,1,1), (1,2,1), (2,1,1) une seule fois. Par contre, vous pourrez ajouter autant de fois que vous le voulez les triplets (1,1,NULL), (1,NULL,1), (1,NULL,NULL), (NULL,NULL,NULL) etc...

mysql> INSERT INTO t1 VALUES(1,1,1);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(1,2,1);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(2,1,1);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(2,1,1);
ERROR 1062 (23000): Duplicate entry '2-1-1' for key 'id1'

mysql> INSERT INTO t1 VALUES(1,1,NULL);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(1,1,NULL);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(1,NULL,NULL);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(1,NULL,NULL);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(NULL,NULL,NULL);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(NULL,NULL,NULL);
Query OK, 1 row affected (0.00 sec)

Apparemment, MySQL respecte le SQL 2003 qui spécifie que les contraintes d'unicité ne s'appliquent que sur les valeurs non nulles, ce qui ne semble pas très intuitif quand on choisi ce type de contrainte. Oracle ne prend pas en compte cette considération et vérfie l'unicité sur l'ensemble des valeurs, ce que nombre de personnes auraient certainement voulu retrouver chez MySQL...

Pour plus d'information, vous pouvez lire le bug report 25544