Pourquoi «Select * from table» est-il considéré comme une mauvaise pratique?


96

Hier, je discutais avec un programmeur "hobby" (je suis moi-même un programmeur professionnel). Nous avons découvert une partie de son travail et il dit qu'il interroge toujours toutes les colonnes de sa base de données (même sur le code / serveur de production).

J'ai essayé de le convaincre de ne pas le faire, mais je n'ai pas encore eu autant de succès. À mon avis, un programmeur ne devrait interroger que ce qui est réellement nécessaire pour des raisons de "joliesse", d'efficacité et de trafic. Est-ce que je me trompe avec ma vue?


1
Je dirais que c'est parce que si le contenu de la table change? ajouter / supprimer des colonnes? vous sélectionnez toujours * .. alors vous allez rater des choses ou extraire plus de données que nécessaire.
JF il

2
@JFit Cela en fait partie, mais loin de toute l'histoire.
Jwenting



@gnat une question peut-elle vraiment être considérée comme une copie d'une question fermée? (c.-à-d. parce que celui qui était fermé ne convenait pas vraiment en premier lieu)
gbjbaanb

Réponses:


67

Réfléchissez à ce que vous récupérez et à la manière dont vous les liez aux variables de votre code.

Pensez maintenant à ce qui se passe lorsque quelqu'un met à jour le schéma de la table pour ajouter (ou supprimer) une colonne, même si vous ne l'utilisez pas directement.

L'utilisation de select * lorsque vous saisissez des requêtes à la main convient, pas lorsque vous écrivez des requêtes pour du code.


8
Les performances, la charge du réseau, etc., sont bien plus importants que la commodité de remettre les colonnes dans l'ordre et avec le nom que vous voulez.
Jwenting

21
@jwenting vraiment? La performance compte-t-elle plus que l'exactitude? En tout cas, je ne vois pas que "select *" fonctionne mieux que de sélectionner uniquement les colonnes souhaitées.
gbjbaanb

9
@Bratch, dans des environnements de production réels, vous pouvez avoir des centaines d'applications utilisant les mêmes tables et il est impossible que toutes ces applications puissent être gérées correctement. Vous avez raison, mais, dans la pratique, l'argument échoue en raison de la réalité du travail dans les entreprises. Les modifications de schéma dans les tables actives ont lieu tout le temps.
user1068

18
Je ne comprends pas le but de cette réponse. Si vous ajoutez une colonne à une table, SELECT * et SELECT [Colonnes] fonctionneront. La seule différence est que si le code doit être lié à la nouvelle colonne, SELECT [Colonnes] devra être modifié alors que le SELECT * ne sera pas. Si une colonne est supprimée d'une table, SELECT * sera rompu au point de liaison, alors que SELECT [Colonnes] sera rompu lors de l'exécution de la requête. Il me semble que SELECT * est l'option la plus flexible, car toute modification de la table ne nécessiterait que des modifications de la liaison. Est-ce que je manque quelque chose?
TallGuy

11
@gbjbaanb puis accédez aux colonnes par nom. Tout le reste serait évidemment stupide, sauf si vous spécifiez l'ordre des colonnes dans la requête.
user253751

179

Changements de schéma

  • Extraire par ordre --- Si le code extrait la colonne # comme moyen d’obtenir les données, une modification du schéma entraînera un réajustement des numéros de colonne. Cela va gâcher l'application et de mauvaises choses vont arriver.
  • Extraire par nom: si le code extrait une colonne par son nom foo, et qu'une autre table de la requête ajoute une colonne foo, la façon dont cela est traité peut entraîner des problèmes lors de la tentative d'obtention de la colonne de droite foo .

Dans les deux cas, une modification de schéma peut entraîner des problèmes lors de l'extraction des données.

Déterminez ensuite si une colonne en cours d'utilisation est supprimée de la table. Le select * from ...fonctionne toujours mais des erreurs quand essayant de tirer les données sur l'ensemble des résultats. Si la colonne est spécifiée dans la requête, la requête sera erreur en donnant au lieu une indiciation claire quant à quoi et où est le problème.

Surcharge de données

Certaines colonnes peuvent avoir une quantité importante de données associées. Sélectionner en arrière *va extraire toutes les données. Oui, voici que varchar(4096)c'est sur 1000 lignes que vous avez sélectionnées en vous donnant une possibilité supplémentaire de 4 mégaoctets de données dont vous n'avez pas besoin, mais qui sont quand même envoyées sur le réseau.

