JOIN crée une nouvelle table en combinant des colonnes d’une ou de plusieurs tables à partir de valeurs communes. Il s’agit d’une opération courante dans les bases de données compatibles SQL, qui correspond à la jointure en algèbre relationnelle. Le cas particulier où une table est jointe à elle-même est souvent appelé « self-join ».
Syntaxe
ON et les colonnes de la clause USING sont appelées « clés de jointure ». Sauf indication contraire, un JOIN produit un produit cartésien à partir des lignes dont les « clés de jointure » correspondent, ce qui peut donner des résultats comportant bien plus de lignes que les tables sources.
Types de JOIN pris en charge
Tous les types standard de SQL JOIN sont pris en charge :JOINsans type spécifié impliqueINNER.- Le mot-clé
OUTERpeut être omis sans risque. - Une syntaxe alternative pour
CROSS JOINconsiste à spécifier plusieurs tables dans la clauseFROM, séparées par des virgules. - S’il n’y a pas de colonnes correspondantes pour un
NATURAL JOIN, il fonctionne comme unCROSS JOIN.
Avec l’analyseur, la désactivation du paramètre
semi_join_include_columns_from_both_sides ou anti_join_include_columns_from_both_sides fait que la jointure correspondante n’expose que son côté préservé aux expressions résolues une fois le résultat de la jointure formé.LEFT SEMI JOINetLEFT ANTI JOINn’exposent que les colonnes du côté gauche.RIGHT SEMI JOINetRIGHT ANTI JOINn’exposent que les colonnes du côté droit.- Cela concerne les clauses telles que
SELECT,PREWHERE,WHERE,GROUP BY,HAVING,QUALIFY,ORDER BYetLIMIT BY, y compris les jokers qualifiés commet1.*. - L’expression
ONdu mêmeJOINpeut toujours faire référence aux deux côtés.
SELECT * développe les colonnes des deux tables.Lorsque join_algorithm est défini sur
partial_merge, RIGHT JOIN et FULL JOIN sont pris en charge uniquement avec la strictness ALL (SEMI, ANTI, ANY et ASOF ne sont pas pris en charge).LATERAL JOIN
JOIN LATERAL permet à la sous-requête située du côté droit d’une jointure de faire référence aux colonnes des expressions de table
situées à sa gauche ; la sous-requête est évaluée pour chaque combinaison distincte des valeurs de colonnes de gauche
auxquelles elle fait référence, et son résultat est joint à chaque ligne de gauche présentant cette combinaison :
ON true est obligatoire, comme pour toute autre jointure INNER ou LEFT JOIN ; l’omettre provoque une erreur de syntaxe.
Cette fonctionnalité est expérimentale et désactivée par défaut ; activez-la avec le paramètre
allow_experimental_lateral_join.
Seul le sous-ensemble suivant est pris en charge pour l’instant ; toute autre forme est rejetée avec une erreur :
- Uniquement
INNER JOIN LATERALetLEFT JOIN LATERAL; les jointuresRIGHT,FULL,PASTEetNATURALne sont pas prises en charge, etLATERALne peut en aucun cas être combiné avec une jointureCROSSou une jointure par virgule. - Uniquement la strictness
ALLpar défaut ;ANY,SEMI,ANTIetASOFne sont pas pris en charge. - Aucun prédicat de jointure autre que
ON true(ON 1est également accepté) ;USINGn’est pas pris en charge, et le prédicat ne peut pas être omis. Placez les filtres qui relient les deux côtés dans la clauseWHEREde la sous-requête latérale. - Les modificateurs de jointure
GLOBALetLOCALne sont pas pris en charge. - La sous-requête latérale doit référencer au moins une colonne du côté gauche. Pour une sous-requête non corrélée, utilisez une jointure classique.
- La sous-requête latérale est évaluée une fois par valeur distincte des colonnes du côté gauche qu’elle référence, et non
une fois par ligne de gauche ; elle ne doit donc pas contenir de fonctions non déterministes au sein d’une requête, telles que
randougenerateUUIDv4, ni de fonctions de table générant des lignes aléatoires, telles quegenerateRandom. Les fonctions constantes au sein d’une requête, telles quenow, sont autorisées. - Seule une sous-requête est prise en charge comme expression de table latérale. Les formes de source de table PostgreSQL
LATERAL unnest(...)etCROSS JOIN UNNEST(...)ne sont pas prises en charge : utilisez plutôt la clauseARRAY JOIN. - Les clauses
GROUP BYetORDER BYde la sous-requête latérale s’exécutent une seule fois sur l’ensemble des évaluations ; les limitesmax_rows_to_group_by,max_rows_to_sortetmax_bytes_to_sortcomptabilisent donc les lignes de toutes les évaluations, et non d’une seule. Elles ne sont prises en charge qu’avec le mode de dépassement de capacitéthrow;anyetbreaksont rejetés. - Les lignes de toutes les évaluations sont mises en correspondance avec les lignes de gauche par une seule jointure. Comme pour toute jointure de hachage,
max_rows_in_joinetmax_bytes_in_joinlimitent le côté de cette jointure conservé en mémoire, et c’est le planificateur qui choisit ce côté : avec les paramètres par défaut (correlated_subqueries_use_in_memory_buffer = 1), il s’agit du côté gauche deJOIN LATERAL, car les lignes de gauche doivent être entièrement lues avant l’évaluation de la sous-requête latérale ; sinon, il peut s’agir des résultats de l’ensemble des évaluations. Les limites sont toujours appliquées comme sijoin_overflow_modevalaitthrow: avecbreak, la jointure supprimerait silencieusement des lignes de gauche sans rapport.
Paramètres
Le type de jointure par défaut peut être remplacé à l’aide du paramètrejoin_default_strictness.
Le comportement du serveur ClickHouse pour les opérations ANY JOIN dépend du paramètre any_join_distinct_right_table_keys.
Voir aussi
join_algorithmjoin_any_take_last_rowjoin_use_nullspartial_merge_join_rows_in_right_blocksjoin_on_disk_max_files_to_mergeany_join_distinct_right_table_keys
cross_to_inner_join_rewrite pour définir le comportement lorsque ClickHouse ne parvient pas à réécrire un CROSS JOIN en INNER JOIN. La valeur par défaut est 1, ce qui permet à la jointure de se poursuivre, mais plus lentement. Définissez cross_to_inner_join_rewrite sur 0 si vous voulez qu’une erreur soit générée, et sur 2 pour ne pas exécuter les jointures CROSS, mais forcer à la place la réécriture de toutes les jointures par virgule/CROSS. Si la réécriture échoue lorsque la valeur est 2, vous recevrez un message d’erreur indiquant “Please, try to simplify WHERE section”.
Conditions de la section ON
Une sectionON peut contenir plusieurs conditions combinées à l’aide des opérateurs AND et OR. Les conditions qui spécifient des clés de jointure doivent :
- faire référence à la fois aux tables de gauche et de droite
- utiliser l’opérateur d’égalité
JOIN. Notez que si les mêmes conditions sont placées dans une section WHERE et qu’elles ne sont pas remplies, les lignes sont alors toujours exclues du résultat.
L’opérateur OR à l’intérieur de la clause ON fonctionne à l’aide de l’algorithme de jointure de hachage : pour chaque argument OR avec des clés de jointure pour JOIN, une table de hachage distincte est créée, de sorte que la consommation de mémoire et le temps d’exécution de la requête augmentent linéairement avec le nombre d’expressions OR dans la clause ON.
Seul l’opérateur d’égalité (
=) fait d’une condition une clé de jointure. Les autres opérateurs entre des colonnes de tables différentes sont pris en charge, mais sans aucune égalité, la jointure doit examiner chaque paire de lignes, ce qui est beaucoup plus lent ; voir JOIN avec une condition ON arbitraire.table_1 et table_2 :
table_2 :
Query
C ainsi que la colonne de texte vide. Elle est incluse dans le résultat, car une jointure de type OUTER est utilisée.
Response
INNER et plusieurs conditions :
Query
Response
INNER et une condition avec OR :
Query
Response
INNER et des conditions avec OR et AND :
Une condition d’inégalité qui utilise des colonnes d’une seule table, comme
t1.a = t2.key AND t1.b > 0 AND t2.b > t2.c, s’applique uniquement à cette table : t1.b > 0 n’utilise que des colonnes de t1 et t2.b > t2.c n’utilise que des colonnes de t2.
Une condition d’inégalité qui compare des colonnes de tables différentes, comme t1.a = t2.key AND t1.b > t2.key, est également prise en charge ; consultez les sections ci-dessous pour plus de détails.Query
Response
JOIN avec des conditions d’inégalité sur des colonnes de tables différentes
ClickHouse prend en chargeALL/ANY/SEMI/ANTI INNER/LEFT/RIGHT/FULL JOIN avec des conditions d’inégalité, en plus des conditions d’égalité.
Lorsque la section ON contient, en plus de l’inégalité, une égalité entre les deux tables, cette égalité sert de clé de jointure et l’inégalité est vérifiée sur les lignes qu’elle a fait correspondre. Une telle condition mixte n’est exécutée que par les algorithmes de jointure hash, parallel_hash et grace_hash.
Lorsque la section ON ne contient aucune égalité entre les deux tables, il n’existe aucune clé de jointure sur laquelle effectuer la correspondance. Une telle jointure est exécutée par ie_join si la condition est une paire d’inégalités, et sinon sous forme de jointure par boucles imbriquées par blocs.
Une condition évaluée lors de la jointure ne peut pas contenir arrayJoin, car elle doit préserver le nombre de lignes ; utilisez plutôt ARRAY JOIN dans une sous-requête.
Exemple
Table t1:
t2
JOIN avec une condition ON arbitraire
La sectionON peut contenir une expression booléenne arbitraire portant sur les colonnes des deux tables, par exemple une vérification de plage, une comparaison arithmétique ou un appel à une fonction scalaire. Lorsqu’elle ne contient aucune égalité entre les deux tables, il n’existe aucune clé de jointure sur laquelle faire correspondre les lignes. Une paire de conditions d’inégalité est exécutée par ie_join lorsque cet algorithme figure dans join_algorithm, ce qui est le cas par défaut. Toute autre condition est exécutée sous la forme d’une jointure par boucles imbriquées par blocs : la table de droite est matérialisée, puis la condition est évaluée sur chaque paire de lignes. Un ALL INNER JOIN prend alors la forme équivalente d’un CROSS JOIN dont la condition sert de filtre.
La jointure par boucles imbriquées par blocs prend en charge tous les types de JOIN et toutes les strictness, à l’exception de ASOF JOIN, PASTE JOIN et ANY FULL JOIN. Elle ne fait pas partie des valeurs de join_algorithm : il s’agit d’une solution de dernier recours, utilisée uniquement lorsqu’aucun autre algorithme ne peut exécuter la condition. Dans la sortie de EXPLAIN, elle apparaît sous la forme d’une étape BlockNestedLoopJoin. Lorsque allow_block_nested_loop_join est désactivé, toute requête qui en aurait besoin est rejetée avec l’erreur INVALID_JOIN_ON_EXPRESSION.
Exemple
Query
Response
join_use_nulls, comme pour toute autre jointure : avec une valeur par défaut dans l’exemple ci-dessus, et avec NULL lorsque le paramètre est activé.
Strictness
ANY et SEMI conservent normalement une ligne par groupe de lignes partageant une clé de jointure. Comme un tel groupe n’existe pas ici, ils conservent une ligne par ligne de la table qui pilote la jointure : LEFT ANY et LEFT SEMI émettent chaque ligne de gauche au plus une fois, RIGHT ANY et RIGHT SEMI chaque ligne de droite au plus une fois. ANY INNER limite les deux côtés à la fois — chaque ligne de l’une ou l’autre table est utilisée au plus une fois, de sorte que le résultat contient au plus autant de lignes que la plus petite des deux tables. Les paires qui composent un tel résultat sont arbitraires, comme le sous-entend précisément ANY, et peuvent varier d’une exécution à l’autre de la même requête. Pour ANY INNER, cela vaut aussi pour le nombre de lignes : apparier de manière gloutonne chaque ligne des deux côtés, quel que soit l’ordre dans lequel les lignes sont examinées, peut laisser un nombre variable de lignes non appariées. Le résultat de count() sur une jointure ANY INNER sans clé de jointure n’est donc pas reproductible et dépend du nombre de threads ainsi que de l’ordre physique de la table de droite.
Performances
Chaque paire de lignes est examinée : le travail croît donc avec le produit des nombres de lignes des deux tables, et non avec leur somme. Une jointure par boucles imbriquées par blocs est par conséquent plus coûteuse de plusieurs ordres de grandeur qu’une jointure de hachage sur les mêmes données, et l’écart se creuse à mesure que les tables grossissent. Si une requête peut être écrite avec au moins une égalité dans sa section ON, privilégiez cette forme.
Les jointures LEFT ANY, ANY INNER, LEFT SEMI et LEFT ANTI interrompent le parcours de la table de droite dès la première ligne correspondant à une ligne de gauche, ce qui les rend généralement moins coûteuses que la jointure ALL correspondante. Leurs équivalents pilotés par la droite (RIGHT ANY, RIGHT SEMI, RIGHT ANTI) examinent chaque paire, car le résultat dépend des lignes de droite qui ont trouvé une correspondance. join_any_take_last_row n’a aucun effet ici : sans clé de jointure, il n’existe aucun groupe de lignes correspondantes dont on pourrait retenir la dernière.
La consommation mémoire est limitée par la table de droite matérialisée et n’augmente pas avec la taille du résultat. La table de droite est soumise aux mêmes paramètres que pour les autres algorithmes de jointure, répertoriés dans la section Limitations de mémoire : max_rows_in_join, max_bytes_in_join et join_overflow_mode en limitent la taille, et max_bytes_before_external_join déclenche son déversement sur disque.
Valeurs NULL et NaN dans les clés de jointure
NULL n’est égal à aucune valeur, pas même à lui-même. Cela signifie que si une clé de JOIN contient une valeur NULL dans une table, elle ne correspondra pas à une valeur NULL dans l’autre table.
Exemple
Table A :
B :
Charlie de la table A et la ligne avec le score 88 de la table B n’apparaissent pas dans le résultat en raison de la valeur NULL dans la clé de JOIN.
Si vous souhaitez faire correspondre des valeurs NULL, utilisez la fonction isNotDistinctFrom pour comparer les clés de JOIN.
NaN dans les clés de JOIN de type flottant ne suivent pas la règle relative à NULL ci-dessus.
La comparaison scalaire de deux valeurs NaN (NaN = NaN) renvoie 0 ; toutefois, les clés de JOIN ne sont pas comparées selon une sémantique scalaire : des clés NaN peuvent correspondre.
Le fait qu’elles correspondent réellement est un détail d’implémentation qui dépend de l’algorithme de jointure, du type de clé et des paramètres de session.
Ne vous fiez pas à un comportement particulier.
Si vous voulez vous assurer que les lignes contenant NaN ne correspondent pas, mappez-les à des valeurs NULL : ON if(isNaN(A.id), NULL, A.id) = B.id.
Dans un ASOF JOIN, la colonne de correspondance la plus proche est comparée selon l’ordre, ce que NaN ne permet pas ; filtrez ces lignes des deux côtés.
Utilisation de ASOF JOIN
ASOF JOIN est utile lorsque vous devez joindre des enregistrements qui n’ont pas de correspondance exacte.
Cet algorithme de JOIN nécessite une colonne spéciale dans les tables. Cette colonne :
- Doit contenir une séquence ordonnée.
- Peut être de l’un des types suivants : Int, UInt, Float, Date, DateTime, Decimal.
- Pour l’algorithme de JOIN
hash, elle ne peut pas être la seule colonne de la clauseJOIN.
ASOF JOIN ... ON :
SELECT count() FROM table_1 ASOF LEFT JOIN table_2 ON table_1.a == table_2.b AND table_2.t <= table_1.t.
Conditions prises en charge pour la correspondance la plus proche : >, >=, <, <=.
Syntaxe ASOF JOIN ... USING :
ASOF JOIN utilise equi_columnX pour les correspondances exactes et asof_column pour la correspondance la plus proche avec la condition table_1.asof_column >= table_2.asof_column. La colonne asof_column est toujours la dernière de la clause USING.
Par exemple, considérons les tables suivantes :
ASOF JOIN peut prendre le timestamp d’un événement utilisateur de table_1 et trouver un événement dans table_2 dont le timestamp est le plus proche de celui de l’événement de table_1, selon la condition de correspondance la plus proche. Des valeurs de timestamp identiques sont considérées comme les plus proches lorsqu’elles sont disponibles. Ici, la colonne user_id peut être utilisée pour une jointure par égalité, et la colonne ev_time pour une jointure sur la correspondance la plus proche. Dans notre exemple, event_1_1 peut être joint à event_2_1 et event_1_2 à event_2_3, mais event_2_2 ne peut pas être joint.
ASOF JOIN est pris en charge uniquement par les algorithmes de jointure hash et full_sorting_merge.
Il n’est pas pris en charge par le moteur de table Join.Utilisation de PASTE JOIN
Le résultat dePASTE JOIN est une table qui contient toutes les colonnes de la sous-requête de gauche, suivies de toutes les colonnes de la sous-requête de droite.
Les lignes sont appariées en fonction de leur position dans les tables d’origine (l’ordre des lignes doit être défini).
Si les sous-requêtes renvoient un nombre différent de lignes, les lignes supplémentaires seront tronquées.
Exemple :
JOIN distribué
Il existe deux façons d’exécuter un JOIN impliquant des tables distribuées :- Lorsqu’on utilise un
JOINnormal, la requête est envoyée aux serveurs distants. Les sous-requêtes sont exécutées sur chacun d’eux afin de constituer la table de droite, puis le JOIN est effectué avec cette table. Autrement dit, la table de droite est constituée séparément sur chaque serveur. - Lorsqu’on utilise
GLOBAL ... JOIN, le serveur à l’origine de la requête exécute d’abord une sous-requête pour calculer l’un des côtés du JOIN et rassemble le résultat dans une table temporaire. Cette table temporaire est ensuite transmise à chaque serveur distant, et les requêtes y sont exécutées à l’aide des données temporaires transmises. Pour les JOINLEFTetINNER, la table de droite est calculée par la sous-requête. Pour les JOINRIGHT, c’est la table de gauche qui est calculée, puisque la table de droite est celle qui est conservée et doit être lue depuis les shards.
GLOBAL. Pour plus d’informations, consultez la section Sous-requêtes distribuées.
Conversion implicite de type
Les requêtesINNER JOIN, LEFT JOIN, RIGHT JOIN et FULL JOIN prennent en charge la conversion implicite de type pour les clés de jointure. Toutefois, la requête ne peut pas être exécutée si les clés de jointure des tables de gauche et de droite ne peuvent pas être converties vers un même type (par exemple, il n’existe aucun type de données pouvant contenir toutes les valeurs de UInt64 et Int64, ou de String et Int32).
Exemple
Considérez la table t_1 :
t_2 :
Conseils d’utilisation
Traitement des cellules vides ou NULL
Lors d’une jointure entre tables, des cellules vides peuvent apparaître. Le paramètre join_use_nulls définit comment ClickHouse remplit ces cellules. Si les clés deJOIN sont des champs Nullable, les lignes pour lesquelles au moins une des clés a la valeur NULL ne sont pas prises en compte dans la jointure.
Syntaxe
Les colonnes spécifiées dansUSING doivent avoir les mêmes noms dans les deux sous-requêtes, et les autres colonnes doivent porter des noms différents. Vous pouvez utiliser des alias pour renommer les colonnes dans les sous-requêtes.
La clause USING spécifie une ou plusieurs colonnes de jointure, ce qui établit l’égalité entre ces colonnes. La liste des colonnes s’écrit sans parenthèses. Les conditions de jointure plus complexes ne sont pas prises en charge.
Limites de la syntaxe
Pour plusieurs clausesJOIN dans une même requête SELECT :
- La sélection de toutes les colonnes via
*n’est possible que lorsque des tables sont jointes, et non des sous-requêtes. - La clause
PREWHEREn’est pas disponible. - La clause
USINGn’est pas disponible.
ON, WHERE et GROUP BY :
- Les expressions arbitraires ne peuvent pas être utilisées dans les clauses
ON,WHEREetGROUP BY, mais vous pouvez définir une expression dans une clauseSELECT, puis l’utiliser dans ces clauses via un alias.
Performance
Lors de l’exécution d’unJOIN, l’ordre d’exécution n’est pas optimisé par rapport aux autres étapes de la requête. La jointure (recherche dans la table de droite) est exécutée avant le filtrage dans WHERE et avant l’agrégation.
Chaque fois qu’une requête est exécutée avec le même JOIN, la sous-requête est relancée, car le résultat n’est pas mis en cache. Pour éviter cela, utilisez le moteur de table spécial Join, qui est une structure préparée pour les jointures et qui réside toujours en RAM.
Dans certains cas, il est plus efficace d’utiliser IN plutôt que JOIN.
Si vous avez besoin d’un JOIN avec des tables de dimension (des tables relativement petites qui contiennent des propriétés de dimension, comme des noms de campagnes publicitaires), un JOIN peut ne pas être très pratique, car la table de droite est de nouveau consultée pour chaque requête. Dans ce type de cas, il existe la fonctionnalité « dictionnaires », à utiliser à la place de JOIN. Pour plus d’informations, consultez la section Dictionnaires.
Limitations de mémoire
Par défaut, ClickHouse utilise l’algorithme de jointure par hachage. ClickHouse prend la table de droite et crée une table de hachage en RAM pour celle-ci. Sijoin_algorithm = 'auto' est activé, alors au-delà d’un certain seuil de consommation de mémoire, ClickHouse bascule vers l’algorithme de jointure par fusion. Pour une description des algorithmes de jointure, consultez le paramètre join_algorithm.
Si vous devez limiter la consommation de mémoire de l’opération JOIN, utilisez les paramètres suivants :
- max_rows_in_join — Limite le nombre de lignes dans la table de hachage.
- max_bytes_in_join — Limite la taille de la table de hachage.
enable_adaptive_memory_spill_scheduler peut malgré tout déverser une jointure rendue apte au déversement par le seuil décrit ci-dessous, et
legacy_join_size_limits_trigger_spilling retransforme ces deux plafonds en déclencheurs de déversement sur disque.
Pour permettre à une jointure de poursuivre son exécution en déversant le côté droit sur disque plutôt que d’échouer, utilisez :
- max_bytes_before_external_join — Seuil de déversement absolu.
- max_bytes_ratio_before_external_join — Seuil de déversement exprimé en ratio de la mémoire disponible.
grace_hash ; en cas de pression mémoire, enable_adaptive_memory_spill_scheduler peut déverser plus tôt qu’ils ne le demandent — mais uniquement si l’un d’eux est non nul, car une jointure dépourvue de tout seuil ne déverse jamais. L’algorithme join_algorithm que vous choisissez détermine la manière dont une jointure déverse — grace_hash partitionne la table de droite dès le premier bloc, tandis que hash et parallel_hash la collectent en mémoire puis basculent lorsque le seuil est franchi — et non si ces paramètres s’appliquent. La seule exception est legacy_join_size_limits_trigger_spilling : lorsqu’il est activé, grace_hash utilisé seul ignore les deux seuils et déverse en fonction des deux plafonds stricts.
Exemples
Exemple :Contenu connexe
- Blog : ClickHouse : un SGBD ultra-rapide avec une prise en charge complète des jointures SQL - Partie 1
- Blog : ClickHouse : un SGBD ultra-rapide avec une prise en charge complète des jointures SQL - Dans les coulisses - Partie 2
- Blog : ClickHouse : un SGBD ultra-rapide avec une prise en charge complète des jointures SQL - Dans les coulisses - Partie 3
- Blog : ClickHouse : un SGBD ultra-rapide avec une prise en charge complète des jointures SQL - Dans les coulisses - Partie 4