Quels sont les avantages et les inconvénients d'effectuer des calculs en SQL par rapport à votre application


154

shopkeeper table contient les champs suivants:

id (bigint),amount (numeric(19,2)),createddate (timestamp)

Disons que j'ai le tableau ci-dessus. Je veux obtenir les enregistrements d'hier et générer un rapport en imprimant le montant en cents.

Une façon de faire est d'effectuer des calculs dans mon application java et d'exécuter une requête simple

Date previousDate ;// $1 calculate in application

Date todayDate;// $2 calculate in application

select amount where createddate between $1 and $2 

puis parcourez les enregistrements et convertissez le montant en cents dans mon application java et générez le rapport

Une autre méthode consiste à effectuer des calculs dans la requête SQL elle-même:

select cast(amount * 100 as int) as "Cents"
from shopkeeper  where createddate  between date_trunc('day', now()) - interval '1 day'  and  date_trunc('day', now())

puis parcourez les enregistrements et générez le rapport

D'une certaine manière, tout mon traitement est effectué dans l'application java et une simple requête est déclenchée. Dans les autres cas, toutes les conversions et calculs sont effectués dans une requête SQL.

Le cas d'utilisation ci-dessus n'est qu'un exemple, dans un scénario réel, une table peut avoir de nombreuses colonnes qui nécessitent un traitement du même type.

Pouvez-vous me dire quelle approche est la meilleure en termes de performances et d'autres aspects et pourquoi?


2
Les calculs de date auront peu ou pas d'effet du tout - en supposant que votre moteur SQL ne calculera effectivement vos dates qu'une seule fois. les avoir définis dans votre application est parfaitement logique, car ils y seront de toute façon définis à un moment donné, que ce soit pour le titre du rapport ou d'autres choses. la multiplication de la valeur par 100 dans ce cas peut être effectuée sur n'importe quel niveau, car vous allez de toute façon boucler sur ces lignes pour le rendu et * 100 est peu susceptible d'être plus lent sur n'importe quel niveau sauf le front-end. Dans les deux cas, vos calculs sont minimes et éclipsés par les opérations environnantes, pas un problème de performances.
Morg.

Réponses:


206