En ce qui concerne le changement de schéma, varchar peut ne pas exister lors de la création initiale de la table, mais maintenant, il existe.

Défaut de transmettre l'intention

Lorsque vous sélectionnez retour *et obtenez 20 colonnes mais n'en avez besoin que de 2, vous ne communiquez pas l'intention du code. En regardant la requête qui fait, select *on ne sait pas quelles sont les parties importantes de celle-ci. Puis-je modifier la requête pour qu'elle utilise plutôt cet autre plan afin de l'accélérer en n'incluant pas ces colonnes? Je ne sais pas parce que l'intention de ce que la requête retourne n'est pas claire.


Regardons quelques violons SQL qui explorent un peu plus ces changements de schéma .

Tout d’abord, la base de données initiale: http://sqlfiddle.com/#!2/a67dd/1

DDL:

create table one (oneid int, data int, twoid int);
create table two (twoid int, other int);

insert into one values (1, 42, 2);
insert into two values (2, 43);

SQL:

select * from one join two on (one.twoid = two.twoid);

Et de retour les colonnes que vous obtenez sont oneid=1, data=42, twoid=2et other=43.

Maintenant, que se passe-t-il si j'ajoute une colonne à la première table? http://sqlfiddle.com/#!2/cd0b0/1

alter table one add column other text;

update one set other = 'foo';

Et comme avant mes résultats de la même requête sont oneid=1, data=42, twoid=2et other=foo.

Un changement dans l'une des tables perturbe les valeurs de a select *et soudainement votre liaison de 'other' à un int va lancer une erreur et vous ne savez pas pourquoi.

Si au lieu de cela votre instruction SQL était

select 
    one.oneid, one.data, two.twoid, two.other
from one join two on (one.twoid = two.twoid);

Le changement de table 1 n'aurait pas perturbé vos données. Cette requête s'exécute de la même manière avant et après le changement.


Indexage

Lorsque vous faites un, select * fromvous tirez toutes les lignes de toutes les tables qui correspondent aux conditions. Même les tables qui vous intéressent vraiment. Cela signifie que plus de données sont transférées, mais un autre problème de performances se cache plus loin dans la pile.

Index. (lié à SO: Comment utiliser index dans une instruction select? )

Si vous extrayez un grand nombre de colonnes, l'optimiseur de plan de base de données peut ne pas utiliser un index, car vous devrez quand même extraire toutes ces colonnes. Cela prendrait plus de temps d'utiliser l'index, puis toutes les colonnes de la requête. que ce serait juste de faire un scan complet de la table.

Si vous êtes sélectionnez le, par exemple, le nom d'un utilisateur (que vous faire beaucoup et ont donc un indice sur elle), la base de données peut faire un balayage d' index unique ( index postgres wiki uniquement scan , mysql analyse complète tableau vs complet scan d'index , Index seule analyse: Éviter le tableau d' accès ).

Il y a pas mal d'optimisations pour ne lire que des index, si possible. Les informations peuvent être extraites plus rapidement sur chaque page d’index, car vous en tirez moins aussi - vous n’ajoutez pas toutes les autres colonnes pour le select *. Il est possible qu'une analyse d'index seulement renvoie des résultats de l'ordre de 100 fois plus rapidement (source: Select * is bad ).

Cela ne veut pas dire qu'une analyse d'index complète est excellente, c'est toujours une analyse complète - mais c'est mieux qu'une analyse de table complète. Une fois que vous commencez à rechercher toutes les conséquences select *négatives sur les performances, vous continuez à en trouver de nouvelles.

Lecture connexe


2
@ Tonny, je serais d'accord - mais quand j'ai répondu (le premier), je n'avais jamais pensé que cette question générerait autant de discussions et de commentaires! Il est évident de ne demander que les colonnes nommées, n'est-ce pas?!
gbjbaanb

3
Tout casser en ajoutant une colonne est également une bonne raison pour laquelle le code devrait toujours accéder aux colonnes dans un datareader par nom, et non par ordinal codé en dur ...
Julia Hayward

1
@ gbjbaanb C'est pour moi. Mais beaucoup de gens viennent pour écrire des requêtes SQL sans formation / fond formel. Pour eux, c'est peut-être pas évident.
Tonny

