Archives par étiquette : mysql

MySQL – les clés étrangères et l’intégrité référentielle

L’heure est venue de nous pencher sur la notion de clé étrangère dans une table. Les clés étrangères servent à établir des contraintes dites d’ « intégrité référentielle ».
192-246-thickbox

Qu’est-ce qu’une clé étrangère ?

C’est un champ (ou une colonne, ou un attribut, ou une propriété) d’une table. Jusque là, voilà qui est plutôt rassurant !

Qu’est-ce l’intégrité référentielle ?

Dans cette expression, nous avons la notion centrale de « référence ». Il est en effet question d’un champ qui fait référence à un autre. Qu’est-ce que cela signifie, dans les faits ? Et bien simplement que, si A est un champ qui référence B, alors A aura pour valeurs admissibles un sous-ensemble des valeurs de B.

Plus concrètement, si votre champ B, référencé par A, a pour valeurs entières 1,2 et 3, alors A ne pourra avoir lui aussi que ces valeurs et pas 4 ou 0 !

Les clés étrangères dans MySQL

En SQL, quelque soit le SGBDR cible, la notion de clé étrangère s’applique à l’aide du mot clé FOREIGN KEY. Comme vous vous apprêtez à modifier la structure de la table pour lui imposer cette contrainte, vous devrez utiliser ALTER TABLE.

Nous allons voir la syntaxe en détail dans l’exemple à venir.

Un exemple, tout de suite !

Soit une table qui recense les joueurs d’un sport quelconque (qui se joue à deux):

create table joueur (
id smallint unsigned not null primary key,
nom varchar(20),
prenom varchar(20));

La table joueur possède un champ qui est son identifiant primaire : c’est id ! Ce champ là possède un index parce que je l’ai explicitement déclaré comme clé primaire de ma table (primary key). Voilà pour ma table joueur, qui est somme toute très simple.

Nous allons maintenant créer une table qui va servir à opposer un joueur à un autre :

create table matches (
id smallint unsigned not null primary key,
id_joueur1 smallint unsigned not null,
id_joueur2 smallint unsigned not null,
duree tinyint unsigned not null);

Cette table là possède elle aussi un identifiant (une clé primaire): en plus de cet identifiant, elle possède deux champs qui vont correspondre à des identifiants de joueurs, donc à des valeurs du champ id présentes dans notre table joueur ! Dans l’état rudimentaire de cette table il est tout à fait possible de rentrer des joueurs, mais comment forcer l’utilisateur de nos données à ne rentrer que des identifiants de joueurs qui existent ? C’est là que la notion de clé étrangère entre en jeu !

Nous allons faire en sorte que les valeurs admissibles pour les champs id_joueur1 et id_joueur2 de la table matches soient uniquement celles présentes dans le champ id de la table joueur, en clair : interdit de faire s’affronter des joueurs qui n’existent pas !

Il va nous falloir modifier la structure de la table matches, il y a donc de l’ALTER dans l’air ! Notez qu’il va falloir créer un index sur le champ référençant (celui qui va servir à référencer l’autre, qui est le référencé) : cet index est INDISPENSABLE car il évite les full table scans et accélère donc la vérification des contraintes d’intégrité référentielle !

ALTER TABLE matches ADD INDEX (id_joueur1); 
ALTER TABLE matches ADD FOREIGN KEY (id_joueur1)
REFERENCES joueur (id);

La voilà enfin notre référence ! Nous disons en substance « crée moi un lien entre le champ id de joueur et le champ id_joueur1 de matches » ! Dorénavant, toute tentative d’insérer un identifiant de joueur qui n’existe pas se soldera par un échec ! Faisons le de suite :

mysql> insert into matches (id_joueur1) values (1);
ERROR 1452 (23000): Cannot add or update a child row:
a foreign key constraint fails (`test`.`matches`, 
CONSTRAINT `matches_ibfk_1` FOREIGN KEY (`id_joueur1`) 
REFERENCES `joueur` (`id`))

Il n’y a pas de joueur dans la table qui leur est réservée, donc toute tentative de rentrer un numéro de joueur dans id_joueur1 sera sanctionnée par ce message d’erreur !

Pour que la même contrainte pèse sur le champ id_joueur2, il faut effectuer exactement la même opération que précédemment, à avoir : la pose d’un index sur le champ ET la pose de la clé étrangère :

ALTER TABLE matches ADD INDEX (id_joueur2); 
ALTER TABLE matches ADD FOREIGN KEY (id_joueur)
REFERENCES joueur (id);

Voilà que maintenant pèse sur notre table matches deux contraintes d’intégrité référentielle qui font qu’il n’est possible de faire s’affronter que des joueurs répertoriés dans notre table joueur !