Cela dépend de nombreux facteurs - mais surtout:

  • la complexité des calculs (préfèrent faire crissement complexe sur une application serveur, depuis que les échelles sur , plutôt que d' un serveur db, qui échelles jusqu'à )
  • volume de données (si vous avez besoin d'accéder / d'agréger beaucoup de données, le faire sur le serveur de base de données économisera de la bande passante, et disk io si les agrégats peuvent être effectués dans des index)
  • commodité (sql n'est pas le meilleur langage pour le travail complexe - surtout pas idéal pour le travail procédural, mais très bon pour le travail basé sur des ensembles; gestion des erreurs moche, cependant)

Comme toujours, si vous faites apporter les données à l'application serveur, ce qui réduit les colonnes et les lignes seront à votre avantage. S'assurer que la requête est réglée et correctement indexée aidera les deux scénarios.

Re votre note:

puis parcourez les enregistrements en boucle

Faire une boucle dans les enregistrements est presque toujours la mauvaise chose à faire en SQL - l'écriture d'une opération basée sur un ensemble est préférable.

En règle générale , je préfère réduire au minimum le travail de la base de données "stocker ces données, récupérer ces données" - cependant, il existe toujours des exemples de scénarios où une requête élégante sur le serveur peut économiser beaucoup de bande passante.

Considérez également: si cela coûte cher en calcul, peut-il être mis en cache quelque part?

Si vous voulez un "ce qui est mieux" précis ; codez-le dans les deux sens et comparez-le (en notant qu'un premier brouillon de l'un ou l'autre n'est probablement pas réglé à 100%). Mais tenez compte de l'utilisation typique à cela: si, en réalité, il est appelé 5 fois (séparément) à la fois, alors simulez cela: ne comparez pas juste un seul "1 de ceux-ci contre 1 de ceux-ci".


Le bouclage implique un traitement plus ou moins «ligne à la fois». Et cela signifie une latence réseau de 2 * plus quatre commutateurs de contexte aller-retour. Oui: c'est cher. Une opération de SGBD "native" fait tout le travail pour minimiser les E / S disque (appels système) mais parvient à récupérer plus d'une ligne par appel système. Une ligne à la fois prend au moins quatre appels système.
wildplasser

@wildplasser n'est pas nécessaire; le serveur peut diffuser des lignes que vous consommez au fur et à mesure qu'elles arrivent - une métaphore de «lecteur» n'est pas rare.
Marc Gravell

1
@Marc Cavell: Eh bien, cela dépend. Dans le cas où l'empreinte d'un programme d'application n'est qu'un enregistrement logique, c'est plus ou moins OK. Mais la plupart des "frameworks" que je connais ont tendance à aspirer tous les enregistrements au démarrage et à les déclencher, un par un. Le verrouillage est un autre écueil.
wildplasser

Je pense qu'une bonne règle de base est la suivante: ne ramenez pas du serveur SQL des lignes de données dont vous n'avez finalement pas besoin. Par exemple, si vous devez effectuer des opérations d'agrégation, elles appartiennent probablement à SQL. Jointures entre tables ou sous-requêtes? SQL. C'est aussi l'approche que nous utilisons avec les badges, et, jusqu'à présent, nous faisons face à l'échelle :-)
Sklivvz

1
@zinking ce serait une opération basée sur un ensemble. Dans ce scénario, vous n'écrivez pas le code de la boucle - c'est un détail d'implémentation. Par "boucle", j'entends des boucles explicites, par exemple un curseur
Marc Gravell

86

Permettez-moi d'utiliser une métaphore: si vous voulez acheter un collier en or à Paris, l'orfèvre pourrait s'asseoir au Cap ou à Paris, c'est une question de compétence et de goût. Mais vous n'expédiez jamais des tonnes de minerai d'or d'Afrique du Sud vers la France pour cela. Le minerai est traité sur le site minier (ou du moins dans la zone générale), seul l'or est expédié. La même chose devrait être vraie pour les applications et les bases de données.

En ce qui concerne PostgreSQL , vous pouvez faire presque tout sur le serveur, de manière assez efficace. Le SGBDR excelle dans les requêtes complexes. Pour les besoins de procédure, vous pouvez choisir parmi une variété de langages de script côté serveur : tcl, python, perl et bien d'autres. Cependant, j'utilise principalement PL / pgSQL .

Le pire des cas serait d'aller à plusieurs reprises sur le serveur pour chaque ligne d'un ensemble plus grand. (Ce serait comme expédier une tonne de minerai à la fois.)

Deuxièmement , si vous envoyez une cascade de requêtes, chacune dépendant de la précédente, alors que tout cela peut être fait en une seule requête ou procédure sur le serveur. (C'est comme expédier l'or et chacun des bijoux avec un navire séparé, séquentiellement.)

Les allers-retours entre l'application et le serveur sont coûteux. Pour serveur et client. Essayez de réduire cela, et vous gagnerez - ergo: utilisez des procédures côté serveur et / ou SQL sophistiqué si nécessaire.

Nous venons de terminer un projet où nous avons intégré presque toutes les requêtes complexes dans les fonctions Postgres. L'application transmet les paramètres et obtient les ensembles de données dont elle a besoin. Rapide, propre, simple (pour le développeur de l'application), E / S réduites au minimum ... un collier brillant à faible empreinte carbone.


12
Je serais prudent d'utiliser cette analogie pour prendre des décisions de conception significatives avec d'autres développeurs. Les analogies sont plus un artifice rhétorique qu'un instrument logique. Entre autres facteurs, il est beaucoup moins coûteux d'envoyer des données à un serveur d'application que d'envoyer du minerai d'or à un orfèvre.
Doug

3
Vous enverrez des minerais ou de l'or en fonction de ce qui est moins cher, si vous n'avez pas de technologie pour convertir le minerai en or, ou si c'est trop cher (parce que les mineurs veulent tuer ces autres travailleurs), vous l'expédierez à un autre endroit, peut-être en entre orfèvre et mineurs, surtout si vous avez plus d'un orfèvre.
Dainius

1
exactement ce que je suis d'accord, je ne pense pas que ce soit toujours une mauvaise chose de faire un calcul basé sur la boucle dans SQL @a_horse_with_no_name, parfois cela doit être fait de toute façon, je préférerais qu'il soit calculé lorsque les données sont récupérées comme la métaphore d'Erwin l'indique. ou vous devez répéter cela à un coût lorsque les données sont récupérées.
zinking le