1
@Aaronaught Je l'ai mis à jour avec le bit supplémentaire sur les problèmes d'indexation. Y a-t-il d'autres points que je devrais aborder pour le tort de select *?

3
Wow, la réponse acceptée était si pauvre à expliquer quoi que ce soit que j'ai voté contre. Je suis étonné que ce ne soit pas la réponse acceptée. +1
Ben Lee

38

Autre préoccupation: s'il s'agit d'une JOINrequête et que vous récupérez les résultats dans un tableau associatif (comme cela pourrait être le cas en PHP), cela risque de provoquer des bogues.

Le truc c'est que

  1. si la table fooa des colonnes idetname
  2. si la table bara des colonnes idet address,
  3. et dans votre code que vous utilisez SELECT * FROM foo JOIN bar ON foo.id = bar.id

devinez ce qui se passe quand quelqu'un ajoute une colonne nameà la bartable.

Le code cessera soudainement de fonctionner correctement, car maintenant la namecolonne apparaît deux fois dans les résultats et si vous stockez les résultats dans un tableau, les données de second name( bar.name) écraseront le premier name( foo.name)!

C'est un bug assez méchant parce que c'est très peu évident. Cela peut prendre un certain temps, et il est impossible que la personne qui ajoute une autre colonne à la table ait pu anticiper un tel effet secondaire indésirable.

(Histoire vraie).

Donc, n'utilisez pas *, contrôlez les colonnes que vous récupérez et utilisez des alias, le cas échéant.


d'accord dans ce cas (que je considère comme une sorte de rare) cela pourrait être un problème majeur. Mais vous pouvez toujours l'éviter (et la plupart des gens le feront probablement) en interrogeant le caractère générique et en ajoutant simplement un alias pour les noms de colonnes identiques.
le baconing

4
En théorie, mais si vous utilisez un caractère générique pour plus de commodité, vous vous en remettez à vous donner automatiquement toutes les colonnes existantes et ne vous souciez jamais de mettre à jour la requête à mesure que les tables s'agrandissent. Si vous spécifiez chaque colonne, vous devez obligatoirement accéder à la requête pour en ajouter une autre à votre SELECTclause. Nous espérons que le nom n'est pas unique. BTW Je ne pense pas que ce soit si rare dans les systèmes avec des bases de données volumineuses. Comme je l'ai dit, une fois, j'ai passé une ou deux heures à la recherche de ce bogue dans un gros fichier de code PHP. Et j'ai trouvé un autre cas tout à l'heure: stackoverflow.com/q/17715049/168719
Konrad Morawski

3
J'ai passé une heure la semaine dernière à essayer de faire passer cela par le biais d'une tête de consultants. Il est supposé être un gourou de SQL ... Soupir ...
Tonny

22

Interroger chaque colonne peut être parfaitement légitime, dans de nombreux cas.

Toujours interroger chaque colonne n'est pas.

Cela demande plus de travail à votre moteur de base de données, qui doit fouiller et scruter ses métadonnées internes pour déterminer les colonnes à traiter avant de pouvoir réellement obtenir les données et les vous renvoyer. Bien sûr, ce n’est pas la plus grosse surcharge au monde, mais les catalogues système peuvent être un goulot d’étranglement appréciable.

Cela représente plus de travail pour votre réseau, car vous retirez un grand nombre de champs alors que vous ne voulez peut-être qu’un ou deux d’entre eux. Si quelqu'un [d'autre] va et ajoute quelques dizaines de champs supplémentaires, qui contiennent tous de gros morceaux de texte, votre débit passe soudainement au sol - sans raison apparente. Cela est aggravé si votre clause "where" n'est pas particulièrement bonne et que vous retirez également un grand nombre de lignes, ce qui risque de générer beaucoup de données qui traversent le réseau (c'est-à-dire que cela va être lent).

Cela demande plus de travail à votre application, car elle doit extraire et stocker toutes ces données supplémentaires dont elle ne s’occupe probablement pas.

Vous courez le risque que les colonnes changent leur ordre. OK, vous ne devriez pas avoir à vous soucier de cela (et vous ne le ferez pas si vous ne sélectionnez que les colonnes dont vous avez besoin), mais si vous les récupérez toutes en même temps et que quelqu'un d'autre décide de réorganiser l'ordre des colonnes dans le tableau. , cette exportation CSV soigneusement conçue que vous donnez aux comptes dans le couloir va tout à coup à la poterie - encore une fois, sans raison apparente.

