private void genJoinObject() throws HiveException {
    if (needsPostEvaluation && 0 == numAliases - 2) {
      int nextType = condn[0].getType();
      if (nextType == JoinDesc.RIGHT_OUTER_JOIN || nextType == JoinDesc.FULL_OUTER_JOIN) {
        // Initialize container to use for storing tuples before emitting them
        rowContainerPostFilteredOuterJoin = new HashMap<>();
      }
    }

    boolean rightFirst = true;
    boolean hasFilter = hasFilter(order[0]);
    AbstractRowContainer.RowIterator<List<Object>> iter = storage[order[0]].rowIter();
    for (List<Object> rightObj = iter.first(); rightObj != null; rightObj = iter.next()) {
      boolean rightNull = rightObj == dummyObj[0];
      if (hasFilter) {
        filterTags[0] = getFilterTag(rightObj);
      }
      skipVectors[0][0] = rightNull;
      intermediate[0] = rightObj;

      genObject(1, rightFirst, rightNull);
      rightFirst = false;
    }

    // Consolidation for outer joins
    if (needsPostEvaluation && 0 == numAliases - 2) {
      int nextType = condn[0].getType();
      if (nextType == JoinDesc.RIGHT_OUTER_JOIN || nextType == JoinDesc.FULL_OUTER_JOIN) {
        // If it is a RIGHT / FULL OUTER JOIN, we need to iterate through the row container
        // that contains all the right records that did not produce results. Then, for each
        // of those records, we replace the left side with NULL values, and produce the
        // records.
        // Observe that we only enter this block when we have finished iterating through
        // all the left and right records (aliasNum == numAliases - 2), and thus, we have
        // tried to evaluate the post-filter condition on every possible combination.
        // NOTE: the left records that do not produce results (for LEFT / FULL OUTER JOIN)
        // will always be caught in the genObject method
        Arrays.fill(forwardCache, null);
        for (Object[] row : rowContainerPostFilteredOuterJoin.values()) {
          if (row == null) {
            continue;
          }
          System.arraycopy(row, 0, forwardCache, offsets[numAliases - 1], row.length);
          internalForward(forwardCache, outputObjInspector);
          countAfterReport = 0;
        }
      }
    }
  }
On filterTags

ANDed value of all filter tags in current join group
if any of values passes on outer join alias (which makes zero for the tag alias),
it means there exists a pair for it and safely regarded as a inner join

for example, with table a, b something like,
  a = 100, 10 | 100, 20 | 100, 30
  b = 100, 10 | 100, 20 | 100, 30

the query "a FO b ON a.k=b.k AND a.v&gt;10 AND b.v&gt;30" makes filter map
  0(a) = [1(b),1] : a.v&gt;10
  1(b) = [0(a),1] : b.v&gt;30

for filtered rows in a (100,10) create a-NULL
for filtered rows in b (100,10) (100,20) (100,30) create NULL-b

with 0(a) = [1(b),1] : a.v&gt;10
  100, 10 = 00000010 (filtered)
  100, 20 = 00000000 (valid)
  100, 30 = 00000000 (valid)
-------------------------
      sum = 00000000 : for valid rows in b, there is at least one pair in a

with 1(b) = [0(a),1] : b.v&gt;30
  100, 10 = 00000001 (filtered)
  100, 20 = 00000001 (filtered)
  100, 30 = 00000001 (filtered)
-------------------------
      sum = 00000001 : for valid rows in a (100,20) (100,30), there is no pair in b

result :
  100, 10 :   N,  N
    N,  N : 100, 10
    N,  N : 100, 20
    N,  N : 100, 30
  100, 20 :   N,  N
  100, 30 :   N,  N
ql/src/java/org/apache/hadoop/hive/ql/exec/CommonJoinOperator.java