La fin d’une idée reçue

Je me suis aperçu que beaucoup de gens étaient encore persuadés qu’une contrainte de clé étrangère ne pouvait s’appliquer qu’entre un champ référençant et un champ référéncé obligatoirement clé primaire de sa table. C’est absolument faux et ce même si en pratique on s’en sert le plus souvent comme ça ! Pour qu’un champ puisse en référencer un autre, il suffit que tous les deux soient indexés et rien de plus. Comme la clé primaire est par définition un index, c’est elle qui rend cette référence possible mais autrement, vous pouvez tout à fait dans T1(A,B) faire que B référence D de T2(C,D) qui n’est pas le moins du monde la clé primaire de T2 ! (les C.P sont en gras)

A retenir

  • Pour poser une clé étrangère sur A qui référence B, il faut que A et B soient rigoureusement du même type ! Attention quand vous utilisez des types numériques, il faut que les signes (SIGNED/UNSIGNED) soient identiques !
  • Seul le moteur InnoDB autorise l’utilisation de ce mécanisme !
  • On remplit d’abord les tables référencées avant de remplir les référençantes !
  • On vide les tables référençantes avant de vider les référencées !
  • Si vous souhaitez contourner momentanément la vérification des contraintes d’intégrité, pour charger des données en masse par exemple, utilisez SET FOREIGN_KEY_CHECKS = 0;

MySQL et InnoDB : activer les fichiers au niveau table avec innodb_file_per_table

Fichiers de base d’InnoDB

Par défaut, votre configuration pour les tables utilisant le moteur InnoDB avec MySQL est la suivante :

  • un fichier ibdata1 constamment alimenté est créé
  • deux fichiers de log (ib_logfile0 et ib_logfile1) sont crées

Vous pouvez les voir en faisant : 

sebastien.ferrandez@sebastien$ ls -l /var/lib/mysql
total 28736
-rw-rw---- 1 mysql mysql 18874368 mai   21 10:17 ibdata1
-rw-rw---- 1 mysql mysql  5242880 mai   21 10:17 ib_logfile0
-rw-rw---- 1 mysql mysql  5242880 mai   20 15:58 ib_logfile1

Evidemment, il faudra au besoin modifier le chemin si vous avez customisé votre configuration MySQL. Le problème de ce fichier ibdata1 en mode append est qu’il a tendance à grossir très vite et à atteindre des tailles problématiques (de nature à remettre en cause le bon fonctionnement de MySQL) : sur ma machine locale, le mien faisait 3.4G ! Il m’a fallu dumper mes bases de données à des fins de sauvegarde, faire en sorte de ne plus avoir ce fichier monolithique mais plusieurs (par base de données et par table), redémarrer MySQL et rejouer mon script SQL pour remettre en place les données dont j’avais besoin rapidement.

mysql

Pour effectuer ce découpage propre par table, nous allons nous servir de la directive de configuration innodb_file_per_table. Comme je le disais plus haut, celle-ci n’est pas activée par défaut :

mysql> SHOW VARIABLES LIKE 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | OFF   |
+-----------------------+-------+

Pour l’activer, il vous suffit d’en faire mention dans votre fichier my.cnf (situé par défaut sur mon installation dans /etc/mysql/my.cnf), dans la section réservée à mysqld (par exemple du côté du commentaire « Fine-tuning ») et évidemment il vous faudra redémarrer le démon mysqld pour prendre en compte ce changement.

Voici l’option de configuration à rajouter :

[mysqld]
#
# * Fine Tuning
#
innodb_file_per_table   = 1

Vous pouvez également écrire tout simplement « innodb_file_per_table », ceci fonctionnera. Redémarrez ensuite le service MySQL :

sudo service mysql restart

Connectez-vous en ligne de commande à votre MySQL et retapez la commande donnée ci-dessus, vous devriez avoir du nouveau :

mysql> SHOW VARIABLES LIKE 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON    |
+-----------------------+-------+
1 row in set (0.00 sec)

Bien ! Notre modification est donc bien prise en compte ! Si je crée une nouvelle base de données et une nouvelle table :

mysql> create database lolcats; use lolcats;
Query OK, 1 row affected (0.00 sec)

Database changed
mysql> create table cat(id tinyint unsigned primary key, name varchar(20), age tinyint unsigned);
Query OK, 0 rows affected (0.04 sec)

Je retrouve bien tout ça dans mon répertoire MySQL :

