Exécution parallèle de requêtes relationnelles et - smis
Transcription
Exécution parallèle de requêtes relationnelles et - smis
Exécution parallèle de requêtes relationnelles et équilibrage de charge Luc Bouganim Projet Rodin - INRIA Rocquencourt 78153 Le Chesnay Cedex * Travail effectué dans le GIE Dyade (Bull Inria) RÉSUMÉ : L’utilisation de machines parallèles pour les systèmes de gestion de bases de données constitue une solution aux besoins de performances de ces applications. Afin d’exploiter le parallélisme, les requêtes sont découpées en plusieurs tâches, exécutées sur plusieurs processeurs. A cet effet, le parallélisme inter-opérateurs (plusieurs opérateurs sont exécutés en parallèle sur plusieurs processeurs) et intra-opérateur (un opérateur est cloné sur plusieurs processeurs) sont utilisés. Un des obstacles majeur à l’obtention de bonnes performances réside dans l’équilibrage de la charge entre les différents processeurs. En effet, le temps de réponse d’une exécution parallèle est égal au temps de réponse du processeur le plus chargé. Une mauvaise distribution de la charge de travail peut donc entraîner une chute de performance importante. Après une présentation des différentes architectures multiprocesseurs pour les SGBD, nous exposons, les techniques mises en oeuvres pour l’exécution parallèle de requêtes ainsi que les solutions existantes au problème de l’équilibrage de charge. ABSTRACT : The use of parallel machines for database management system is the solution for high performance aplications. There are two dimensions for parallelizing queries : horizontally (i.e. intra-operator parallelism) by distributing each operator among several processors, and vertically (i.e. inter-operator parallelism) by distributing all operators of the query among several processors. However, query response time can be hurt by several barriers, the major one being poor load balancing, i.e. some processors are overloaded while some others remain idle. As the response time of a set of parallel activities is that of the longest one, this can severely degrade performance. After presenting parallel architectures for database systems, we describe technics for parallel query execution. Afterwards, we present existing solutions for load balancing. MOTS-CLÉS : bases de données, parallélisme, équilibrage de charge KEYWORD : databases, parallelism, load balancing L’objectif spécifique d’un système parallèle de gestion de bases de données est de profiter au mieux des caractéristiques de la machine multiprocesseur sur laquelle il s’exécute. Son efficacité est directement fonction de sa rapidité à traiter l’ensemble des transactions s’appliquant sur les données de la base. On distingue deux types de transactions qui conduisent à exploiter le parallélisme de manière différente. Le parallélisme interrequête vise à améliorer le débit transactionnel de transactions courtes de type OLTP (On Line Transaction Processing). Nous ne considérons pas ici cette forme de parallélisme potentiellement présent dans les systèmes de bases de données multi-utilisateurs. Dans le cas de transactions longues de type aide à la décision, le parallélisme intra-requête, consistant à exécuter en parallèle différentes parties d’une même requête, est utilisé pour réduire le temps de réponse de chaque transaction. La Figure 1 présente l’architecture standard d’un SGBD exploitant ce type de parallélisme. Les requêtes de l’utilisateur sont envoyées au préprocesseur qui vérifie sa validité syntaxique et sémantique à l’aide d’informations stockées dans le schéma de la base. La requête est alors transformée en algèbre relationnelle puis optimisée. L’optimiseur évalue un certain nombre de stratégies d’exécution potentiellement efficaces, et choisi celle qui optimise l’utilisation des ressources de la machine, tout en produisant le meilleur temps de réponse. Le plan d’exécution parallèle ainsi produit, décrit la décomposition de la requête en fragments qu’il est ensuite possible d’exécuter en parallèle. L’exécution est finalement prise en charge par le moteur d’exécution dont le rôle est d’obtenir les meilleures performances sur l’architecture multiprocesseur cible. Compilation Requête SQL Préprocesseur Informations schéma Schéma de la Base Algèbre Relationnelle Optimiseur parallèle Informations physiques Plan d’exécution parallèle Exécution Moteur d’exécution .... Machine parallèle Figure 1. Architecture Standard d’un SGBD Parallèle Cet article présente l’ensemble des techniques ainsi que les principaux travaux relatifs au problème de l’exécution parallèle de requêtes. Dans la section 1, nous décrivons en détail les architectures matérielles, leurs avantages et inconvénients, ainsi que les principaux prototypes ou produits existants pour chaque architecture. La section 2 présente les différentes approches possibles pour la mise en oeuvre du parallélisme. La section 3 présente les plans d’exécution parallèle et en particulier, les différentes formes d’arbres d’exécution. La section 4 décrit les métriques permettant de quantifier le gain d’une exécution parallèle. Les problèmes liés à l’exécution parallèle sont décrit dans la section 5. Le problème de la répartition de charge est plus largement abordé, et les solutions existantes sont décrites. La section 6 conclut cette étude. 1. Architectures matérielles Dans cette section, nous présentons les architectures parallèles les plus courament utilisées pour les systèmes de gestion de bases de données. 1.1. Architectures à mémoire partagée ou shared-memory Dans les architectures mémoire partagée, la mémoire est accessible et partagée par l’ensemble des processeurs de la machine. Chaque processeur a un coût uniforme d’accès à la mémoire, on parle alors de machine UMA (Uniform Memory Access). Une architecture UMA est constituée par un bus reliant entre eux tous les éléments matériels de la machine : processeurs, modules mémoire et disques. Appelée aussi SMP (Symetric MultiProcessor), cette architecture a été adoptée sur des plate-formes diverses, depuis les stations de travail à quatre processeurs jusqu’aux plus grosses configurations comme les machines MULTIMAX série 500 fabriquées par ENCORE ou la série SYMMETRY fabriquée par SEQUENT [Lov88]. P1 cache Mémoire1 Pn Mémoirek Disque1 Disquep cache Bus Figure 2. Architecture UMA L’architecture UMA (c.f. Figure 2) permet un accès rapide aux données mais limite le nombre de processeurs du système. En effet, le bus, élément matériel partagé par tous les processeurs constitue le goulot d’étranglement d’une telle architecture. Sa bande passante limitée interdit de très grosses configurations. Ce type de système représente cependant un excellent rapport coût/performance pour des applications de bases de données de taille moyenne. De plus, la répartition de la charge de travail sur les différents processeurs peut être efficacement réalisée grâce à l’utilisation de la mémoire partagée. Enfin, le modèle de programmation des architectures à mémoire partagée facilite grandement le portage d’une application développée pour un système monoprocesseur. Pour ces raisons, l’architecture UMA a été largement adoptée par l’ensemble des constructeurs du marché. Les prototypes de systèmes parallèles de gestion de bases de données s’exécutant sur des machines à mémoire partagée sont principalement XPRS [Sto88], Volcano [Gra94], DBS3 [Ber93] et Monet [Bon96]. Pour les systèmes commerciaux, Oracle [Dav92] et Informix [Dan92] proposent des solutions sur ce type d’architecture. 1.2. Architecture distribuées ou shared-nothing (SN) Dans l’architecture SN, excepté le réseau d’interconnexion, rien n’est partagé entre les processeurs. Ainsi, chaque noeud de la machine consiste en un processeur avec ses disques et sa mémoire locale. Les noeuds communiquent entre eux par envois de messages via un réseau d’interconnexion permettant des échanges à haut débit (Figure 3). Réseau d’interconnexion P1 P2 Pn Mémoire locale Mémoire locale Mémoire locale Disque(s) Disque(s) Disque(s) Figure 3. Architecture shared-nothing Les partisans de l’architecture shared-nothing [Bor88] [DeW92b] considèrent que l’extensibilité de cette architecture est un facteur décisif justifiant l’adoption des machines SN pour les systèmes parallèles de gestion de bases de données. En effet, sur ce type de machines, chacun des processeurs gère directement une portion de la base. Le partage des ressources matérielles et logicielles entre les différents processeurs est donc limité au minimum. Ainsi, une machine parallèle adoptant une architecture SN peut théoriquement monter jusqu’à quelques centaines de processeurs. Cependant, la répartition de la charge de travail entre les processeurs de la machine SN est complexe et coûteuse. De nombreux systèmes parallèles de gestion de bases de données exploitent ce type d’architecture comme TANDEM [Che93], Bubba [Bor88], Gamma [DeW90], Teradata [Ter83], EDBS [Lop89] et PRISMA/DB [Ape92]. 1.3. Architectures à disques partagés ou shared disks L’architecture à disques partagés est illustrée Figure 4. Cette architecture a été notamment adoptée dans Oracle à partir de la version 6.0 (Oracle Parallel Server [Dav92]). L’architecture à disques partagés tente de faire un compromis entre l’architecture à mémoire partagée et shared-nothing. Les disques sont partagés pour faciliter la répartition de la charge de travail tandis que la mémoire reste distribuée pour ne pas trop pénaliser l’extensibilité. De plus, contrairement à l’architecture shared-nothing, le partage des disques permet une grande disponibilité des données puisque la totalité de la base de données continue à être accessible même si l’un des nœuds devient indisponible. P1 P2 Pn Mémoire locale Mémoire locale Mémoire locale Réseau d’interconnexion Disque Disque Disque Figure 4. Architecture shared-disks Comme la mémoire n’est pas partagée, une page disque peut se retrouver dupliquée dans plusieurs noeuds de la machine. Il est nécessaire de garantir la cohérence entre toutes les copies d’une même page. Le surcoût de maintenance de cette cohérence est bien sûr le point faible de ce type d’architecture, et cela d’autant plus que le nombre de mises à jour est important. L’architecture à disques partagés est décrite de manière plus détaillée dans . 2. Formes de parallélisme. Le modèle relationnel se prête bien à la parallélisation des requêtes. Une requête relationnelle, sous sa forme algébrique, est formée par un graphe d’opérateurs s’appliquant sur de grands ensembles de données ou relations. En exécutant en parallèle plusieurs opérateurs du graphe de la requête, il est possible de générer du parallélisme de type interopérateur. En fragmentant les opérandes d’un seul opérateur, il est possible de la décomposer en sous-opérateurs identiques conduisant à générer du parallélisme de type intra-opérateur. Ces deux types de parallélisme peuvent être combinés ou non pour améliorer le temps de réponse de la requête. Dans la section 2.1, nous présentons la mise en oeuvre du parallélisme intra-et interopérateur pour des modèles d’exécutions basés sur la fragmentation, i.e., où chaque processeur n’accède qu’à un sous ensemble des relations accédées. L’approche non fragmentée, présentée dans la section 2.2, est uniquement applicable sur des architectures à mémoire partagée. Elle est une simple parallélisation d’une exécution centralisée. Dans ce cas, on ne peut parler réellement de parallélisme intra ou inter-opérateur. 2.1. Modèle d’exécution fragmenté Dans un modèle d’exécution fragmenté, l’ensemble des relations (de base ou temporaires) sont fragmentées grâce à une fonction de fragmentation. Chaque processeur n’accède alors qu’à un sous ensemble des données. Cette approche est la seule possible sur une architecture shared-nothing, sans quoi les accès aux données, souvent distant entraînerait des surcoûts de communication prohibitifs. 2.1.1 Parallélisme intra-opérateur Le parallélisme intra-opérateur repose sur la décomposition d’un opérateur relationnel en un ensemble de sous-opérateurs indépendants, généralement appelées instances de l’opérateur initial. Cette décomposition est rendue possible grâce à la fragmentation statique et/ou dynamique des relations de la base, consistant à fragmenter horizontalement chaque relation en paquets. Il existe trois méthodes basiques de fragmentation des données. Ces méthodes, illustrées à la Figure 5, sont round-robin, par hachage et par intervalle (range). ... ... ... ... ... Round-Robin a-g h-m Hachage ... u-z Intervalle Figure 5. Méthodes de Fragmentation des Données La méthode round-robin place le iième tuple de la relation sur le disque numéro i mod n où n est le nombre disque. La méthode par hachage associe les tuples aux disques selon une fonction de hachage appliquée à l’un des attributs de la relation. Enfin, la méthode par intervalle place les tuples sur les disques par intervalles de valeurs. Chacune de ces méthodes a ses avantages et inconvénients. La méthode round-robin permet de répartir uniformément les tuples contrairement à la méthode par hachage ou par intervalle. Ainsi, elle garanti une bonne parallélisation des entrées/sorties lors de la lecture des relations. Elle n’a cependant pas d’intérêt pour les relations intermédiaires (sauf si elles sont stockées sur disque). Les deux dernières méthodes permettent, elles, de restreindre, dans certains cas, un calcul à un sous-ensemble de paquets, mais peuvent générer des fragments de taille très différente. Dans [Ber92], cette propriété est utilisée afin de réduire la complexité d’une requête. Une technique appelée task elimination permet de supprimer, dans une requête multi-jointure l’ensemble des sous-jointure ne produisant aucun résultat. Notons que, pour cet algorithme, une mauvaise distribution est un avantage, dans la mesure où elle permet plus d’élimination. Basé sur la fragmentation des données, le mécanisme général de parallélisation intraopérateur consiste alors à décomposer l’opérateur initial en sous-opérateurs utilisant chacun, un paquet de chaque relation en entrée. Souvent, la décomposition de l’opérateur peut bénéficier de la répartition initiale des données, sinon, une redistribution préalable des données est requise avant l’exécution de l’opérateur. Pour illustrer le parallélisme intraopérateur, prenons successivement le cas d’un opérateur de sélection puis d’un opérateur de jointure. Un opérateur de sélection sur une relation R peut être directement décomposé en n opérateurs de sélection identiques, chacun d’eux étant appliqué sur l’un des paquets de la relation initiale. Dans ce cas, aucune redistribution n’est requise et l’on a directement SEL (R) = ∪ SELi(Ri) quelque soit la méthode de fragmentation (c.f Figure 6). Notons que cela est indépendant de l’algorithme de sélection (avec ou sans index). Dans le cas d’une sélection avec index, l’index est fragmenté de la même manière. S S1 S2 S3 Sn Sel. Sel.1 Sel.2 Sel.3 Sel.n R1 R R2 R3 n = degré de parallélisme intra-opérateur Rn Sel. opérateur Sel.i L’instance i de l’opérateur Figure 6. Parallélisme Intra-opérateur Le cas d’un opérateur de jointure, opérateur binaire, est plus complexe que celui d’un opérateur de sélection. Pour ramener l’exécution de la jointure à l’exécution de n sousjointures utilisant un paquet de chaque relation en entrée, il faut que chaque paquet de la deuxième relation contienne tous les tuples de cette relation susceptibles de joindre avec le paquet correspondant de la première relation. Ceci n’est possible qu’avec la fragmentation par hachage ou par intervalle si l’attribut utilisé pour la fragmentation est l’attribut de jointure. Dans le cas contraire, une redistribution de l’une, ou des deux relations (suivant l’attribut de jointure) est nécessaire. Enfin, il faut noter que la méthode de fragmentation (hachage, round-robin, range) n’a aucun rapport avec l’algorithme local utilisé pour effectuer la jointure (boucle imbriquée, tri-fusion, hachage, etc..). Par exemple, dans le cas d’une jointure par hachage utilisant une fragmentation par hachage, deux fonctions de hachage distinctes seront utilisées. La première h1, permettra de fragmenter les relations de bases en paquet, la seconde h2 (qui peut être différente pour chaque instance) permettra d’effectuer la jointure par hachage. 2.1.2 Parallélisme inter-opérateur Le parallélisme inter-opérateur consiste à exécuter en parallèle des opérateurs d’une même requête. Il existe deux formes de parallélisme inter-opérateur. Le parallélisme inter-opérateur indépendant consiste à exécuter en parallèle deux opérateurs indépendants du graphe de la requête. Les plans d’exécution de type bushy-tree offrent de nombreuses opportunités pour mettre en œuvre cette forme de parallélisme. Sur l’exemple de la Figure 7a, OP1 et OP2 peuvent profiter de ce type de parallélisme. OP3 OP1 OP2 OP2 R T S S (a) OP1 (b) R Figure 7. Parallélisme inter-opérateur indépendant (a) et pipeline (b) Le parallélisme inter-opérateur pipeline consiste à exécuter deux opérateurs du graphe ayant un lien producteur-consommateur. Le résultat du producteur (OP1 sur la Figure 7b) n’est pas matérialisé mais envoyé en pipeline à l’opérateur suivant (OP2) sur le même principe qu’un pipe unix. Cela évite notamment la matérialisation complète de la relation intermédiaire (T). On appelle une chaîne pipeline l’ensemble des opérateurs exécutés en pipeline (OP1 et OP2 dans l’exemple). Peu de systèmes exploitent le parallélisme inter-opérateur. Par exemple, certains systèmes à mémoire distribuée comme Teradata [Ter83] ou Gamma [DeW86] forcent chaque relation à être fragmentée sur l’ensemble des noeuds de la machine parallèle (fulldeclustering). Le parallélisme intra-opérateur met alors à contribution l’ensemble des processeurs, ne laissant plus aucun intérêt au parallélisme inter-opérateur. De plus, la possibilité d’exploiter le parallélisme inter-opérateur complexifie à la fois le système d’exécution et l’optimiseur de requêtes qui doit considérer un nombre beaucoup plus important de plans d’exécution. 2.2. Modèle d’exécution non fragmenté On parle d’une approche non fragmentée lorsque la mise en oeuvre du parallélisme revient à la simple parallélisation d’une exécution mono-processeur. Sur un mono-processeur, l’exécution de plusieurs opérateurs successifs peut aussi être faite en utilisant le pipeline. Cette approche a l’intérêt de supprimer la matérialisation des relations intermédiaires. Dans ce cas, le pipeline est implémenté grâce à des appels de procédure. Par exemple, une sélection d’une relation R suivie de deux jointures avec S puis T se déroulera comme suit : Pour chaque tuple de R, s’il satisfait le prédicat de sélection, un appel sera fait à la procédure join1 avec comme argument le tuple (ou sa projection). Ce tuple sera alors testé avec la relation S. S’il joint, un appel sera fait à la procédure join2 avec comme argument le tuple résultat. Si de nouveau, ce tuple joint, il sera inséré dans la relation résultat, et ainsi de suite. Conceptuellement, nous avons donc les appels de procédures suivant : join2(T, join1(S, select(R))). Sur un multiprocesseur à mémoire partagée, il est possible de paralléliser cette stratégie en utilisant une forme de parallélisme intra-opérateur. Plusieurs threads1 sont crées au début de la l’exécution. Chaque thread traite alors un sous ensemble de la relation racine (R dans l’exemple) et exécute l’ensemble des opérateurs grâce au pipeline synchrone. Bien qu’il y ait une fragmentation des données pour les relations de bases (stockées sur disque), ce n’est pas le cas pour les autres relations. Chaque thread accède potentiellement à l’ensemble des relations S et T. Cette stratégie appelée pipeline parallèle synchrone est décrite dans [Hon92] [She93]. Ainsi, dans le cas du pipeline synchrone, on ne peut plus vraiment parler de parallélisme inter-opérateur puisque l’exécution pipeline est justement séquentielle. Par contre, le parallélisme indépendant peut encore être mis en oeuvre en exécutant plusieurs chaînes pipelines indépendantes concurremment. 3. Arbre d’opérateurs L’arbre d’opérateur provient de la macro-expansion de l’arbre de jointure [Has94]. Les 1. Un thread est un processus léger. Plusieurs threads peuvent être créés dans un même processus. Leur ordonnancement est alors moins coûteux que pour des processus. noeuds représentent les opérateurs atomiques de l'algèbre relationnelle et les arcs représentent les flots de données. Ces arcs peuvent être de deux types : bloquants ou pipelines. Un arc bloquant indique que les données doivent être entièrement produites avant de pouvoir commencer l’opérateur consommateur. Ainsi, un opérateur ayant une entrée bloquante doit attendre la matérialisation complète de l'opérande avant de commencer. Un arc pipeline indique que les données peuvent être consommées au fur et à mesure. Ainsi, le consommateur peut débuter dés que le premier tuple à consommer a été produit. Plusieurs formes d’arbres peuvent être considérées : linéaires (gauches ou droits) [Sch90], zigzag [Zia93] ou bushy [She93]. La Figure 8 présente les arbres droits, gauches et bushy pour une requête effectuant la jointure de 4 relations R, S, T et U. Dans un arbre droit, toutes les jointures sont effectuées en pipeline. On peut ainsi dire que tous les opérateurs s’exécutent en parallèle. En fait, l’exécution s’effectue souvent en au moins en deux phases. Dans une première phase, les relations à gauche (S, T et U sur la figure) sont chargées en mémoires (et éventuellement indexées, hachées ou triées). Dans la deuxième phase, les tuples de R sont testées successivement avec les relations S, T et U en pipeline. L’avantage d’un arbre droit est qu’il permet de ne pas matérialiser les relations intermédiaires et produit rapidement le premier résultat (pipeline). Toutefois, pour obtenir de bonnes performances, il faut que toutes les relations à gauche puissent tenir en mémoire. Dans le cas contraire, l’exécution pipeline générera du swapping. Dans un arbre gauche, les jointures sont toutes exécutées en séquence. Ainsi, l’exécution se fait en autant de phases qu’il y a de relations à joindre. Dans chaque phase une relation intermédiaire (ou le résultat final) est matérialisée. Cette stratégie d’exécution est utilisée lorsque la mémoire est limité car elle ne matérialise jamais plus de deux relations simultanément : une relation intermédiaire à gauche et le résultat de sa jointure, la relation à droite étant lue page à page. Les arbres gauches et droits sont deux stratégies extrêmes d’exécution d’un arbre linéaire. les arbres zigzag, introduits dans [Zia93], donnent une stratégie intermédiaire entre les arbres gauches et droits. Dans un arbre zigzag, une relation intermédiaire peut être matérialisée ou pipelinée selon la quantité de mémoire disponible. L’arbre linéaire droit est ainsi découpé en plusieurs sous-arbres droits exécutés séquentiellements. L’intérêt de cette stratégie est qu’elle permet d’effectuer plus de pipeline que les arbres gauches mais consomme moins de mémoire que les arbres droits. U U T R S T R S T U S R (c) (a) (b) Figure 8. Arbres linéaire gauche (a), bushy (b) et linéaire droit (c) Les arbres bushy sont les plus généraux. Ils n’imposent aucune contrainte sur les liens entre les opérateurs (bloquant, non-bloquant). Ils permettent d’obtenir du parallélisme indépendant [Lan93]. De plus, ils offrent un bon compromis entre le degré de parallélisme inter-opérateur et la consommation mémoire [She93]. Enfin, la considération des arbres bushy lors de l’optimisation augmente considérablement l’espace de recherche, permettant, de ce fait, l’obtention potentielle de meilleurs plans d’exécution. 4. Mesure de performances Le but de l’exécution parallèle de requêtes est d’obtenir un gain de performance. En plus du temps de réponse, il existe plusieurs métriques permettant de quantifier le gain obtenu par une exécution parallèle. Les plus connues sont le speed-up et le scale-up (voir par ex. ). Un système parallèle idéal est celui qui permet d’obtenir à la fois un speed-up linéaire et un scale-up constant. De manière informelle, un speed-up idéal indique qu’une tâche peut être exécutée deux fois plus vite si l’on dispose de deux fois plus de ressources matérielles (processeurs, disques, mémoire,...). Un scale-up idéal signifie qu’une tâche deux fois plus grosse peut être exécutée dans le même temps si l’on dispose de deux fois plus de ressources matérielles. La définition formelle du speed-up est la suivante : soit une tâche de taille fixe exécutée de manière séquentielle en un temps Ts puis exécutée en parallèle sur p processeurs en un temps Tp, le speed-up obtenu par l’exécution parallèle est alors défini par : Ts speed – up ( p ) = -----Tp et le speed-up est idéal si speed-up (p) = p. On peut déduire du speed-up l’efficacité Ep de l’algorithme parallèle, c’est à dire le rapport entre le speed-up effectif et le speed-up idéal : Ts Ep = ---------------Tp × p Par exemple, si l’on considère un problème résolu en 120 secondes sur une machine séquentielle alors qu’il faut 10 secondes sur une machine parallèle avec 16 processeurs, le speed-up est égal à 12 et l’efficacité du traitement parallèle est de 75%. Il est important de remarquer qu’il est plus facile d’obtenir de bon speed-up avec des algorithmes de jointures peu efficaces (p. ex. boucles imbriquées). En effet, la part des surcoûts d’exécution est d’autant plus grande que le temps de réponse est petit. La mesure de speed-up ne peut donc être prise pour comparer deux systèmes ou deux stratégies d’exécution. Il est dans ce cas, important de mesurer aussi les performances relatives des deux systèmes ou stratégies. La définition formelle du scale-up est la suivante : soient deux problèmes P1 et P2 avec P2 n fois plus gros que P1 et T(Pi, p) le temps d’exécution du problème Pi sur un système avec p processeurs, le scale-up est défini par la formule suivante : T ( P1, p ) scale – up = -----------------------------T ( P2, p × n ) Le scale-up est idéal si il reste toujours égal à 1. Notons qu’un problème n fois plus gros est obtenu par augmentation du volume de données à traiter. Par exemple, supposons que l’on désire mesurer le scale-up d’un opérateur de sélection. Pour obtenir une sélection n fois plus grosse, il suffit simplement de multiplier le volume des données à sélectionner par n. Ceci n’est plus vrai pour un opérateur de jointure dont la complexité n’est pas linéairement proportionnelle à la taille de ses opérandes. C’est pour cette raison que la mesure de speed-up, plus facile à mettre en oeuvre, est souvent préférée à celle du scale-up. 5. Problèmes liés à l’exécution parallèle Un ensemble de facteurs limite les gains obtenus lors d’une exécution parallèle. Dans cette section, nous montrons quels sont ces facteurs. Nous insistons particulièrement sur les problèmes de répartition de charge et présentons les solutions existantes. 5.1. Initialisation et mise en place du parallélisme Avant que l’exécution parallèle ne débute réellement, toute une phase d’initialisation et de mise en place du parallélisme est nécessaire. La phase d’initialisation consiste par exemple à ouvrir les relations impliquées dans la requête, à créer la relation résultat, à initialiser le contexte d’exécution propre à chaque opérateur. La phase de mise en place du parallélisme consiste, par exemple, à créer les processus ou les threads impliqués dans l’exécution parallèle et à établir les connexions entre ceux-ci pour mettre en place les communications inter-opérateur. Ce processus est, en général séquentiel et précède l’exécution parallèle proprement dite. Le temps d’initialisation et de mise en place du parallélisme explique en grande partie les mauvais speed-up obtenus pour les requêtes de faible complexité comme un opérateur de sélection sur une relation. Par exemple, les mesures de performance effectuées dans PRISMA/DB [Wil92] montrent un effondrement du speed-up sur les petites sélections, et cela dès que le nombre de processeurs utilisés devient supérieur à 8. Le degré de parallélisme doit être choisi en fonction de la complexité de la requête. [Wil92] présente une formule permettant de déterminer le speed-up maximal atteignable lors de l’exécution d’un seul opérateur et d’en déduire le nombre de processeur optimal pour son exécution. Considérons l’exécution d’un opérateur traitant N tuples avec n processeurs, le temps de traitement de chaque tuple étant c et le temps d’initialisation de l’exécution par processeur a. Le temps de réponse peut s’écrire avec la formule suivante : cN R = an + ------n Le temps de réponse minimum (correspondant au speed-up maximum) s’obtient alors facilement en dérivant cette formule par rapport à n et en l’égalant à 0. On obtient alors : 1 cN S 0 = -le speed-up maximal étant égal à n 0 = ------2 n0 a Dans le cas des systèmes à grande mémoire, le temps d’initialisation (an) n’est pas négligeable devant le temps de réponse. Ce qui limite le nombre de processeurs optimal. Pour des systèmes lisant les relations sur disque, le temps d’initialisation limite aussi le nombre maximum de processeurs mais dans d’autres proportions. Ces équations montrent, entre autres, qu’il ne semble pas judicieux d’exécuter chaque opérateur sur l’ensemble des processeurs (utilisant alors uniquement le parallélisme intra-opérateur). Il faut plutôt combiner parallélisme inter et intra-opérateur afin de diminuer le degré de parallélisme de chaque opérateur et d’en augmenter le speed-up. 5.2. Contrôle de l’exécution parallèle Lors de l’exécution parallèle d’une requête, des messages de contrôle sont échangés entre les différentes entités d’exécution (ex. processus) mises en jeu pour l’exécution parallèle. Ces messages de contrôle permettent par exemple de démarrer les opérateurs de la requête ou de signaler leur fin. Ainsi, plus le degré de parallélisme est élevé et plus le nombre de messages de contrôle est important. Pour les premiers systèmes parallèles de bases de données, le surcoût lié au contrôle de l’exécution parallèle était l’un des obstacles majeurs à l’obtention de bons speed-up. Un cas typique est le prototype DIRECT [Bor82], ancêtre de Gamma. Dans DIRECT, chaque page de données traitée donnait lieu à l’échange d’au minimum 1 message de contrôle vers un scheduler centralisé. L’échange de ces messages constituait le goulot d’étranglement du système et contre-balançait tous les gains de l’exécution parallèle. Depuis l’expérience de DIRECT, de nombreuses solutions ont été proposées visant à limiter le nombre de messages échangés pour la synchronisation et le déclenchement tout en éliminant la nécessité d’un contrôleur centralisé. Le travail le plus complet dans ce domaine est certainement celui effectué dans le cadre du projet EDS [EDS90] au travers du langage d’expression des plans d’exécution parallèle LERA-par [Cha92]. L’idée de base est d’introduire directement dans le code de la requête l’ensemble des opérateurs de contrôle nécessaires pour assurer une exécution parallèle correcte. De plus, les stratégies de contrôle sont construites à partir d’une bibliothèque d’opérateurs où chaque opérateur représente une fonctionnalité de contrôle élémentaire. Cette approche apporte une grande souplesse car la combinaison des fonctions élémentaires de contrôle disponibles dans la bibliothèque [Dag91][BoS91] permet l’élaboration de stratégies optimisées et adaptées à chaque requête, qui conduisent à une réduction du nombre de messages échangés. 5.3. Interférences et effets de convoi Une exécution hautement parallèle peut être freinée par l’apparition de phénomènes d’interférences. Ces phénomènes apparaissent lorsque plusieurs processeurs accèdent simultanément à une même ressource non partageable (ou peu partageable). Certains processeurs doivent alors être mis en attente, freinant ainsi l’exécution parallèle. Les phénomènes d’interférence peuvent survenir tant au niveau matériel (i.e. la ressource est matérielle) que logiciel (i.e. la ressource est une donnée partagée). Un exemple typique d’interférence matérielle est la contention créée au niveau du bus d’une machine à mémoire partagée lors des accès mémoire effectués par les processeurs. Plus le nombre de processeurs augmente et plus les conflits au niveau de ce bus augmentent. C’est la raison pour laquelle le nombre de processeurs est limité à quelques dizaines sur ce type d’architecture. Il en est de même pour les disques. Lorsque deux processus effectuent une entrée/sortie sur le même disque, ils sont séquentialisés au niveau du système. Notons que la multiplication des ressources matérielles permet de limiter les conflits d’accès. Par exemple, au niveau d’un disque, l’adoption de disques RAID (Redondant Array of Inexpensive Disks [Pat88]), permet de limiter ces interférences. Quant aux interférences logicielles, elles apparaissent lorsque plusieurs threads ou processus accèdent en parallèle à une même donnée [Fon89]. Pour prévenir tout risque d’incohérence, une variable verrou [Lam86] est généralement utilisée afin de séquentialiser les accès à la donnée (c.f. Figure 9). Lorsqu’un processus tente de prendre un verrou sur une donnée et que celui-ci est détenu par un autre processus, il est mis en attente. Ces interférences logicielles peuvent alors devenir le goulot d’étranglement du système et cela d’autant plus rapidement que des effets de convoi peuvent survenir. Un effet de convoi [Bla79] survient lorsqu’un processus est de-schedulé par le système alors qu’il détient un verrou. Cela peut survenir suite à l’épuisement de son quantum de temps sur le processeur ou à cause d’une entrée-sortie. Ce temps allonge la période de prise du verrou, augmentant du même coup la probabilité de conflits. Plus cette probabilité augmente et plus les processus en attente du verrou vont s’accumuler conduisant à un effet boule de neige. Un exemple typique d’interférence logicielle survient lors des accès aux tampons de la base de données par les différentes entités d’exécutions (threads ou processus). Pour prévenir tout risque d’incohérence, une variable verrou séquentialise l’accès à ces tampons. Comme le montre l’étude sur le réglage d’une requête de tri effectuée dans Volcano [Gra92], l’accès aux tampons peut alors devenir rapidement le goulot d’étranglement du système et cela même lorsque le nombre de processeurs est peu élevé. Par exemple, cette étude a montré que pour 16 processeurs, le surcoût occasionné par les interférences logicielles représentaient 45% du temps total d’exécution. Ce problème de contention lors de l’accès aux tampons de la base a aussi été reporté dans [Hon91] comme un des facteurs limitant le degré de parallélisme. Variables partagées Variables partagées Verrou Threads Accés incontrolé Accés séquentiel áccés parallèle Figure 9. Accés à des structures partagées La solution générale pour remédier aux problèmes d’interférences consiste à paralléliser la ressource partagée, c’est à dire à la décomposer en sous-ressources indépendantes. Deux sous-ressources distinctes peuvent alors être accédées en parallèle, limitant ainsi la probabilité d’interférence (c.f. Figure 9). Cette solution a été adoptée dans DBS3 [Cas94] à la structure d’accès aux tampons de la base, ce qui permet leur accès simultané par un grand nombre de threads. 5.4. Le problème de la répartition de la charge de travail Ce problème est certainement celui qui affecte le plus la mesure de speed-up d’une exécution parallèle. Nous avons présenté dans la section précédente les différentes techniques pour générer du parallélisme intra-requête. Ces techniques sont le parallélisme inter-opérateur et intra-opérateur. Utilisées conjointement ou non, elles permettent de fragmenter le code séquentiel d’une requête pour constituer plusieurs fragments ou tâches qu’il est ensuite possible d’exécuter en parallèle. Pour obtenir de bonnes performances, il est nécessaire que les tâches ainsi formées soient réparties équitablement entre les différents processeurs alloués pour l’exécution. Dans le cas contraire, certains processeurs vont recevoir plus de travail que d’autres et le temps de réponse de la requête sera alors égal au temps de réponse du processeur le plus chargé. L’algorithme de répartition des tâches sur l’ensemble des processeurs impliqués dans l’exécution est rendu difficile car le temps d’exécution peut grandement varier d’une tâche à l’autre. Cela est principalement dû à deux raisons. Premièrement, si le parallélisme interopérateur est exploité, ces tâches sont issues d’opérateurs différents du graphe de la requête conduisant à des coûts d’exécution très divers. Deuxièmement, même en ne considérant que le parallélisme intra-opérateur, une répartition biaisée des données (ou data skew) peut conduire à des différences de temps d’exécution entre les instances d’un même opérateur. La section 5.4.1 et 5.4.2 détaillent le problème de la répartition de charge intra et interopérateur et donnent un aperçu des solutions existantes pour chaque forme de parallélisme. 5.4.1 Répartition de charge intra-opérateur Walton et al. [Wal91] a classifié les effets d’une mauvaise répartition des données lors d’une exécution parallèle avec un modèle d’exécution fragmenté. Ceux-ci sont résumés sur un exemple, présenté à la Figure 10. JPS AVS/TPS JPS Res1 Res2 Join1 Join2 AVS/TPS S2 AVS/TPS S1 RS/SS Scan1 R2 RS/SS AVS/TPS Scan2 R1 Figure 10. Exemple de problèmes de mauvaises distributions de données Cet exemple décrit l’exécution d’une requête de sélection-jointure appliquée à deux relations R et S mal distribuées fragmentées en deux paquets. La mauvaise fragmentation de R et S provient soit des données elles-mêmes (skew de valeur des attributs ou AVS), soit de la fonction de fragmentation (skew de placement des tuples ou TPS). Les temps de traitement du paquet R1 ne sera alors pas égal à celui du paquet R2. Sur cet exemple, pour obtenir une répartition équitable de la charge de travail, il est nécessaire de mettre plus de puissance de calcul sur l’instance scan2 que sur l’instance scan1. Pour l’opérateur de jointure, c’est encore pire. D’une part, la taille inégale des paquets de la relation S (due à l’AVS et au TPS) entraîne des temps de traitement différents pour chaque tuple transmis depuis l’opérateur de scan. De plus, le nombre de tuples transmis varie d’une instance à l’autre suite à la mauvaise redistribution des paquets de R (skew de redistribution ou RS) ou à une sélectivité variable suivant le paquet de R traité (skew de sélectivité ou SS). Finalement, la sélectivité de la jointure peut aussi varier d’une instance à l’autre (skew de sélectivité de la jointure ou JPS). Une solution de répartition statique de la charge, i.e. basée sur une estimation de la complexité de chaque instance d’opérateur, semble donc complexe à mettre en oeuvre. En effet, pour que l’estimation soit correcte, il faut maintenir des statistiques du type histogramme sur les attributs de jointure, les attributs de fragmentation, les attributs participant à un prédicat (sélection ou jointure), donc potentiellement sur tous les attributs de la relation. Il semble plus raisonnable de tenter de redistribuer la charge de travail dynamiquement. Wolf et al. [Wol93] proposent de modifier les algorithmes classiques de jointure parallèle par tri-fusion et par hachage pour y ajouter une phase dite de scheduling. Cette phase, placée après la phase de tri ou de hachage, vise à balancer le travail entre les différents processeurs impliqués dans la phase ultérieure de jointure. Pour ce la jointure est décomposée en un nombre de sous-jointures plus important que le nombre de processeurs effectifs. Le coût d’exécution de chacune des sous-jointures est alors estimé et est utilisé pour les répartir sur les processeurs de manière à ce que l’écart à la moyenne de la charge de travail affectée à un processeur ne dépasse pas un seuil de tolérance fixé par avance. Si aucune répartition ne satisfait ce prérequis, la méthode est itérée en modifiant la phase de tri ou de hachage de manière à générer un plus grand nombre de sous-jointures, offrant ainsi plus d’opportunités à la phase de scheduling. Notons que la phase de scheduling utilise l’heuristique dite de LPT (Longest Processing Time first [Gra69]) pour répartir au mieux les sous-jointures entre les processeurs. Kitsuregawa et al. [Kit90] proposent un algorithme supportant les mauvaises distributions de données et conçu spécifiquement pour la machine SDC (Super Database Computer) basé sur une architecture distribuée. L’idée est de refragmenter chaque paquet de relation en un grand nombre de sous-paquets. Ceux-ci sont alors temporairement distribués sur les processeurs. Un mécanisme matériel, intégré au réseau d’interconnexion (appelé réseau Oméga) est alors utilisé pour redistribuer dynamiquement les sous paquets ainsi formés, de manière à obtenir une bonne répartition de charge. Omiecinski [Omi91] propose une approche similaire pour une architecture à mémoire partagée et utilise l’heuristique first fit decreasing pour redistribuer les sous-paquets aux processeurs. Cependant, cette redistribution ne nécessite pas un mécanisme matériel particulier. Dewitt et al. [DeW92a] suggèrent l’utilisation de plusieurs algorithmes, chacun d’eux étant spécialisés pour un certain degré de skew. Pour cela, une première phase d’échantillonnage des relations est réalisé afin de déterminer le degré de skew et permet de choisir un algorithme plus ou moins insensible au mauvaises distributions. Quatre algorithmes spécifiques sont proposés et implémentés, en plus de l’algorithme de jointure par hachage Hybrid hash join [Sch89]. L’évaluation de performances, menée sur le prototype GAMMA, montrent qu’une telle approche permet d’obtenir un bon support des mauvaises distributions sans surcoût lorsque les relations sont bien distribuées. Shatdal et al. [Sha93] proposent d’utiliser la mémoire virtuellement partagée (SVM), implémentée par logiciel, sur un environnement shared-nothing, afin de réaliser dynamiquement une redistribution de la charge de travail. L’algorithme proposé combine l’utilisation de la mémoire virtuellement partagée et l’envoi de messages afin d’éviter des réplications de données intensives qui saturerait la mémoire de chaque processeur. Lorsqu’un processeur est inactif, il ‘vole’ une partie de la charge de travail à un processeur actif grâce à la mémoire virtuellement partagée. Une simulation est réalisée afin de comparer cette stratégie avec celle développé dans [DeW92a]. L’ajout d’une phase d’échantillonnage préalable des relations permet d’améliorer encore les performances. Plus récemment, Brunie et al. [Bru95] [Bru96] ont proposé de déterminer statiquement l’allocation des processeurs aux opérateurs et de permettre une réallocation dynamique des processeurs en cours d’exécution en cas de mauvaises distributions des données. Pour cela, des opérateurs spécifiques, appelés control operators sont insérés dans le plan d’exécution parallèle afin de déterminer si les estimations statiques ne diffèrent pas trop des valeurs réelles. Si la différence est trop grande, le control operator déclenche une phase de redistribution des données afin de prévenir le skew de redistribution et le skew de produit de jointure. Notons que cet opérateur peut aussi modifier le degré de parallélisme intraopérateur de l’opérateur suivant si besoin est. Il réalise ainsi dynamiquement la répartition de charge intra et inter-opérateur. 5.4.2 Répartition de charge inter-opérateur Afin d’obtenir une bonne répartition de charge au niveau inter-opérateur, il est nécessaire de déterminer, pour chaque opérateur, son degré de parallélisme intra-opérateur et de choisir judicieusement les processeurs alloués pour son exécution. Supposons qu’un modèle de coût nous permette de déterminer, sans erreur, le temps d’exécution séquentiel de chaque opérateur. Il nous faut alors trouver une méthode d’assignation des processeurs aux opérateurs afin d’obtenir la meilleure répartition de charge. Chen et al. [Che93] proposent d’effectuer l’allocation des processeurs en utilisant uniquement le parallélisme intra-opérateur et inter-opérateur de type indépendant. Il n’y a donc pas d’exécution pipeline. Les processeurs sont alors alloués récursivement de la racine aux feuilles de l’arbre d’opérateur de manière à deux sous arbre d’un noeud terminent leur exécution au même moment. Ainsi tous les processeurs sont alloués à la dernière jointure (racine de l’arbre) puis récursivement alloués à chaque sous-arbre en fonction de leur complexité. Hsiao et al. [Hsi94] utilisent la même méthode que précédemment mais l’applique sur ce qu’ils appellent l’arbre d’allocation au lieu de l’appliquer sur l’arbre d’opérateur. L’arbre d’allocation est déduit de l’arbre d’opérateur. Il faut d’abord identifier les chaînes pipelines existantes dans l’arbre d’opérateur. Chaque chaîne pipeline devient alors un noeud de l’arbre d’allocation. Les arcs entre ces noeuds représentent les contraintes de précédence entre les chaînes pipelines. Ainsi, les processeurs sont alloués pour une chaîne pipeline et non plus pour un opérateur. La distribution des processeurs alloués pour une chaîne pipeline sur chaque opérateur est décrite dans [Lo93]. Cette stratégie d’allocation privilégie l’exécution pipeline en essayant de produire toutes les données nécessaires à l’exécution d’une chaîne pipeline de manière synchrone. Wilshut et al. [Wil95] utilisent l’algorithme de jointure par hachage non bloquant pour autoriser l’exécution de tous les opérateurs d’un arbre bushy simultanément (puisque cet algorithme n’entraîne pas de blocages). Ils répartissent les processeurs sur les opérateurs en fonction de leur complexité estimée. Cette technique appelé Full Parallel permet un degré de parallélisme inter-opérateur maximum. Dans cette même étude, les auteurs étudient les performances des stratégies présentées précédemment. Ils montrent que ces approches (y compris la jointure non bloquante) souffrent des problèmes suivant : • Problèmes de partie entière (discretization error) : Comme les processeurs et les opérateurs sont des entités discrètes, les arrondis inévitables entraînent des problèmes de répartition de charge. • Délai dans les pipelines (pipeline delays) : Lors d’une exécution pipeline, les processeurs associés à l’opérateur du haut de la chaîne sont inactifs tant que le premier tuple n’est pas produit. Les délais dans les pipelines entraînent donc aussi une sous utilisation des processeurs. • Erreurs du modèle de coût : Enfin, et c’est sans doute le plus grave, il est clair qu’un modèle de coût ne peut fournir que des résultats très approximatifs puisqu’ils ne prennent pas en compte les aspects dynamiques de l’exécution (interférences, contexte de l’exécution) et peut difficilement évaluer le coût d’un opérateur affecté par le skew. Toutes ces stratégies sont donc dépendantes de la qualité du modèle de coût Metha et al. [Meh95] propose de déterminer dynamiquement (juste avant l’exécution) le degré de parallélisme ainsi que l’ensemble des processeurs utilisés pour chaque opérateur. L’algorithme Rate Match est décrit, il utilise un modèle de coût pour choisir le degré de parallélisme de chaque opérateur. Toutefois, au lieu de se baser sur une estimation de la taille des opérandes, l’algorithme Rate Match tente de faire correspondre le taux de production des tuples résultat d’un opérateur avec le taux de consommation des tuples de l’opérateur suivant. L’aspect multi-utilisateur est pris en compte en réduisant dans le modèle de coût, la vitesse des processeurs d’un facteur dépendant de son utilisation. Enfin, six algorithmes sont proposés pour le choix des processeurs qui exécuteront un opérateur (basés sur la capacité mémoire, l’utilisation des processeurs, des disques, etc..). Rahm et al. [Rah95] proposent d’autres algorithmes pour le choix du degré de parallélisme et des processeurs. Les algorithmes proposés cherchent à répartir efficacement les opérateurs de manière à maximiser l’utilisation des ressources (disques, CPU, mémoire), grâce à des statistiques sur l’utilisation des processeurs et de la mémoire, maintenus par un noeud de contrôle. Les algorithmes proposés sont évalués dans un contexte multi-utilisateurs sur une simulation. Sur des architectures à mémoire partagée, l’utilisation de modèles non fragmentés change la nature du problème de répartition de charge inter-opérateur. En effet, chaque processeur participant à l’ensemble des opérateurs d’une chaine pipeline, il n’y a pas réellement de parallélisme inter-opérateur sauf en considérant deux chaînes pipelines indépendantes. Hong et al. [Hon92] proposent, lors de l’exécution concurrente de plusieurs chaînes pipeline, d’ajuster dynamiquement le degré de parallélisme intra-opérateur de chacune afin d’optimiser l’utilisation des ressources disques et CPU. Pour cela, ils proposent d’exécuter concurremment deux chaînes pipeline, les performances de chacune étant limitées par des facteurs différents (i.e., CPU bound et I/O bound). Le processus de contrôle de l’exécution parallèle détermine alors le point d’utilisation maximum des ressources, (i.e. le degré de parallélisme optimal de chaque chaîne pipeline pour maximiser l’utilisation des ressources) et modifie le degré de parallélisme de chaque chaîne pipeline dynamiquement (en cours d’exécution). 6. Conclusion Nous avons donc, dans cet article présenté un ensemble de notions et techniques relatives à l’exécution parallèle de requêtes : architectures matérielles, formes de parallélisme, plans d’exécution parallèle, ainsi que les métriques permettant de quantifier le gain d’une exécution parallèle. Nous avons alors montré les problèmes liés à l’exécution parallèle et plus particulièrement insisté sur le problème de la répartition de charge lors d’une exécution parallèle. Les différentes solutions existante ont été décrites. Elles se divisent en deux catégories. D’une part, les solutions au problème de la répartition de charge au niveau intraopérateur, généralement dynamiques, cherchent à redistribuer la charge durant l’exécution. Les solutions au niveau inter-opérateur, sont elles, statiques et utilisent des techniques basées sur un modèle de coût. Nous avons alors décris l’approche que nous avons suivi pour réaliser une répartition dynamique de la charge au niveau intra-opérateur et inter-opérateur lors de l’exécution sur des architectures hybrides car elles nous semblent être des architectures clés pour les besoins en performances des nouvelles applications. 7. Bibliographie [Ape92] P. M. G. Apers, C. A. van den Berg, J. Flokstra, P. W. P. J. Grefen, M. L. Kersten, A. N. Wilschut, “PRISMA/DB : A Parallel Main Memory Relational DBMS”. IEEE Transaction on Knowledge and Data Engineering, 4(6), 1992. [Ber92] C. A. van den Berg, M, L, Kersten, “Analysis of a Dynamic Query Optimization Technique for Multi-join Queries”. Int. Conf. on Information and Knowledge Engineering, 1992. [Ber93] B. Bergsten, M. Couprie, P. Valduriez, “Overview of Parallel Architectures for Databases”. Computer Journal, 36(8), 1993. [Bla79] M. Blasgen, J. Gray, M. Mitoma, T. Price, “The Convoy Phenomenon”. Operating Systems Review 13(2), 1979. [Bon96] Peter A. Boncz, Fred Kwakkel, Martin L. Kersten : “High Performance Support for OO Traversals in Monet”. BNCOD, 1996. [Bor82] H. Boral, D. DeWitt, D. Friedland, N. Jarrell, W. Wilkinso, “Implementation of the Database Machine DIRECT”. IEEE Transaction on Software Engineering, 8(6), 1982. [Bor88] H. Boral, “Parallelism and data management”. Int. Conf. on Data and Knowledge Engineering, 1988. [BoS91] P. Borla-Salamet, C. Chachaty, B. Dageville, “Compiling Control into Database Queries for Parallel Execution Management”. Int. Conf. on Parallel and Distributed Information Systems, 1991. [Bou96a] L. Bouganim, D. Florescu, P. Valduriez, “Dynamic Load Balancing in Hierarchical Parallel Database Systems”. Int. Conf. on Very Large Data Bases, 1996. [Bou96b] L. Bouganim, D. Florescu, P. Valduriez : “Parallel Query Execution in NUMA Multiprocessors”. Soumis à la publication. 1996. [Bru95] L. Brunie, H. Kosch, A. Flory, “New static scheduling and elastic load balancing methods for parallel query processing”. BIWIT 95, IEEE Computer Society Press, 1995 [Bru96] L. Brunie, H. Kosch, “Control strategies for complex relational query processing in shared nothing systems”. ACM Sigmod Records, 25(3), 1996. [Cas94] P. Casadessus, B. Dageville, P. Borla-Salamet, “Evaluation du SGBD parallèle DBS3 sur la machine KSR1”. Ingénierie des Systèmes d’Information (ISI), 2(4), 1994. [Cha92] C. Chachaty, P. Borla-Salamet, M. Ward, “A Compositional Approach for the Design of a Parallel Query Processing Language”. Int. Conf. on Parallel Architectures and Language Europe, 1992. [Che93] A. Chen, Y. F. Kao, M. Pong, D. Sak, S. Sharma, J. Vaishnav, H. Zeller, “Query Processing in NonStop SQL”. IEEE Data Engenering Bulletin, 16(4), 1993. [Dag91] B. Dageville, P. Borla-Salamet, C. Chachaty, “Compiling Control Into Database Queries For Parallel Execution Management”. Journées Bases de Données Avancées, BDA’91, 1991. [Dav92] D. D. Davis, “Oracle’s Parallel Punch for OLTP”. Datamation, 1992. [Dan92] W. Davison, “Parallel Index Building in Informix OnLine 6.0”. ACM-SIGMOD Int. Conf., 1992. [DeW86] D. J. DeWitt, R. H. Gerber, G. Graefe, M. L. Heytens, K. B. Kumar, M. Muralikrishna, “GAMMA - A High Performance Dataflow Database Machine”. Int. Conf. on Very Large Data Bases, 1986. [DeW90] D. J. DeWitt, S. Ghandeharizadeh, D. Schneider, A. Bricker, H. Hsiao, R. Rasmussen, “The Gamma Database Machine Project”. IEEE Trans. on Knowledge and Data Engineering, 2(1), 1990. [DeW92a] D.J. DeWitt, J.F. Naughton, D.A. Schneider, S. Seshadri, “Practical Skew Handling in Parallel Joins”. Int. Conf. on Very Large Data Bases, 1992. [DeW92b] D.J. DeWitt, J. Gray, “Parallel Database Systems : The Future of High Performance Database Processing”. Communications of the ACM, 35(6), 1992. [EDS90] EDS Database Group, “EDS - Collaborating for High Performance Parallel Relationnal Database”. Esprit Conf., 1990. [Fon89] M. Fontenot, “Software Congestion, Mobile Servers, and the Hyperbolic Model”. IEEE Transactions on Software Engineering 15, 1989. [Gra92] G. Graefe, S. Thakkar, “Tunning a Parallel Database Algorithm on a Shared-memory Multiprocessor”. Software - Practice and Experience, 22(7), 1992. [Gra94] G. Graefe, “Volcano, An Extensible and Parallel Dataflow Query Evaluation System”. IEEE Trans. on Knowledge and Data Engineering, 6(1), 1994. [Gra69] R. L. Graham, “Bounds on Multiprocessing Timing Anomalies”. SIAM Journal of Applied Mathematics, V17, 1969. [Has94] W. Hasan, R. Motwani, “Optimization Algorithms for Exploiting the Parallel Communication Tradeoff in Pipelined Parallelism”. Int. Conf. on Very Large Data Bases, 1994. [Hon91] W. Hong, M. Stonebraker, “Optimization of Parallel Query Execution Plans in XPRS”. Int. Conf. on Parallel and Distributed Information Systems, 1991. [Hon92] W. Hong, “Exploiting Inter-Operation Parallelism in XPRS”. ACM-SIGMOD Int. Conf., 1992. [Hsi94] H. Hsiao, M. S. Chen, P. S. Yu, “On Parallel Execution of Multiple Pipelined Hash Joins”. ACM-SIGMOD Int. Conf., 1994. [Kit90] M. Kitsuregawa, Y. Ogawa, “Bucket Spreading Parallel Hash : A New, Robust, Parallel Hash Join Method for Data Skew in the Super Database Computer”. Int. Conf. on Very Large Data Bases, 1990. [Lam86] L. Lamport, “The Mutual Exclusion Problem : Part II - Statement and Solutions”. Journal of ACM, 33(2), 1986. [Lan93] R. Lanzelotte, P. Valduriez, M. Zait, “On the Effectiveness of Optimization Search Strategies for Parallel Execution Spaces”. Int. Conf. on Very Large Data Bases, 1993. [Lo93] M-L. Lo, M-S. Chen, C. V. Ravishankar, P. S. Yu, “On Optimal Processor Allocation to Support Pipelined Hash Joins”. ACM-SIGMOD Int. Conf., 1993. [Lop89] M. Lopez, “The EDS Database Server”. EDS Working Report EDS.D901.B, 1989. [Lov88] T. Lovett, S. S. Thakkar, “The Symmetry Multiprocessor System”. Int. Conf. on Parallel Processing, 1988. [Meh95] M. Metha, D. DeWitt, “Managing Intra-operator Parallelism in Parallel Database Systems”. Int. Conf. on Very Large Data Bases, 1995. [Omi91] E. Omiecinski, “Performance Analysis of a Load Balancing Hash-Join Algorithm for a Shared-Memory Multiprocessor”. Int. Conf. on Very Large Data Bases, 1991. [Pat88] D. A. Patterson, G. Gibson and R. H. Katz, “A Case for Redundant Arrays of Inexpensive Disks (RAID)”. ACM-SIGMOD Int. Conf., 1988. [Rah95] E. Rahm, R. Marek, “Dynamic Multi-Resource Load Balancing in Parallel Database Systems”. Int. Conf. on Very Large Data Bases, 1995. [Sch89] D. Schneider, D. DeWitt, “A Performance Evaluation of Four Parallel Join Algorithms in a Shared-Nothing Multiprocessor Environment”. ACM-SIGMOD Int. Conf., 1989. [Sch90] D. Schneider, D. DeWitt, “Tradeoffs in processing complex join queries via hashing in multiprocessor database machines”. Int. Conf. on Very Large Data Bases, 1990. [Sha93] A. Shatdal, J. F. Naughton, “Using Shared Virtual Memory for Parallel Join Processing”. ACM-SIGMOD Int. Conf., 1993. [She93] E. J. Shekita, H. C. Young, “Multi-Join Optimization for Symmetric Multiprocessor”. Int. Conf. on Very Large Data Bases, 1993. [Sto88] M. Stonebraker, R. Katz, D. Patterson & J. Ousterhout, “The design of XPRS”. Int. Conf. on Very Large Data Bases, 1988. [Ter83] Teradata, “DBC/1012 Data Base Computer, Concepts and Facilities”. 1983. [Wal91] C.B. Walton, A.G. Dale, R.M. Jenevin, “A taxonomy and Performance Model of Data Skew Effects in Parallel Joins”. Int. Conf. on Very Large Data Bases, 1991. [Wil92] A. N. Wilschut, J. Flokstra & P. M. G. Apers, “Parallelism in a main-memory system : The performance of PRISMA/DB”. Int. Conf. on Very Large Data Bases, 23-27, 1992. [Wil95] A. N. Wilshut, J. Flokstra, P.G Apers, “Parallel Evaluation of multi-join queries”. ACMSIGMOD Int. Conf., 1995. [Wol93] J. L. Wolf, D. M. Dias, P. S. Yu, J. Turek, “Algorithms for Parallelizing Relational Database Joins in the Presence of Data Skew”. Research Report RC19236. IBM. 1993. [Zia93] M. Ziane, M. Zait, P. Borla-Salamet, “Parallel Query Processing With Zig-Zag Trees”. VLDB Journal, 2(3), 1993.