数据库Join实现

Join 实现算法

一.Nested Loopsb Join

1.定义

  Nested Loops也称为嵌套迭代,它将一个联接输入用作外部输入表(显示为图形执行计划中的顶端输入),将另一个联接输入用作内部(底端)输入表。外部循环逐行消耗外部输入表。内部循环为每个外部行执行,在内部输入表中搜索匹配行。最简单的情况是,搜索时扫描整个表或索引;这称为单纯嵌套循环联接。如果搜索时使用索引,则称为索引嵌套循环联接。如果将索引生成为查询计划的一部分(并在查询完成后立即将索引破坏),则称为临时索引嵌套循环联接。伪码表示如下:

  for each row R1 in the outer table
for each row R2 in the inner table
if R1 joins with R2
return (R1, R2)

2.应用场景

   适用于outer table(有的地方叫Master table)的记录集比较少(<10000)而且inner table(有的地方叫Detail table)索引选择性较好的情况下(inner table要有index)。

   inner table被outer table驱动,outer table返回的每一行都要在inner table中检索到与之匹配的行。当然也可以用ORDERED 提示来改变CBO默认的驱动表,使用USE_NL(table_name1 table_name2)可是强制CBO 执行嵌套循环连接。

   cost = outer access cost + (inner access cost * outer cardinality)

3.常用于执行的连接

  Nested Loops常执行Inner Join(内部联接)、Left Outer Join(左外部联接)、Left Semi Join(左半部联接)和Left Anti Semi Join(左反半部联接)逻辑操作。
Nested Loops通常使用索引在内部表中搜索外部表的每一行。根据预计的开销,Microsoft SQL Server决定是否对外部输入进行排序来改变内部输入索引的搜索位置。
将基于所执行的逻辑操作返回所有满足 Argument 列内的(可选)谓词的行。

二.Merge Join

  1.定义  

   Merge Join第一个步骤是确保两个关联表都是按照关联的字段进行排序。如果关联字段有可用的索引,并且排序一致,则可以直接进行Merge Join操作;否则,SQL Server需要先对关联的表按照关联字段进行一次排序(就是说在Merge Join前的两个输入上,可能都需要执行一个Sort操作,再进行Merge Join)。
