在分区器和索引部分铺垫了很多,其实Simba中Spatial join算子的部分是真正利用前面的结构来有效降低计算量的逻辑,也是simba最大的亮点。simba主要实现了三类spatial join算子:
KNN query:select * from table IN KNN ($target) within ($k);
Distance join:SELECT * FROM R JOIN S ON (R.x - S.x) * (R.x - S.x) + (R.y - S.y) * (R.y - S.y)
<= 10.0 * 10.0
KNN join:SELECT * FROM point1 AS p1 KNN JOIN point2 AS p2 ON POINT(p2.x, p2.y) IN KNN(POINT(p1.x, p1.y), 10).
出于篇幅原因,本章会对每类算子选择一个算子具体讲述代码和算法原理,对于其余算子仅会分析代码实现,具体的算法原理可以参考simba的论文。在本章的介绍中,会把参数join的两部分数据分别用leftRDD和rightRDD表示。
KNN Query
Simba基于two-level index对KNN Query进行了特别的优化,值得注意的是,不同的索引对于KNN Query的优化方式有所不同,作者所实现的效果最好的KNN算子需要配合提前建立RTree索引。另外KNN query算子并没有作为单独一个类出现,KNN query部分代码放在了org.apache.spark.sql.simba.index.IndexedRelationScan类的doExceute()函数的RTreeIndexedRelation优化逻辑的部分,在由physicalPlan生成RDD的时候执行。KNN Query算子的主要逻辑如下:
1)首先会根据global index未给定点找到最近的几个partition,然后调用knnGlobalPrune方法粗略找到第一个KNN候选集。在knnGlobalPrune方法中会首先调用PartitionPruningRDD,基于给定的paritionID裁剪不满足要求的partition;然后在裁剪后的每个分区内部调用local inedx的KNN方法找到最近的k个点,再把每个裁剪分区的k个点进行归并,去距离最近的k个,生成候选集tmp_ans,并计算候选集与点之间的距离theta作为第二次的裁剪;
def knnGlobalPrune(global_part: Set[Int]): Array[InternalRow] = {
val pruned = new PartitionPruningRDD(rtree._indexedRDD, global_part.contains)
pruned.flatMap{ packed =>
var tmp_ans = Array[(Shape, Int)]()
if (packed.index.asInstanceOf[RTree] != null) {
tmp_ans = packed.index.asInstanceOf[RTree]
.kNN(query_point, k, keepSame = false)
}
tmp_ans.map(x => packed.data(x._2))
}.takeOrdered(k)(ord)
}
// first prune, get k partitions, but partitions may not be final partitions
val global_part1 = rtree.global_rtree.kNN(query_point, k, keepSame = false).map(_._2).toSet
val tmp_ans = knnGlobalPrune(global_part1) // to get a safe and tighter bound
val theta = evalDist(tmp_ans.last, query_point, column_keys, rtree.isPoint)
采用上面这样的逻辑的原因是:首先选择与目标点最近的、含有至少k个点的partition,以这些parition到目标点的最远距离theta作为裁剪依据。到目标点的距离大于theta的分区内的点一定不属于到目标点最近的k个点。
上图中每个矩形代表一个数据分区,步骤1)就是找到深灰色的部分,并计算红色圆形的半径,深灰色内至少含有k点,所以到目标点最近的k个点一定全部位于红色圆形内的部分,也就一定位于浅灰色的分区内,步骤2)就是找到浅灰色分区的部分。
2)计算到半径theta之后,simba会调用global index的circleRange方法找到半径为theta圆相交的partitionID,进行裁剪得到第二个候选集tmp_knn_res;第3行部分的逻辑可以看做一个懒加载,如果两部分重合就不再计算,不重合说明数据可能位于红色圈内、深灰色矩形之外,需要继续计算。
val global_part2 = rtree.global_rtree.circleRange(query_point, theta).
map(_._2).toSet -- global_part1
val tmp_knn_res = if (global_part2.isEmpty) tmp_ans
else knnGlobalPrune(global_part2).union(tmp_ans).sorted(ord).take(k)
3)在裁剪失败(候选集为空)或者上一步的结果大于给定阈值的场景下,会尝试利用global index的range方法计算所有与给定点的MBR相交的部分进行计算。
var global_part = rtree.global_rtree.range(queryMBR).map(_._2).toSeq
if (cir_ranges.nonEmpty){ // circle range
global_part = global_part.intersect(
rtree.global_rtree.circleRangeConj(cir_ranges).map(_._2)
)
}
val pruned = new PartitionPruningRDD(rtree._indexedRDD, global_part.contains)
Distance Join
Distance Join部分代码位于org.apache.spark.sql.simba.execution.join包中,实现了DJSpark、CDJSpark、RDJSpark、BDJSpark四类Distance Join算子。
DJSpark
DJSpark的思路与SpatialHadoop的思路一致:1)分别对参与join的leftRDD和rightRDD按key值进行分桶;2)对各个分桶内的数据做两两组合,每个分桶的数据与其他分桶数据组合做nested loop join;3)数据合并。对应地simba中的算法实现如下:
1)分别把leftRDD和rightRDD进行STR分区(对应于hadoop的分桶),并对rightRDD建立RTree索引:
val (left_partitioned, left_mbr_bound) = STRPartition(left_rdd, dimension, num_partitions,
sample_rate, transfer_threshold, max_entries_per_node)
val (right_partitioned, right_mbr_bound) = STRPartition(right_rdd, dimension, num_partitions,
sample_rate, transfer_threshold, max_entries_per_node)
val right_rt = RTree(right_mbr_bound.zip(Array.fill[Int](right_mbr_bou