private void joinFinalLeftData() throws HiveException {
    @SuppressWarnings("rawtypes")
    RowContainer bigTblRowContainer = this.candidateStorage[this.posBigTable];

    boolean allFetchDone = allFetchDone();
    // if all left data in small tables are less than and equal to the left data
    // in big table, let's them catch up
    while (bigTblRowContainer != null && bigTblRowContainer.rowCount() > 0 && !allFetchDone) {
      joinOneGroup();
      bigTblRowContainer = this.candidateStorage[this.posBigTable];
      allFetchDone = allFetchDone();
    }

    while (!allFetchDone) {
      List<Byte> ret = joinOneGroup();
      // if we are in close op phase, we have definitely exhausted the big table input
      fetchDone[posBigTable] = true;
      // First, handle the condition where the first fetch was never done (big table is empty).
      doFirstFetchIfNeeded();
      // in case of outer joins, we need to pull in records from the sides we still
      // need to produce output for apart from the big table. for e.g. full outer join
      // TODO: this reproduces the logic of the loop that was here before, assuming
      // firstFetchHappened == true. In reality it almost always calls joinOneGroup. Fix it?
      int lastPos = (fetchDone.length - 1);
      if (posBigTable != lastPos
          && (fetchInputAtClose.contains(lastPos)) && (fetchDone[lastPos] == false)) {
        // Do the join. It does fetching of next row groups itself.
        ret = joinOneGroup();
      }

      if (ret == null || ret.size() == 0) {
        break;
      }

      reportProgress();
      numMapRowsRead++;
      allFetchDone = allFetchDone();
    }

    boolean dataInCache = true;
    while (dataInCache) {
      for (byte pos = 0; pos < order.length; pos++) {
        if (this.foundNextKeyGroup[pos] && this.nextKeyWritables[pos] != null) {
          fetchNextGroup(pos);
        }
      }
      joinOneGroup();
      dataInCache = false;
      for (byte pos = 0; pos < order.length; pos++) {
        if (candidateStorage[pos] == null) {
          continue;
        }
        if (this.candidateStorage[pos].hasRows()) {
          dataInCache = true;
          break;
        }
      }
    }
  }
Decides if the actual row must be an unmatched row.

Unmatched rows are those which are not part of the inner-join.
The current implementation has issues processing filtered rows in FOJ conditions.
Putting them in a separate group also reduces processing done for them.
ql/src/java/org/apache/hadoop/hive/ql/exec/CommonMergeJoinOperator.java