两个表都按照关联字段排序好之后,Merge Join操作从每个表取一条记录开始匹配,如果符合关联条件,则放入结果集中;否则,将关联字段值较小的记录抛弃,从这条记录对应的表中取下一条记录继续进行匹配,直到整个循环结束。

  在多对多的关联表上执行Merge Join时,通常需要使用临时表进行操作。例如A join B使用Merge Join时,如果对于关联字段的某一组值,在A和B中都存在多条记录A1、A2…An、B1、B2…Bn,则为A中每一条记录A1、A2…An,都必须在B中对所有相等的记录B1、B2…Bn进行一次匹配。这样,指针需要多次从B1移动到Bn,每一次都需要读取相应的B1…Bn记录。将B1…Bn的记录预先读出来放入内存临时表中,比从原数据页或磁盘读取要快。

  2.应用场景另

  用在数据没有索引但是已经排序的情况下。

  通常情况下hash join的效果都比Sort merge join要好,然而如果行源已经被排过序,在执行排序合并连接时不需要再排序了,这时Sort merge join的性能会优于hash join。可以使用USE_MERGE(table_name1 table_name2)来强制使用Sort merge join。

  cost = (outer access cost * # of hash partitions) + inner access cost

 3.常用于执行的连接

  Merge Join常执行Inner Join(内部联接)、Left Outer Join(左外部联接)、Left Semi Join(左半部联接)、Left Anti Semi Join(左反半部联接)、Right Outer Join(右外部联接)、Right Semi Join(右半部联接)、Right Anti Semi Join(右反半部联接)和Union(联合)逻辑操作。

 在 Argument 列中,如果操作执行一对多联接,则 Merge Join 运算符将包含 MERGE:() 谓词;如果操作执行多对多联接,则该运算符将包含 MANY-TO-MANY MERGE:() 谓词。Argument 列还包含一个用于执行操作的列的列表,该列表以逗号分隔。Merge Join 运算符要求在各自的列上对两个输入进行排序,这可以通过在查询计划中插入显式排序操作来实现。如果不需要显式排序(例如,如果数据库内有合适的 B 树索引或可以对多个操作(如合并联接和对汇总分组)使用排序顺序),则合并联接尤其有效。

三.Hash Join

 1.定义

  Hash Match有两个输入:build input(也叫做outer input)和probe input(也叫做inner input),不仅用于inner/left/right join等,象union/group by等也会使用hash join进行操作,在group by中build input和probe input都是同一个记录集。

Hash Match操作分两个阶段完成:Build(构造)阶段和Probe(探测)阶段。
Build(构造)阶段主要构造哈希表(hash table)。在inner/left/right join等操作中,表的关联字段作为hash key;在group by操作中,group by的字段作为hash key;在union或其它一些去除重复记录的操作中,hash key包括所有的select字段。
Build操作从build input输入中取出每一行记录,将该行记录关联字段的值使用hash函数生成hash值,这个hash值对应到hash table中的hash buckets(哈希表目)。如果一个hash值对应到多个hash buckts,则这些hash buckets使用链表数据结构连接起来。当整个build input的table处理完毕后,build input中的所有记录都被hash table中的hash buckets引用/关联了。
Probe(探测)阶段,SQL Server从probe input输入中取出每一行记录,同样将该行记录关联字段的值,使用build阶段中相同的hash函数生成hash值,根据这个hash值,从build阶段构造的hash table中搜索对应的hash bucket。hash算法中为了解决冲突,hash bucket可能会链接到其它的hash bucket,probe动作会搜索整个冲突链上的hash bucket,以查找匹配的记录。 

  如果build input记录数非常大,构建的hash table无法在内存中容纳时,SQL Server分别将build input和probe input切分成多个分区部分(partition),每个partition都包括一个独立的、成对匹配的build input和probe input,这样就将一个大的hash join切分成多个独立、互相不影响的hash join,每一个分区的hash join都能够在内存中完成。SQL Server将切分后的partition文件保存在磁盘上,每次装载一个分区的build input和probe input到内存中,进行一次hash join。这种hash join叫做Grace Hash join,使用的Grace Hash Join算法。

  2.应用场景

  适用于两个表的数据量差别很大。但需要注意的是:如果HASH表太大,无法一次构造在内存中,则分成若干个partition,写入磁盘的temporary segment,则会多一个I/O的代价,会降低效率,此时需要有较大的temporary segment从而尽量提高I/O的性能。

  可以用USE_HASH(table_name1 table_name2)提示来强制使用散列连接。如果使用散列连HASH_AREA_SIZE 初始化参数必须足够的大,如果是9i,Oracle建议使用SQL工作区自动管理,设置WORKAREA_SIZE_POLICY 为AUTO,然后调整PGA_AGGREGATE_TARGET 即可。

  也可以使用HASH_JOIN_ENABLED=FALSE(默认为TRUE)强制不使用hash join。

  cost = (outer access cost * # of hash partitions) + inner access cost

 3.常用于执行的链接

  Hash Match运算符通过计算其生成输入中每行的哈希值生成哈希表。HASH:()谓词以及一个用于创建哈希值的列的列表出现在Argument列内。然后,该谓词为每个探测行(如果适用)使用相同的哈希函数计算哈希值并在哈希表内查找匹配项。如果存在残留谓词(由 Argument 列中的 RESIDUAL:() 标识),则还须满足此残留谓词,只有这样行才能被视为是匹配项。行为取决于所执行的逻辑操作:
  (1)对于联接,使用第一个(顶端)输入生成哈希表,使用第二个(底端)输入探测哈希表。按联接类型规定的模式输出匹配项(或不匹配项)。如果多个联接使用相同的联接列,这些操作将分组为一个哈希组。
  (2)对于非重复或聚合运算符,使用输入生成哈希表(删除重复项并计算聚合表达式)。生成哈希表时,扫描该表并输出所有项。
  (3)对于 union 运算符,使用第一个输入生成哈希表(删除重复项)。使用第二个输入(它必须没有重复项)探测哈希表,返回所有没有匹配项的行,然后扫描该哈希表并返回所有项。

Hive中Join物理实现过程

测试sql :
select * from a join b where a.id= b.id;

a 0 hash map table
b 1 scan table

alias : 1

MapJoinOperator

实现思路

  • 遍历主表alias的每一条记录
  • 去Hash表查看join的key是否存在,如果存在,取出结果集
  • 过滤当前记录
  • 将上面两个结果集做nest loop join,并转发结果
// MapJoinOperator.java
在cleanUpInputFileChangedOp() 方法中load hash table

generateMapMetaData();
loadHashTable();


// HashMapWrapper.java
// 通过join的key在hash表中查看是否有对应的值,如果有,取出结果List作为MapJoinRowContainer rowContainer,

public void setFromRow(Object row, List<ExprNodeEvaluator> fields,
        List<ObjectInspector> ois) throws HiveException {
      if (currentKey == null) {
        currentKey = new Object[fields.size()];
      }
      for (int keyIndex = 0; keyIndex < fields.size(); ++keyIndex) {
        currentKey[keyIndex] = fields.get(keyIndex).evaluate(row);
      }   
      key = MapJoinKey.readFromRow(output, key, currentKey, ois, !isFirstKey);  // currentKey 为join的相关column列,key 为计算出的对应的standardObject
      isFirstKey = false;
      this.currentValue = mHash.get(key);  // 去hash表查看join的值是否存在
    }

// 计算出当前alias行数据,并进行数据过滤  getFilteredValue()
List<Object> nr = JoinUtil.computeValues(row, joinValues[alias],
        joinValuesObjectInspectors[alias], hasFilter);
    if (hasFilter) {
      short filterTag = JoinUtil.isFiltered(row, joinFilters[alias],
          joinFilterObjectInspectors[alias], filterMaps[alias]);
      nr.add(new ShortWritable(filterTag));
      aliasFilterTags[alias] &= filterTag;
    }


// 所有上面操作结果是将当前join key的表数据全部放到storage[] 数组中

MapJoinRowContainer rowContainer = adaptor.getCurrentRows();  // 通过key join出来的一组value值

storage[pos] = rowContainer.copy(); 
            aliasFilterTags[pos] = rowContainer.getAliasFilter();  // 这个表示join的数据是否需要保留

CommonJoinOperator.checkAndGenObject() 
// 将关联好的结果表数据转发到下一个operator
if (!hasEmpty && !mayHasMoreThanOne) {
        genAllOneUniqueJoinObject();
      } else if (!hasEmpty && !hasLeftSemiJoin) {
        genUniqueJoinObject(0, 0);
      } else {
        genJoinObject();
      }

// 生成join数据。将storage中的记录全部取出,做nest loop join并将数据forward
private void genUniqueJoinObject(int aliasNum, int forwardCachePos)
      throws HiveException {
    AbstractRowContainer.RowIterator<List<Object>> iter = storage[order[aliasNum]].rowIter();
    for (List<Object> row = iter.first(); row != null; row = iter.next()) {
      int sz = joinValues[order[aliasNum]].size();
      int p = forwardCachePos;
      for (int j = 0; j < sz; j++) {
        forwardCache[p++] = row.get(j);
      }
      if (aliasNum == numAliases - 1) {
        internalForward(forwardCache, outputObjInspector);
        countAfterReport = 0;
      } else {
        genUniqueJoinObject(aliasNum + 1, p);
      }
    }
  }

关键变量:

order : 所有的表下标
posBigTab :需要扫描的表的下标

CommonMergeJoinOperator

整体实现思路

  • 从source 0 (Master table)中开始遍历数据,调用processOp()
  • processOp()中会初始化 (Detail table)表数据,并放入容器,直到Detail 表中取到nextGroup
  • 取Master table 的结果放入容器,直到Master table中取到nextGroup
  • 将所有storage容器中的结果进行Join生成结果
  • 结束主表循环在close() 方法中取出剩余的数据,生成结果直到结束

Source 消费数据流程

  • ReduceRecordSource.pushRecord()
  • reader.next() 获取getCurrentKey() ,getCurrentValues()
  • 将values 放入groupIterator,之后每次调用这个迭代器来取数据
  • 取出的数据传递给当前实例对象的reducer对象进行消费
TezProcessor::run()
TezProcessor::initializeAndRunProcessor(inputs, outputs); 
----- run() 调用流程
rproc.run()
ReduceRecordSource::pushRecord()
groupIterator.next() 循环调用子Operator处理数据
CommonMergeJoinOperator.processOp() 开始处理主表(0)数据.在主表的数据取到nextKeyGroup 时,开始join数据,否则将数据放入container

----- close() 调用流程
在rproc.close() 中调用对应的close方法
ReduceRecoredProcessor.run()
ReduceRecoredProcessor::close()
reducer.close(abort)
Operator.closeOp(abort)
CommonMergeJoinOperator::closeOp()
CommonMergeJoinOperator::joinFinalLeftData()

----- 具体处理代码
在处理新表的时候
如果是新的group:
    this.nextGroupStorage[alias].addRow(value);
    foundNextKeyGroup[tag] = true;
    return

如果是同一个group:
    candidateStorage[tag].addRow(value);

部分方法说明:
joinOneGroup() -> joinObject()  // join一组数据,并返回下一次需要fetch的表下标
candidateStorage[] -> storage[]  -> checkAndGenObject(); // 通过将临时容器中的数据放入正式容器中,开始生成join结果
nextKeyGroup // 标识是否为一组新的数据
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值