Au fait, j'ai déjà dit "quelqu'un d'autre" plusieurs fois, ci-dessus. Rappelez-vous que les bases de données sont intrinsèquement multi-utilisateurs; vous ne pouvez pas avoir le contrôle sur eux que vous pensez avoir.


3
Je penserais que toujours interroger chaque colonne peut être légitime pour des choses comme des facilités d’affichage de tables sans schéma. Ce n'est pas une situation très courante, mais dans le contexte d'outils à usage interne uniquement, de telles choses peuvent être pratiques.
Supercat

1
@ supercat C'est à peu près le SEUL cas d'utilisation valide pour un "SELECT *" auquel je peux penser. Et même alors, je préférerais limiter la requête à "SELECT TOP 10 *" (dans MS SQL) ou ajouter "LIMIT 10" (mySQL) ou ajouter "WHERE ROWNUM <= 10" (Oracle). Habituellement, dans ce cas, il s'agit plus de "quelles colonnes sont là et de quelques exemples de données" que du contenu complet.
Tonny

@ Tonny: SQL Server a modifié ses scripts par défaut pour ajouter la TOPlimitation; Je ne sais pas à quel point c'est important si le code en lit autant qu'il veut en afficher et ensuite se débarrasser de la requête. Je pense que les réponses aux requêtes sont traitées un peu paresseusement, bien que je ne connaisse pas les détails. En tout cas, je pense qu'au lieu de dire "n'est pas légitime", il serait préférable de dire "... est légitime dans beaucoup moins"; En gros, je résumerais les cas légitimes comme ceux où l'utilisateur aurait une meilleure idée de ce qui est significatif que le programmeur.
Supercat

@supercat je peux accepter cela. Et j'aime vraiment la façon dont vous le mettez dans votre dernière phrase. Je dois me souvenir de celui-là.
Tonny

11

La réponse courte est: cela dépend de la base de données utilisée. Les bases de données relationnelles sont optimisées pour extraire les données dont vous avez besoin de manière rapide, fiable et atomique . Sur des jeux de données volumineux et des requêtes complexes, il est beaucoup plus rapide et probablement plus sûr que SELECTing * et effectue l'équivalent de jointures du côté "code". Les magasins de valeurs clés ne disposent peut-être pas de telles fonctionnalités ou ne sont pas suffisamment matures pour être utilisés en production.

Cela dit, vous pouvez toujours renseigner la structure de données que vous utilisez avec SELECT * et utiliser le reste dans le code, mais vous rencontrerez des goulots d'étranglement si vous souhaitez évoluer.

La comparaison la plus proche est le tri des données: vous pouvez utiliser quicksort ou bubblesort et le résultat sera correct. Mais ne sera pas optimisé, et aura certainement des problèmes lorsque vous introduisez la concurrence et que vous devez trier de manière atomique.

Bien sûr, il est moins coûteux d’ajouter de la RAM et des processeurs que d’investir dans un programmeur capable de faire des requêtes SQL et ayant même une compréhension vague de ce qu’est un JOIN.


Apprenez le SQL! Ce n'est pas si difficile. C'est le langage "natif" des bases de données de loin. C'est puissant. C'est élégant. Il a résisté à l'épreuve du temps. Et il n’est pas possible d’écrire une jointure du côté "code" qui soit plus efficace que celle de la base de données, à moins que vous ne soyez vraiment incapable de faire des jointures SQL. Considérez que pour faire une "jointure de code", vous devez extraire toutes les données des deux tables, même dans une simple jointure à 2 tables. Ou utilisez-vous des statistiques d'index et utilisez-vous celles-ci pour choisir les données de la table à extraire avant de vous rejoindre? Je ne le pensais pas ... Apprenez à utiliser la base de données correctement, les gens.
Craig

@Craig: SQL est commun dans les bases de données relationnelles de loin. C'est loin d'être le seul type de base de données, cependant ... et il y a une raison pour laquelle les approches de base de données plus modernes s'appellent souvent NoSQL. : P Personne que je connais n'irait appeler SQL "élégant" sans une lourde dose d'ironie. Cela craint moins que la plupart des alternatives, en ce qui concerne les bases de données relationnelles.
cHao