sebastien.ferrandez@sebastien$ sudo ls -l /var/lib/mysql/lolcats/
total 116
-rw-rw---- 1 mysql mysql  8614 mai   21 10:44 cat.frm
-rw-rw---- 1 mysql mysql 98304 mai   21 10:44 cat.ibd
-rw-rw---- 1 mysql mysql    65 mai   21 10:43 db.opt

C’est bien le signe que le découpage « un fichier par table » est désormais en place !

Bénéficier du découpage avec une base de données déjà implantée

Si vous avez déjà une base de données et que vous vous apercevez qu’ibdata1 a grossi et qu’il est temps de passer à un fichier par table, procédez comme suit :

Créez un répertoire s’apprêtant à recueillir votre sauvegarde :

sebastien.ferrandez@sebastien:$ mkdir -p $HOME/mysql/backups/avant_filepertable

Faites une sauvegarde de l’intégralité de vos données :

sebastien.ferrandez@sebastien:$ mysqldump -u root -p --all-databases > $HOME/mysql/backups/avant_filepertable/all_databases.sql

Arrêtez le démon MySQL :

sebastien.ferrandez@sebastien:~$ sudo service mysql stop
[ ok ] Stopping MySQL database server: mysqld.

Naturellement, rajoutez votre option dans my.cnf :

[mysqld]
#
# * Fine tuning
#
innodb_file_per_table

Redémarrez MySQL :

sebastien.ferrandez@sebastien:~$ sudo service mysql start
[ ok ] Starting MySQL database server: mysqld ..
[info] Checking for tables which need an upgrade, are corrupt or were 
not closed cleanly..

A l’issue de ce redémarrage, pensez à valider la bonne prise en compte de cette option à l’aide du SHOW VARIABLES montré un peu plus haut dans ce billet.
Supprimez vos fichiers (à vous de voir si vous souhaitez conserver les logs) :

