Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Sort-Merge Join: I/O Cost Analysis

Recommended resources

Hello! Let's continue our exploration of join algorithms.

In previous lessons, we examined the Block Nested Loop Join (BNLJ) and the Index Nested Loop Join (INLJ). We saw that BNLJ is a decent fallback but can be slow if both tables are large. INLJ is extremely fast for selective lookups but requires a suitable index and becomes very expensive if the outer table is large, due to the high number of probes.

This raises a critical question: what is the best strategy for joining two large tables when no suitable index is available? Today, we'll answer that by diving into the Sort-Merge Join algorithm. This lesson directly addresses the learning outcome: to implement and analyze the I/O cost of the sort-merge join algorithm. This is a classic and powerful algorithm that cleverly uses sorting to enable an efficient, single-pass join.

1. The Core Idea: Divide and Conquer via Sorting

The Sort-Merge Join algorithm operates in two distinct phases:

  1. Sort Phase: Sort both relations, R and S, based on their join key attributes.
  2. Merge Phase: Scan the two sorted relations in parallel and "merge" them, producing join results wherever the keys match.

This approach avoids the repeated scanning of the inner table seen in BNLJ. Once the tables are sorted, finding matching records becomes a linear scan, much like merging two sorted lists.

Sort-Merge Join Algorithm Explained with Example Tables
This image illustrates the two main phases of a sort-merge join. First, the 'Customers' and 'Orders' tables are independently sorted on their join keys. Then, in the merging phase, the sorted tables are scanned together to produce the final joined output.

2. Phase 1: The Sort Phase

The first step is to ensure both tables are sorted on the join key. Since the tables are often too large to fit in memory, a disk-based sorting algorithm is required. As you may recall from the external sorting lesson (or will see in the implementation project LINK), this is precisely the job of the External Merge Sort algorithm.

An important optimization is to check if a table is already sorted on the join key. This can happen if:

  • The table has a clustered B+ Tree index on the join attribute.
  • The data is the output of a previous query plan operator that already produced a sorted result (this is known as an "interesting order").

If the data is already sorted, the sort phase for that table can be skipped entirely, providing a significant performance boost.

14.416 Sort-Merge Join, Co-Grouping

The video from Prof. Dr. Jens Dittrich's course introduces the sort-merge join and highlights the importance of the initial sorting step, including scenarios where it can be avoided.

Watch the first 2 minutes and 48 seconds. Pay attention to the two conditions under which the initial sorting step can be skipped.


3. Phase 2: The Merge Phase

Once both relations are sorted, the merge phase begins. The logic is elegant and efficient. We use two pointers (or cursors), one for each relation, initialized to the first tuple. We then advance the pointers based on a comparison of the join keys.