@ cHao Je suis très au courant des divers autres types de bases de données disponibles depuis des décennies . La base de données Pick "nosql" existe depuis toujours. "NoSQL" n'est même pas un nouveau concept à distance. Les ORM existent aussi depuis toujours et ils ont toujours été lents. Lent! = Bien. En ce qui concerne l’élégance (LINQ?), Vous ne pouvez pas me convaincre que c’est raisonnable ou élégant pour une clause where: Customer customer = this._db.Customers.Where( “it.ID = @ID”, new ObjectParameter( “ID”, id ) ).First();Voir «Le temps qu'il faut pour prendre une infraction» à la page 2.
Craig

@Craig: Ne me lance même pas sur ORM. Presque tous les systèmes là-bas le font horriblement, et l'abstraction fuit de partout. En effet , les enregistrements de base de données relationnelle ne sont pas des objets . Au mieux, ils représentent les entrailles sérialisables d’une partie d’un objet. Mais pour LINQ, vous voulez vraiment y aller? L'équivalent SQLish ressemble à quelque chose comme var cmd = db.CreateCommand(); cmd.CommandText = "SELECT TOP 1 * FROM Customers WHERE ID = @ID"; cmd.Parameters.AddWithValue("@ID", id); var result = cmd.ExecuteReader();.... et vous pouvez ensuite créer un client à partir de chaque ligne. LINQ bat le pantalon de ça.
cHao

@Craig: Certes, ce n'est pas aussi élégant qu'il pourrait l'être. Mais il ne sera jamais aussi élégant que je le souhaiterais jusqu'à ce qu'il puisse convertir le code .net en SQL. :) A quel point vous pourriez dire var customer = _db.Customers.Where(it => it.id == id).First();.
cHao

8

OMI, c'est à propos d'être explicite vs implicite. Lorsque j'écris du code, je veux que cela fonctionne parce que je l'ai fait fonctionner, et pas seulement parce que toutes les pièces se trouvent juste là. Si vous interrogez tous les enregistrements et que votre code fonctionne, vous aurez tendance à aller de l'avant. Plus tard, si quelque chose change et que votre code ne fonctionne plus, c’est une douleur royale de déboguer de nombreuses requêtes et fonctions à la recherche d’une valeur qui devrait être présente et les seules références référencées sont *.

Également dans une approche à plusieurs niveaux, il est toujours préférable d'isoler les perturbations de schéma de base de données au niveau des données. Si votre couche de données passe * dans la logique métier et très probablement dans la couche de présentation, vous développez votre portée de débogage de manière exponentielle.


3
C'est probablement l'une des raisons les plus importantes ici, et elle ne compte que pour une infime fraction des voix. La maintenabilité d'une base de code encombrée select *est bien pire!
Eamon Nerbonne

6

parce que si la table reçoit de nouvelles colonnes, vous obtenez toutes celles-ci même lorsque vous n'en avez pas besoin. avec varcharscela peut devenir beaucoup de données supplémentaires qui doivent voyager à partir de la DB

Certaines optimisations de base de données peuvent également extraire les enregistrements de longueur non fixe dans un fichier séparé pour accélérer l’accès aux parties de longueur fixe.


1

À part les frais généraux, ce que vous voulez éviter en premier lieu, je dirais qu'en tant que programmeur, vous ne dépendez pas de l'ordre des colonnes défini par l'administrateur de la base de données. Vous sélectionnez chaque colonne même si vous en avez besoin à tous.


3
D'accord, bien que je recommande également d'extraire les valeurs d'un résultat par nom de colonne dans tous les cas.
Rory Hunter

Détaché, porté. Utilisez les noms de colonne, ne dépendez pas de l'ordre des colonnes. L'ordre des colonnes est une dépendance fragile. Les noms doivent (vous espérez) avoir été dérivés de certains efforts de conception, ou vous devez explicitement aliaser des colonnes composites ou des calculs ou des noms de colonnes en conflit dans votre requête, et référencer l'alias explicite que vous avez spécifié. Mais s'en remettre à l'ordre, c'est à peu près tout du ruban adhésif et de la prière ...
Craig

1

