web-dev-qa-db-fra.com

Filtre Spark DataFrame basé sur un autre DataFrame qui spécifie les critères de liste noire

J'ai un largeDataFrame (plusieurs colonnes et des milliards de lignes) et un smallDataFrame (une seule colonne et 10 000 lignes).

Je voudrais filtrer toutes les lignes de la largeDataFrame chaque fois que la colonne some_identifier Dans la largeDataFrame correspond à l'une des lignes de la smallDataFrame.

Voici un exemple:

largeDataFrame

some_idenfitier,first_name
111,bob
123,phil
222,mary
456,sue

smallDataFrame

some_identifier
123
456

sortie désirée

111,bob
222,mary

Voici ma vilaine solution.

val smallDataFrame2 = smallDataFrame.withColumn("is_bad", lit("bad_row"))
val desiredOutput = largeDataFrame.join(broadcast(smallDataFrame2), Seq("some_identifier"), "left").filter($"is_bad".isNull).drop("is_bad")

Existe-t-il une solution plus propre?

22
Powers

Vous devrez utiliser un left_anti joindre dans ce cas.

Le anti-jointure gauche est l'opposé d'un semi-jointure gauche.

Il filtre les données de la table de droite dans la table de gauche selon une clé donnée:

largeDataFrame
   .join(smallDataFrame, Seq("some_identifier"),"left_anti")
   .show
// +---------------+----------+
// |some_identifier|first_name|
// +---------------+----------+
// |            222|      mary|
// |            111|       bob|
// +---------------+----------+
55
eliasah

Une version en pur Spark SQL (et en utilisant PySpark comme exemple, mais avec de petites modifications identiques est applicable pour Scala API)):

def string_to_dataframe (df_name, csv_string):
    rdd = spark.sparkContext.parallelize(csv_string.split("\n"))
    df = spark.read.option('header', 'true').option('inferSchema','true').csv(rdd)
    df.registerTempTable(df_name)

string_to_dataframe("largeDataFrame", '''some_identifier,first_name
111,bob
123,phil
222,mary
456,sue''')

string_to_dataframe("smallDataFrame", '''some_identifier
123
456
''')

anti_join_df = spark.sql("""
    select * 
    from largeDataFrame L
    where NOT EXISTS (
            select 1 from smallDataFrame S
            WHERE L.some_identifier = S.some_identifier
        )
""")

print(anti_join_df.take(10))

anti_join_df.explain()

sortira comme prévu mary et bob:

[Row (some_identifier = 222, first_name = 'mary'),
Ligne (some_identifier = 111, first_name = 'bob')]

et aussi Plan d'exécution physique montrera qu'il utilise

== Physical Plan ==
SortMergeJoin [some_identifier#252], [some_identifier#264], LeftAnti
:- *(1) Sort [some_identifier#252 ASC NULLS FIRST], false, 0
:  +- Exchange hashpartitioning(some_identifier#252, 200)
:     +- Scan ExistingRDD[some_identifier#252,first_name#253]
+- *(3) Sort [some_identifier#264 ASC NULLS FIRST], false, 0
   +- Exchange hashpartitioning(some_identifier#264, 200)
      +- *(2) Project [some_identifier#264]
         +- Scan ExistingRDD[some_identifier#264]

Remarquer Sort Merge Join est plus efficace pour joindre/anti-joindre des ensembles de données qui sont approximativement de la même taille. Puisque vous avez indiqué que la petite trame de données est plus petite, nous devons nous assurer que Spark optimizer choisit Broadcast Hash Join qui sera beaucoup plus efficace dans ce scénario:

Je changerai NOT EXISTS à NOT IN clause pour cela:

anti_join_df = spark.sql("""
    select * 
    from largeDataFrame L
    where L.some_identifier NOT IN (
            select S.some_identifier
            from smallDataFrame S
        )
""")

anti_join_df.explain()

Voyons ce que cela nous a donné:

== Physical Plan ==
BroadcastNestedLoopJoin BuildRight, LeftAnti, ((some_identifier#302 = some_identifier#314) || isnull((some_identifier#302 = some_identifier#314)))
:- Scan ExistingRDD[some_identifier#302,first_name#303]
+- BroadcastExchange IdentityBroadcastMode
   +- Scan ExistingRDD[some_identifier#314]

Notez que Spark Optimizer a en fait choisi Broadcast Nested Loop Join et pas Broadcast Hash Join. Le premier est correct puisque nous n'avons que deux enregistrements à exclure du côté gauche.

Notez également que les deux plans d'exécution ont LeftAnti, c'est donc similaire à la réponse @eliasah, mais il est implémenté à l'aide de SQL pur. De plus, cela montre que vous pouvez avoir plus de contrôle sur le plan d'exécution physique.

PS. Gardez également à l'esprit que si la trame de données de droite est beaucoup plus petite que la trame de données de gauche mais est plus grande que quelques enregistrements, vous voulez avoir Broadcast Hash Join et pas Broadcast Nested Loop Join ni Sort Merge Join. Si cela ne se produit pas, vous devrez peut-être régler spark.sql.autoBroadcastJoinThreshold car il est par défaut de 10 Mo, mais il doit être plus grand que la taille du "smallDataFrame".

3
Tagar