sebastien.ferrandez@sebastien:$ sudo rm -fr /var/lib/mysql/*

Installez de nouvelles tables système. Attention, les messages que j’ai mis en gras nécessitent votre attention !

sebastien.ferrandez@sebastien:$ sudo /usr/bin/mysql_install_db
Installing MySQL system tables...
OK
Filling help tables...
OK

To start mysqld at boot time you have to copy
support-files/mysql.server to the right place for your system

PLEASE REMEMBER TO SET A PASSWORD FOR THE MySQL root USER !
To do so, start the server, then issue the following commands:

/usr/bin/mysqladmin -u root password 'new-password'
/usr/bin/mysqladmin -u root -h sebastien password 'new-password'

Alternatively you can run:
/usr/bin/mysql_secure_installation

which will also give you the option of removing the test
databases and anonymous user created by default.  This is
strongly recommended for production servers.

Je vous conseille de passer par :

sudo /usr/bin/mysql_secure_installation

Voici les options que j’ai personnellement choisi :

Change the root password? [Y/n] n
 ... skipping.

Remove anonymous users? [Y/n] n
 ... skipping.
Disallow root login remotely? [Y/n]
 ... Success!

Remove test database and access to it? [Y/n]
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reload privilege tables now? [Y/n]

Ré-injectez vos données sauvegardées dans votre base qui maintenant sait découper par table :

sebastien.ferrandez@sebastien:~$ mysql -u root -p < $HOME/mysql/backups/avant_filepertable/all_databases.sql

Et validez que les tables que vous avez recrée se trouvent bien dans les répertoires correspondant à vos bases de données :

sebastien.ferrandez@sebastien:~$ sudo ls -l /var/lib/mysql/lolcats
total 116
-rw-rw---- 1 mysql mysql    65 mai   22 09:31 db.opt
-rw-rw---- 1 mysql mysql  8556 mai   22 10:02 cat.frm
-rw-rw---- 1 mysql mysql 98304 mai   22 10:02 cat.ibd

La table cat que j’ai crée se retrouve bien dans le répertoire correspondant à la base de données lolcats dont elle fait partie, l’opération est un succès !

Kit de premiers secours

Problèmes pouvant survenir au shutdown du démon MySQL

Si jamais vous n’arrivez pas à stopper MySQL, il s’agit sans doute d’un problème lié à l’utilisateur ‘debian-sys-maint’ du à la suppression des tables.
Voici comment le résoudre :

Récupérez d’abord le mot de passe courant de cet utilisateur (c’est une installation locale, je vous donne le mot de passe sans crainte) :

sebastien.ferrandez@sebastien:$ sudo grep password /etc/mysql/debian.cnf
password = JZe6ZMa9bMK4aqfm

Ensuite mettez-lui les bons privilèges dans votre ligne de commande MySQL :

mysql> GRANT ALL PRIVILEGES ON *.* TO 'debian-sys-maint'@'localhost' IDENTIFIED BY 'JZe6ZMa9bMK4aqfm' WITH GRANT OPTION;
Query OK, 0 rows affected (0.00 sec)

Changer le mot de passe root

Pour mettre p@$sW0rd! comme mot de passe à l’utilisateur root

mysqladmin -u root password p@$sW0rd!

Pour aller plus loin…

http://dev.mysql.com/doc/refman/5.0/fr/innodb-configuration.html
http://dev.mysql.com/doc/refman/5.5/en/innodb-multiple-tablespaces.html (anglais)

MySQL : comparaison rapide des types de données CHAR et VARCHAR

Quand on choisit d’utiliser des champs de type chaîne de caractères dans une table MySQL (mais pas que…), on en vient rapidement à se poser la question suivante :

CHAR ou VARCHAR ?

Voici le tableau comparatif que nous donne la documentation MySQL. Il suppose que nous utilisions un jeu de caractères codés sur 1 octet comme latin1 par exemple (de son vrai nom ISO 8859-1) :

snapshot2

CHAR va de 0 à 255 caractères. La longueur d’un champ de type CHAR est fixée à la valeur déclarée lors de la définition du champ : si vous créez un champ de type CHAR(30) et que vous souhaitez y insérer une chaîne de 31 caractères, cette valeur sera stockée sous une forme tronquée. Si la valeur insérée est inférieure à 30, le « reste » (les caractères manquants pour arriver à 30) sera comblé avec des espaces. Lorsque la valeur sera récupérée, les espaces seront enlevés automatiquement, vous n’y verrez que du feu !

Les VARCHAR sont eux utilisés pour des chaînes de longueur variable et donc appropriés pour des données dont on ne peut prédire la longueur de façon certaine. VARCHAR peut stocker jusqu’à 65535 caractères (bien plus que les 255 qu’une grande partie des gens ont en tête). Leur taille étant variable, elle doit être stockée quelque part…Ainsi, pour tout type VARCHAR, MySQL réserve un préfixe d’un octet si la taille des données est inférieure ou égale à 255 (un octet = 8 bits et 28 = 256) et deux octets dans le cas contraire (2 octets = 16 bits, 216 = 65536).

Que lit-on sur ce tableau ? Lorsque l’on stocke une chaîne de caractères vide dans un champ en CHAR(4), quatre caractères « espace » sont réservés et donc 4 octets alloués ; avec un VARCHAR(4), on n’alloue que l’octet nécessaire au préfixe des chaînes de moins de 255 caractères. Si l’on stocke deux caractères, en CHAR(4), deux espaces sont alloués « pour rien » alors qu’en VARCHAR(4) on a toujours l’octet nécessité par le préfixe et les deux octets de chaque caractère. Jusqu’ici, VARCHAR est moins gourmand en espace disque. La tendance s’inverse lorsque l’on remplit CHAR avec le nombre exact de caractères attendus : on économise un octet par rapport à un VARCHAR ! Lorsque la chaîne dépasse la longueur maximale prévue, elle est tronquée dans les deux cas, mais c’est toujours CHAR qui est plus économique !

Conclusion, quand on sait qu’une chaîne de caractères aura une longueur définie, mieux vaut privilégier CHAR (si cette longueur est évidemment inférieure à 255, mais ce sera dans 99,99% des cas, n’est-ce pas ?).

MySQL : l’opérateur ensembliste UNION (2/2)

Revenons sur l’opérateur ensembliste UNION et voyons comment il traite les duplicatas :

create table employe (
    id tinyint unsigned NOT NULL PRIMARY KEY,
    nom varchar(20) NOT NULL,
    prenom varchar(30) NOT NULL
);

create table manager(
    id smallint unsigned NOT NULL PRIMARY KEY,
    nom varchar(30),
    prenom varchar(20)
);

INSERT INTO employe VALUES (1, 'Klein', 'Roger'),
                           (2, 'Bagnole', 'Marcel'),
                           (3, 'De Narvale', 'Nadine');

INSERT INTO manager VALUES (3, 'Avignon', 'Eric'),
                           (2, 'Avril', 'Mathilda'),
                           (1, 'Klein', 'Roger');

Faisons l’union de ces deux relations :


select * from employe union select * from manager;

Roger Klein n’apparaît qu’une seule fois : UNION supprime bien les doublons !

Pour avoir l’intégralité de l’union des deux extensions de ces relations, il suffit de faire :


select * from employe union ALL select * from manager;

Et voilà que nous avons tout à présent !

snapshot2

 

Utiliser un UNION ALL vous permettra de faire apparaître l’intégralité des extensions des schémas unis.