Je ne vois aucune raison pour laquelle vous ne devriez pas utiliser le but pour lequel il est construit - récupérer toutes les colonnes d'une base de données. Je vois trois cas:

  1. Une colonne est ajoutée à la base de données et vous la souhaitez également dans le code. a) Avec * échouera avec un message approprié. b) Sans * fonctionnera, mais ne fera pas ce que vous attendez, ce qui est très mauvais.

  2. Une colonne est ajoutée à la base de données et vous ne la voulez pas dans le code. a) avec * échouera; cela signifie que * ne s'applique plus car sa sémantique signifie "tout récupérer". b) Sans * fonctionnera.

  3. Une colonne est supprimée Le code échouera de toute façon.

Le cas le plus courant est le cas 1 (puisque vous avez utilisé *, ce qui signifie tout ce que vous voulez probablement tout); sans * vous pouvez avoir un code qui fonctionne bien mais ne fait pas ce qui est attendu - beaucoup, bien pire - ce code qui échoue avec un message d'erreur approprié .

Je ne prends pas en considération le code qui extrait les données de colonne en fonction de leur index, ce qui est sujet aux erreurs, à mon avis. C'est beaucoup plus logique de le récupérer en fonction du nom de la colonne.


Votre prémisse est incorrecte. Select *était plus destiné à faciliter les requêtes ad-hoc et non à développer des applications. Ou pour une utilisation dans des constructions statistiques telles select count(*)que celles qui permettent au moteur de requête de décider si un index doit être utilisé, quel index utiliser, etc., et vous ne renvoyez aucune donnée de colonne réelle. Ou pour une utilisation dans des clauses telles que where exists( select * from other_table where ... ), qui est à nouveau une invitation au moteur de requête à choisir seul le chemin le plus efficace et la sous-requête est uniquement utilisée pour contraindre les résultats de la requête principale. Etc.
Craig

@Craig Je crois que chaque livre / tutoriel sur SQL dit qu'il select *a la sémantique de récupérer toutes les colonnes; si votre application en a vraiment besoin, je ne vois aucune raison de ne pas l'utiliser. Pouvez-vous indiquer une référence (Oracle, IBM, Microsoft, etc.) qui indique que le but pour lequel la select *construction a été construite n'est pas de récupérer toutes les colonnes?
m3th0dman

Eh bien, bien sûr, il select *existe pour récupérer toutes les colonnes ... comme une fonctionnalité pratique, pour les requêtes ad-hoc, pas parce que c'est une excellente idée dans un logiciel de production. Les raisons sont déjà assez bien couvertes dans les réponses de cette page, c’est pourquoi je n’ai pas créé ma propre réponse détaillée: •) Problèmes de performances, regroupement répétitif de données sur le réseau que vous n’utilisez jamais, •) problèmes liés au repliement de colonnes, •) Echecs d'optimisation du plan de requête (échec dans certains cas d'utilisation des index), •) E / S serveur inefficaces dans les cas où la sélection limitée aurait pu utiliser uniquement des index, etc.
Craig

Peut-être y at- il un cas marginal ici ou là qui justifie l’utilisation de select *dans une application de production réelle, mais la nature d’un cas marginal est qu’il ne s’agit pas d’ un cas courant . :-)
Craig

@Craig Les raisons sont contre l'extraction de toutes les colonnes d'une base de données, pas contre l'utilisation select *; ce que je disais si vous avez vraiment besoin de toutes les colonnes, je ne vois pas pourquoi vous ne devriez pas utiliser select *; bien que peu de scénarios doivent exister où toutes les colonnes sont nécessaires.
m3th0dman

1

Pensez-y de cette façon ... si vous interrogez toutes les colonnes d'une table qui ne contient que quelques chaînes ou champs numériques, cela représente un total de 100 000 données. Mauvaise pratique, mais ça va marcher. Ajoutez maintenant un seul champ contenant, par exemple, une image ou un document Word de 10 Mo. maintenant, votre requête performante commence immédiatement et mystérieusement à mal fonctionner, simplement parce qu'un champ a été ajouté à la table ... vous n'avez peut-être pas besoin de cet énorme élément de données, mais parce que vous l'avez fait, Select * from Tablevous l'obtenez quand même.


6
cela semble simplement répéter ce que nous avons déjà dit il y a quelques heures dans une première réponse et dans quelques autres réponses
gnat le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.