Let's use pr to denote the pointer to the current tuple in relation R and ps for relation S.

  1. If pr.key == ps.key: We have a match! We output a joined tuple. We then need to advance the pointers to check for more matches (we'll address the complication of duplicates shortly).
  2. If pr.key < ps.key: Since the relations are sorted, we know pr cannot possibly match ps or any subsequent tuple in S. So, we advance pr.
  3. If pr.key > ps.key: Symmetrically, we know ps cannot match pr, so we advance ps.

This process continues until one of the pointers reaches the end of its relation.

The following video from the CMU Database Group provides an excellent step-by-step walkthrough of this merge process.

#12 - Join Algorithms: Hash, Sort-Merge, Nested Loop Joins (CMU Intro to Database Systems)

This segment from the CMU lecture visually demonstrates the merge phase of the sort-merge join. It clearly shows how the cursors advance through the two sorted tables.

Watch from 33:55 to 38:50. Follow the example as the instructor moves the cursors through the outer (R) and inner (S) tables, producing join results.

Handling Duplicates: A Critical Detail

The simple pointer advancement logic gets tricky when duplicate join keys are present. For example, if we have multiple tuples in R with key 100 and multiple tuples in S with key 100, we must produce the full cross-product of these matching groups. A naive implementation might miss some of these pairs.

The CMU video you just watched hints at a "backtracking" mechanism. A more robust way to think about this, especially from an implementation perspective, is to use a co-grouping strategy.

The idea is:

  1. When you find a match (pr.key == ps.key), you've identified a matching "group".
  2. Scan forward in both R and S to find all adjacent tuples with that same key.
  3. You now have two sets of tuples (one from R, one from S) that all share the same join key.
  4. Compute the cross-product of these two small sets in memory.
  5. Once done, reposition your main cursors (pr and ps) to the first tuple after these groups and continue the main merge loop.

This approach correctly handles many-to-many relationships without complex backtracking logic.

14.416 Sort-Merge Join, Co-Grouping

This video segment discusses the problem of duplicates in detail and presents the 'co-grouping' concept as the correct way to handle them.

Watch from 10:20 to 15:40. Focus on why the simple pointer logic fails and how thinking in terms of 'groups' and 'cross-products' solves the problem.


4. I/O Cost Analysis

The total I/O cost of a sort-merge join is the sum of the costs of its two phases. Let's assume we have B buffer pages available.

1. Sort Cost:
This is the cost of running external merge sort on both relations. Using the formula from our lectures, where a relation has P pages:

So, the total sort cost is:

For large tables, this sorting cost is the dominant part of the algorithm.

2. Merge Cost:

  • Average Case: In the common scenario where duplicate keys are few and far between, the merge phase requires just a single scan of each sorted relation. The cost is simply I/Os.
  • Worst Case: If all tuples in both relations have the same join key value, the algorithm degenerates. For each tuple in the outer relation's group, we'd have to scan the entire inner relation's group. This leads to a worst-case merge cost of I/Os. This is highly unlikely in practice but is important to know.

Total Cost (Average Case):

Let's see how this stacks up against the other algorithms.

15-445/645 Database Systems (Fall 2025) - 12 Joins Algorithms

The CMU lecture notes provide a concise summary of the sort-merge join algorithm, its cost formula, and a table comparing its performance to other join methods with a concrete numerical example.

Read section 4, 'Sort-Merge Join,' on page 2 for a summary of the algorithm and its cost. Then, review the table in section 6, 'Conclusion,' on page 4 to see how the example cost of 0.75 seconds is calculated and how it compares to other algorithms.

Test your understanding!

Let's reuse the setup from our last lesson, but now we'll analyze Sort-Merge Join.

  • Table R (Orders): 1,000 pages ()
  • Table S (Customers): 500 pages ()
  • Buffer Pool: 100 pages ()

Assume for simplicity that the log term in the external sort cost formula evaluates to 1 for both tables. This is what the CMU notes do for their example 2M * (1+1).

  • Sort cost for R =
  • Sort cost for S =
  1. Calculate the I/O cost for a Block Nested Loop Join (with Orders as the outer table).
  2. Calculate the total I/O cost for a Sort-Merge Join (assuming the average case for the merge phase).
  3. Which one is better in this scenario?
Show answer
  1. Block Nested Loop Join Cost:
    The formula is .

    • Scan of outer table R: I/Os.
    • Number of chunks for R: chunks.
    • We must scan the inner table S 11 times. Each scan costs I/Os.
    • Total Cost = .
  2. Sort-Merge Join Cost:
    The formula is .

    • Sort Cost for R: I/Os.
    • Sort Cost for S: I/Os.
    • Merge Cost: I/Os.
    • Total Cost = .
  3. Conclusion:
    In this specific case, the Block Nested Loop Join is slightly better (6,500 I/Os vs. 7,500 I/Os). This highlights an important trade-off: the high up-front cost of sorting both tables can sometimes make Sort-Merge Join more expensive than a BNLJ, especially when one table is significantly smaller than the other, making the repeated scans in BNLJ "cheap enough". However, as both tables grow larger and more similar in size, the component of BNLJ quickly gets out of hand, and Sort-Merge's more predictable sort + (M+N) cost becomes far superior.


Conclusion

We've now added a powerful tool to our query execution toolbox. Sort-Merge Join is an excellent general-purpose algorithm for joining large tables, especially when a suitable index for an Index Nested Loop Join is not available.

Key Takeaways:

  • Two-Phase Algorithm: Sort-Merge join consists of a Sort Phase (using external merge sort) and a Merge Phase (using a synchronized scan with two pointers).
  • I/O Cost: The cost is dominated by the initial sort, with an average-case total of roughly .
  • Duplicate Handling: Correctly joining tuples with duplicate keys is non-trivial and requires a robust strategy like co-grouping to ensure correctness.
  • "Interesting Orders": A major advantage of this algorithm is that its output is sorted on the join key, which may eliminate the need for a separate ORDER BY operation later in the query plan.

Preview of the Next Lesson

While Sort-Merge join is a huge improvement over nested loop joins for large tables, the CMU video hinted that there's often an even faster method: Hash Join. In our next lesson, we will explore the Grace Hash Join algorithm, which uses hashing to partition relations into memory-sized buckets, replacing the expensive sorting phase with a potentially cheaper hashing phase.