-1 Parce que c'est un argument unilatéral, ignore les compromis et met en place un homme de paille pour le côté opposé au lieu de considérer et de réfuter le meilleur cas du côté opposé. «Les allers-retours entre l'application et le serveur coûtent cher» - absolument: mais ce n'est pas la seule chose qui coûte cher, et les différentes dépenses doivent être mises en balance les unes par rapport aux autres. Il se peut que les requêtes "SQL sophistiquées" ou les procédures stockées soient les meilleures pour le cas particulier; mais les détails de l'affaire doivent généralement être pris en compte lors de ce genre de détermination.
yfeldblum

Analogie sympa mais malheureusement elle est basée sur de fausses hypothèses. L'expédition de minerai d'or est très courante. Le rapport de décapage de l'or est d'environ 1: 1 (or aux déchets), mais il est souvent moins cher de le traiter hors site, où un meilleur équipement et une meilleure qualité de fabrication sont disponibles. En fonction de la taille de l'envoi, augmenter l'efficacité du traitement de 0,1% peut permettre une augmentation relative des revenus (malgré le prix d'expédition doublé) - car l'or est assez cher de nos jours. D'autres minerais, comme le fer par exemple, sont généralement également expédiés (le taux de décapage du fer est d'environ 60%!).
Chris Koston

18

Dans ce cas, il vaut probablement mieux faire le calcul en SQL car le moteur de base de données est susceptible d'avoir des routines d'arithmétique décimale plus efficaces que Java.

En général, cependant, pour les calculs au niveau des lignes, il n'y a pas beaucoup de différence.

Là où cela fait une différence, c'est:

  • Les calculs agrégés comme SUM (), AVG (), MIN (), MAX () ici, le moteur de base de données sera un ordre de grandeur plus rapide qu'une implémentation Java.
  • Partout où le calcul est utilisé pour filtrer les lignes. Le filtrage au niveau de la base de données est beaucoup plus efficace que la lecture d'une ligne puis sa suppression.

12

Il n'y a pas de noir / blanc en ce qui concerne les parties de la logique d'accès aux données qui doivent être exécutées dans SQL et les parties qui doivent être effectuées dans votre application. J'aime la formulation de Mark Gravell , faisant la distinction entre

  • calculs complexes
  • calculs gourmands en données

La puissance et l'expressivité de SQL sont largement sous-estimées. Depuis l'introduction des fonctions de fenêtre , de nombreux calculs non strictement orientés ensemble peuvent être effectués très facilement et élégamment dans la base de données.

Trois règles empiriques doivent toujours être suivies, quelle que soit l'architecture globale de l'application:

  • réduire la quantité de données transférées entre la base de données et l'application (en faveur du calcul des éléments dans la base de données)
  • réduire la quantité de données chargées à partir du disque par la base de données (en faveur de laisser la base de données optimiser les instructions pour éviter tout accès inutile aux données)
  • ne pas pousser la base de données à ses limites de CPU avec des calculs complexes et simultanés (en faveur de l'extraction des données dans la mémoire de l'application et d'y effectuer des calculs)

D'après mon expérience, avec un DBA décent et des connaissances décentes sur votre base de données décente, vous ne rencontrerez pas très bientôt les limites de votre CPU DB.

Quelques lectures supplémentaires où ces choses sont expliquées:


2

En général, faites les choses en SQL s'il y a des chances que d'autres modules ou composants dans le même projet ou d'autres projets aient besoin d'obtenir ces résultats. une opération atomique effectuée côté serveur est également meilleure car il vous suffit d'appeler le proc stocké depuis n'importe quel outil de gestion de base de données pour obtenir les valeurs finales sans autre traitement.

Dans certains cas, cela ne s'applique pas mais quand c'est le cas, cela a du sens. aussi en général, la db box a le meilleur matériel et les meilleures performances.


La réutilisabilité peut être présente à n'importe quel niveau et n'est pas une raison (en termes de performances) de mettre plus de calculs dans SQL. "En général la db box": c'est faux et en plus, comme le disait marc gravell, la mise à l'échelle ne fonctionne pas de la même façon. La plupart des bases de données nécessitent peu de matériel pour être exécutées correctement, et le modèle de performance n'a pas grand-chose à voir avec celui d'un serveur d'applications (c'est-à-dire que je dépenserais 2/3 de mon budget pour un serveur SQL sur des E / S divines alors que je ne dépenserais pas plus. que quelques centaines pour la pile de stockage d'un serveur d'applications).
Morg.

