使用Spark SQL,我有两个数据帧,它们是从一个创建的,例如:
df = sqlContext.createDataFrame(...);
df1 = df.filter("value = 'abc'"); //[path, value]
df2 = df.filter("value = 'qwe'"); //[path, value]
我想过滤df1,如果它的一部分路径是'是df2中的任何路径。 因此,如果df1的行与路径' a / b / c / d / e'我会发现在df2中是否有一行路径是' a / b / c'。 在SQL中它应该像
SELECT * FROM df1 WHERE udf(path) IN (SELECT path FROM df2)
其中udf是用户定义的函数,用于缩短df1的原始路径。 天真的解决方案是使用JOIN然后过滤结果,但它很慢,因为df1和df2每行都超过10mil。
我也尝试过以下代码,但首先我必须从df2
创建广播变量static Broadcast<DataFrame> bdf;
bdf = sc.broadcast(df2); //variable 'sc' is JavaSparkContext
sqlContext.createDataFrame(df1.javaRDD().filter(
new Function<Row, Boolean>(){
@Override
public Boolean call(Row row) throws Exception {
String foo = shortenPath(row.getString(0));
return bdf.value().filter("path = '"+foo+"'").count()>0;
}
}
), myClass.class)
我遇到的问题是,在评估返回时/当执行df2过滤时,Spark卡住了。
我想知道如何使用两个数据帧来执行此操作。 我真的想避免加入。有什么想法吗?
编辑&gt;&GT;
在我原来的代码中,df1有别名&#39; first&#39;和df2&#39; second&#39;。此连接不是笛卡儿,也不使用广播。
df1 = df1.as("first");
df2 = df2.as("second");
df1.join(df2, df1.col("first.path").
lt(df2.col("second.path"))
, "left_outer").
filter("isPrefix(first.path, second.path)").
na().drop("any");
isPrefix是udf
UDF2 isPrefix = new UDF2<String, String, Boolean>() {
@Override
public Boolean call(String p, String s) throws Exception {
//return true if (p.length()+4==s.length()) and s.contains(p)
}};
shortenPath - 它会剪切路径中的最后两个字符
UDF1 shortenPath = new UDF1<String, String>() {
@Override
public String call(String s) throws Exception {
String[] foo = s.split("/");
String result = "";
for (int i = 0; i < foo.length-2; i++) {
result += foo[i];
if(i<foo.length-3) result+="/";
}
return result;
}
};
记录示例。路径是独一无二的。
a/a/a/b/c abc
a/a/a qwe
a/b/c/d/e abc
a/b/c qwe
a/b/b/k foo
a/b/f/a bar
...
所以df1属于
a/a/a/b/c abc
a/b/c/d/e abc
...
和df2属于
a/a/a qwe
a/b/c qwe
...
答案 0 :(得分:1)
您的代码至少存在一些问题:
DataFrame
根本无法正常工作,您应该得到例外。join
是作为笛卡尔积生成的,后跟过滤器。由于Spark使用Hashing
进行连接,因此只有基于相等的连接可以在没有笛卡儿的情况下有效地执行。它与Why using a UDF in a SQL query leads to cartesian product? DataFrames
都相对较大并且具有相似的大小,则广播不太可能有用。请参阅Why my BroadcastHashJoin is slower than ShuffledHashJoin in Spark isPrefix
似乎错了。特别是它看起来可以匹配前缀和后缀col("first.path").lt(col("second.path"))
条件看错了。我假设您希望来自a/a/a/b/c
的{{1}}匹配df1
a/a/a
。如果是,则df2
不是gt
。你能做的最好的事情可能与此类似:
lt
您可以尝试广播其中一个表格(仅限Spark&gt; = 1.5.0):
import org.apache.spark.sql.functions.{col, regexp_extract}
val df = sc.parallelize(Seq(
("a/a/a/b/c", "abc"), ("a/a/a","qwe"),
("a/b/c/d/e", "abc"), ("a/b/c", "qwe"),
("a/b/b/k", "foo"), ("a/b/f/a", "bar")
)).toDF("path", "value")
val df1 = df
.where(col("value") === "abc")
.withColumn("path_short", regexp_extract(col("path"), "^(.*)(/.){2}$", 1))
.as("df1")
val df2 = df.where(col("value") === "qwe").as("df2")
val joined = df1.join(df2, col("df1.path_short") === col("df2.path"))
并增加自动广播限制,但正如我上面提到的,它很可能效率低于普通import org.apache.spark.sql.functions.broadcast
df1.join(broadcast(df2), col("df1.path_short") === col("df2.path"))
。
答案 1 :(得分:1)
作为使用子查询实现IN
的一种可能方式,可以使用LEFT SEMI JOIN
:
JavaSparkContext javaSparkContext = new JavaSparkContext("local", "testApp");
SQLContext sqlContext = new SQLContext(javaSparkContext);
StructType schema = DataTypes.createStructType(new StructField[]{
DataTypes.createStructField("path", DataTypes.StringType, false),
DataTypes.createStructField("value", DataTypes.StringType, false)
});
// Prepare First DataFrame
List<Row> dataForFirstDF = new ArrayList<>();
dataForFirstDF.add(RowFactory.create("a/a/a/b/c", "abc"));
dataForFirstDF.add(RowFactory.create("a/b/c/d/e", "abc"));
dataForFirstDF.add(RowFactory.create("x/y/z", "xyz"));
DataFrame df1 = sqlContext.createDataFrame(javaSparkContext.parallelize(dataForFirstDF), schema);
//
df1.show();
//
// +---------+-----+
// | path|value|
// +---------+-----+
// |a/a/a/b/c| abc|
// |a/b/c/d/e| abc|
// | x/y/z| xyz|
// +---------+-----+
// Prepare Second DataFrame
List<Row> dataForSecondDF = new ArrayList<>();
dataForSecondDF.add(RowFactory.create("a/a/a", "qwe"));
dataForSecondDF.add(RowFactory.create("a/b/c", "qwe"));
DataFrame df2 = sqlContext.createDataFrame(javaSparkContext.parallelize(dataForSecondDF), schema);
// Use left semi join to filter out df1 based on path in df2
Column pathContains = functions.column("firstDF.path").contains(functions.column("secondDF.path"));
DataFrame result = df1.as("firstDF").join(df2.as("secondDF"), pathContains, "leftsemi");
//
result.show();
//
// +---------+-----+
// | path|value|
// +---------+-----+
// |a/a/a/b/c| abc|
// |a/b/c/d/e| abc|
// +---------+-----+
此类查询的物理计划如下所示:
== Physical Plan ==
Limit 21
ConvertToSafe
LeftSemiJoinBNL Some(Contains(path#0, path#2))
ConvertToUnsafe
Scan PhysicalRDD[path#0,value#1]
TungstenProject [path#2]
Scan PhysicalRDD[path#2,value#3]
它将使用LeftSemiJoinBNL进行实际的连接操作,该操作应在内部广播值。从更多细节可以参考Spark中的实际实现 - LeftSemiJoinBNL.scala
P.S。我并不完全理解删除最后两个字符的必要性,但是如果需要的话 - 可以这样做,就像@ zero323提议的那样(使用regexp_extract
)。