1

Si vous écrivez sur ORM ou si vous écrivez des applications occasionnelles à faible performance, utilisez le modèle qui simplifie l'application. Si vous écrivez une application haute performance et réfléchissez bien à l'échelle, vous gagnerez en déplaçant le traitement vers les données. Je recommande vivement de déplacer le traitement vers les données.

Pensons à cela en deux étapes: (1) transactions OLTP (petit nombre d'enregistrements). (2) OLAP (longues analyses de nombreux enregistrements).

Dans le cas OLTP, si vous voulez être rapide (10 000 à 100 000 transactions par seconde), vous devez supprimer les conflits de verrouillage, de verrouillage et de verrouillage mort de la base de données. Cela signifie que vous devez éliminer les longues interruptions dans les transactions: les allers-retours du client à la base de données pour déplacer le traitement vers le client sont l'un de ces longs arrêts. Vous ne pouvez pas avoir de transactions de longue durée (pour rendre la lecture / mise à jour atomique) et avoir un débit très élevé.

Re: mise à l'échelle horizontale. Les bases de données modernes sont mises à l'échelle horizontalement. Ces systèmes implémentent déjà la haute disponibilité et la tolérance aux pannes. Tirez parti de cela et essayez de simplifier votre espace d'application.

Regardons OLAP - dans ce cas, il devrait être évident que faire glisser éventuellement des terrabytes de données vers l'application est une idée horrible. Ces systèmes sont conçus spécifiquement pour fonctionner de manière extrêmement efficace contre des données en colonnes compressées et pré-organisées. Les systèmes OLAP modernes évoluent également horizontalement et disposent de planificateurs de requêtes sophistiqués qui dispersent le travail horizontalement (déplacement interne du traitement vers les données).


0

Que ce soit pour effectuer des calculs en front-end ou en backend est très décidé si nous pouvons déterminer notre objectif dans la mise en œuvre de l'entreprise. À l'époque, le code java peut être plus performant qu'un code SQL à la fois bien écrit ou vice-versa. Mais si vous êtes confus, vous pouvez essayer de déterminer d'abord -

  1. Si vous pouvez réaliser quelque chose de simple via la base de données sql, alors mieux vaut y aller car db fonctionnera beaucoup mieux et effectuera des calculs là et ensuite avec le résultat de la récupération. Cependant, si le calcul réel nécessite trop de calculs ici et là, vous pouvez utiliser le code de l'application. Pourquoi? Parce que dans la plupart des cas, les scénarios comme la boucle ne sont pas mieux gérés par SQL, alors que les langages frontaux sont mieux conçus pour ces choses.
  2. Dans le cas où un calcul similaire est requis à partir de nombreux endroits, il sera évidemment préférable de placer le code de calcul à la fin de la base de données pour garder les choses au même endroit.
  3. S'il y a beaucoup de calculs à faire pour obtenir le résultat final via de nombreuses requêtes différentes, optez également pour db end car vous pouvez placer le même code dans une procédure stockée pour être plus performant que de récupérer les résultats du backend, puis de les calculer à l'avant fin.

Il y a de nombreux autres aspects auxquels vous pouvez réfléchir avant de décider où placer le code. Une perception est totalement fausse - Tout peut être fait mieux en Java (code d'application) et / ou tout est mieux fait par le db (code sql).


0

Formez un point de vue sur les performances: il s'agit d'une opération arithmétique très simple qui peut presque certainement être effectuée beaucoup plus rapidement que d'extraire réellement les données des disques qui sous-tendent la base de données. En outre, le calcul des valeurs dans la clause where est susceptible d'être très rapide à tout moment d'exécution. En résumé, le goulot d'étranglement doit être le disque IO, pas le calcul des valeurs.

En ce qui concerne la lisibilité, je pense que si vous utilisez un ORM, vous devriez le faire dans votre environnement de serveur d'application, car l'ORM vous permettra de travailler très facilement avec les données sous-jacentes, en utilisant des opérations basées sur des ensembles. Si vous allez de toute façon écrire du SQL brut, il n'y a rien de mal à faire le calcul là-bas, votre SQL serait également un peu plus beau et plus facile à lire s'il est formaté correctement.


0

Fondamentalement, la «performance» n'est pas définie.

Celui qui compte le plus pour moi est le temps des développeurs.

Écrivez la requête SQL. Si c'est trop lent ou si la base de données devient un goulot d'étranglement, alors reconsidérez. À ce moment-là, vous serez en mesure de comparer les deux approches et de prendre votre décision en fonction de données réelles pertinentes pour votre configuration (matériel et quelle que soit la pile sur laquelle vous vous trouvez).


0

Je ne pense pas que les différences de performances puissent être raisonnées sans exemples et références spécifiques, mais j'ai une autre idée:

Que pouvez-vous mieux entretenir? Par exemple, vous souhaiterez peut-être basculer votre frontal de Java vers Flash, ou HTML5, ou C ++, ou autre chose. Un grand nombre de programmes ont subi un tel changement, ou existent même dans plusieurs langues pour commencer, car ils doivent fonctionner sur plusieurs appareils.

Même si vous avez une couche intermédiaire appropriée (d'après l'exemple donné, il semble que ce n'est pas le cas), cette couche pourrait changer et JBoss pourrait devenir Ruby / Rails.

D'un autre côté, il est peu probable que vous remplaciez le backend SQL par quelque chose qui n'est pas une base de données relationnelle avec SQL et même si vous le faites, vous devrez de toute façon réécrire le front-end à partir de zéro, donc le point est sans objet.

Mon idée est que si vous effectuez des calculs dans la base de données, il sera beaucoup plus facile d'écrire une deuxième couche frontale ou intermédiaire plus tard, car vous n'avez pas à tout réimplémenter. Dans la pratique cependant, je pense que «où puis-je faire cela avec un code que les gens comprendront» est le facteur le plus important.


Si vous passez de jboss à ruby, il est très probable que vous changiez db (et vous devrez de toute façon adopter ces calculs) et il n'est pas si improbable que vous puissiez changer pour quelque chose de plus différent, comme nosql.
Dainius

0

Pour simplifier la réponse, il faudrait se pencher sur l'équilibrage de charge. Vous voulez placer la charge là où vous avez le plus de capacité (si cela a du sens). Dans la plupart des systèmes, c'est le serveur SQL qui devient rapidement un goulot d'étranglement, donc la réponse est probablement que vous ne voulez pas que SQL fasse une once de travail de plus que nécessaire.

Dans la plupart des architectures, ce sont également les serveurs SQL qui constituent le cœur du système et les systèmes extérieurs qui sont ajoutés.

Mais les calculs ci-dessus sont si triviaux qu'à moins que vous ne poussiez votre système à la limite, le meilleur endroit pour le placer est où vous voulez le mettre. Si les calculs n'étaient pas triviaux, comme le calcul de sin / cos / tan pour, par exemple, un calcul de distance, l'effort pourrait devenir non trivial et nécessiter une planification et des tests minutieux.


0

Les autres réponses à cette question sont intéressantes. Étonnamment, personne n'a répondu à votre question. Vous vous demandez:

  1. Vaut-il mieux effectuer un cast en cents dans la requête? Je ne pense pas que le casting de cents ajoute quoi que ce soit à votre requête.
  2. Vaut-il mieux utiliser now () dans la requête? Je préférerais passer des dates dans la requête au lieu de les calculer dans la requête.

Plus d'informations: Pour la première question, vous voulez être sûr que l'agrégation des fractions fonctionne sans erreurs d'arrondi. Je pense que le numérique 19,2 est raisonnable pour l'argent et dans le second cas, les nombres entiers sont OK. Utiliser un flotteur pour de l'argent est faux pour cette raison.

Pour la deuxième question, j'aime avoir un contrôle total en tant que programmeur sur la date considérée comme «maintenant». Il peut être difficile d'écrire des tests unitaires automatiques lors de l'utilisation de fonctions comme now (). En outre, lorsque vous avez un script de transaction plus long, il peut être bon de définir une variable égale à now () et d'utiliser la variable de sorte que toute la logique utilise exactement la même valeur.


0

Permettez-moi de prendre un exemple concret pour répondre à cette question

J'avais besoin de calculer une moyenne mobile pondérée sur mes données ohlc, j'ai environ 134000 bougies avec un symbole pour chacune pour le faire

  1. Option 1 Faites-le en Python / Node, etc.
  2. Option 2 Faites-le dans SQL lui-même!

Quel est le meilleur?

  • Si je devais le faire en Python, essentiellement, je devrais récupérer tous les enregistrements stockés dans le pire des cas, effectuer le calcul et tout sauvegarder, ce qui, à mon avis, est un énorme gaspillage d'E / S.
  • La moyenne mobile pondérée change à chaque fois que vous obtenez une nouvelle bougie, ce qui signifie que je ferais des quantités massives d'E / S à intervalles réguliers, ce qui n'est pas une bonne opinion à mon avis
  • En SQL, tout ce que j'ai à faire est probablement d'écrire un déclencheur qui calcule et stocke tout, il suffit donc de récupérer les valeurs WMA finales pour chaque paire de temps en temps et c'est tellement plus efficace

Exigences

  • Si je devais calculer WMA pour chaque bougie et la stocker, je le ferais sur Python
  • Mais comme je n'ai besoin que de la dernière valeur, SQL est beaucoup plus rapide que Python

Pour vous donner quelques encouragements, voici la version Python pour faire une moyenne mobile pondérée

WMA fait par code

import psycopg2
import psycopg2.extras
from talib import func
import timeit
import numpy as np
with psycopg2.connect('dbname=xyz user=xyz') as conn:
with conn.cursor() as cur:
t0 = timeit.default_timer()
cur.execute('select distinct symbol from ohlc_900 order by symbol')
for symbol in cur.fetchall():
cur.execute('select c from ohlc_900 where symbol = %s order by ts', symbol)
ohlc = np.array(cur.fetchall(), dtype = ([('c', 'f8')]))
wma = func.WMA(ohlc['c'], 10)
# print(*symbol, wma[-1])
print(timeit.default_timer() - t0)
conn.close()

WMA via SQL

"""
if the period is 10
then we need 9 previous candles or 15 x 9 = 135 mins on the interval department
we also need to start counting at row number - (count in that group - 10)
For example if AAPL had 134 coins and current row number was 125
weight at that row will be weight = 125 - (134 - 10) = 1
10 period WMA calculations
Row no Weight c
125 1
126 2
127 3
128 4
129 5
130 6
131 7
132 8
133 9
134 10
"""
query2 = """
WITH
condition(sym, maxts, cnt) as (
select symbol, max(ts), count(symbol) from ohlc_900 group by symbol
),
cte as (
select symbol, ts,
case when cnt >= 10 and ts >= maxts - interval '135 mins'
then (row_number() over (partition by symbol order by ts) - (cnt - 10)) * c
else null
end as weighted_close
from ohlc_900
INNER JOIN condition
ON symbol = sym
WINDOW
w as (partition by symbol order by ts rows between 9 preceding and current row)
)
select symbol, sum(weighted_close)/55 as wma
from cte
WHERE weighted_close is NOT NULL
GROUP by symbol ORDER BY symbol
"""
with psycopg2.connect('dbname=xyz user=xyz') as conn:
with conn.cursor() as cur:
t0 = timeit.default_timer()
cur.execute(query2)
# for i in cur.fetchall():
# print(*i)
print(timeit.default_timer() - t0)
conn.close()

Croyez-le ou non, la requête s'exécute plus rapidement que la version Pure Python de faire une MOYENNE DE MOUVEMENT PONDÉRÉE !!! Je suis allé étape par étape dans l'écriture de cette requête alors accrochez-vous et vous ferez très bien

La vitesse

0,42141127300055814 secondes Python

0,23801879299935536 secondes SQL

J'ai 134000 faux enregistrements OHLC dans ma base de données répartis sur 1000 actions, c'est donc un exemple où SQL peut surpasser votre serveur d'applications


1
Cependant, si vous devez le faire des millions de fois aussi rapidement que possible, il est beaucoup plus facile de créer des applications Python parallèles que des répliques de base de données. Jusqu'à une certaine échelle, s'appuyer davantage sur SQL est certainement plus rapide / moins cher, mais il y a finalement un point de basculement où il est préférable de faire ce calcul dans votre application.
Lenny
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.