Hash 조인은 인덱스나 정렬이 없어도 효과적으로 조인할 수 있도록 설계된 방식입니다. 특히 대용량 데이터에서 Merge가 부담스러울 때 유용하며, 내부적으로는 Hash Table(해시 테이블) 을 만들어서 데이터를 효율적으로 매칭합니다.
-- Test 테이블 복사 생성
SELECT * INTO TestOrders FROM Orders;
SELECT * INTO TestCustomers FROM Customers;
-- 데이터 개수 확인
SELECT COUNT(*) FROM TestOrders; -- 830개
SELECT COUNT(*) FROM TestCustomers; -- 91개
두 테이블 모두 CustomerID 컬럼을 가지고 있으며, 이 컬럼을 기준으로 조인할 수 있습니다.
SELECT *
FROM TestOrders AS o
INNER JOIN TestCustomers AS c
ON o.CustomerID = c.CustomerID;
해당 쿼리의 실행 계획을 보면 Hash Match Join이 사용된 것을 확인할 수 있습니다.
여기서 주의할 점:
-- NL 조인 (내부에 인덱스 없음)
SELECT *
FROM TestOrders AS o
INNER JOIN TestCustomers AS c
ON o.CustomerID = c.CustomerID
OPTION (FORCE ORDER, LOOP JOIN);
-- Merge 조인
SELECT *
FROM TestOrders AS o
INNER JOIN TestCustomers AS c
ON o.CustomerID = c.CustomerID
OPTION (FORCE ORDER, MERGE JOIN);
class Player { public int playerId; }
class Salary { public int playerId; }
Random rand = new Random();
List<Player> players = new List<Player>();
List<Salary> salaries = new List<Salary>();
for (int i = 0; i < 10000; i++) {
if (rand.Next(0, 2) == 0) continue;
players.Add(new Player() { playerId = i });
}
for (int i = 0; i < 10000; i++) {
if (rand.Next(0, 2) == 0) continue;
salaries.Add(new Salary() { playerId = i });
}
// 해시 테이블 구축
Dictionary<int, Salary> hash = new Dictionary<int, Salary>();
foreach (var s in salaries)
hash[s.playerId] = s;
List<int> result = new List<int>();
foreach (var p in players)
{
if (hash.ContainsKey(p.playerId))
result.Add(p.playerId);
}
NL과 달리 Hash 조인은 내부적으로 해시 테이블을 만들어 빠르게 탐색합니다.
우리가 평소 자주 쓰는 Dictionary<TKey, TValue> 자료형이 바로 해시 구조입니다.
class HashTable
{
int _bucketCount;
List<int>[] _buckets;
public HashTable(int bucketCount = 100)
{
_bucketCount = bucketCount;
_buckets = new List<int>[bucketCount];
for (int i = 0; i < bucketCount; i++)
_buckets[i] = new List<int>();
}
public void Add(int value)
{
int key = value % _bucketCount;
_buckets[key].Add(value);
}
public bool Find(int value)
{
int key = value % _bucketCount;
return _buckets[key].Contains(value);
}
}
해시 테이블은 공간을 희생하고 속도를 얻는 구조입니다.
SELECT *
FROM TestOrders AS o
INNER JOIN TestCustomers AS c
ON o.CustomerID = c.CustomerID;
실행 계획을 보면 작은 쪽인 TestCustomers에서 해시 테이블을 만들고,
큰 쪽인 TestOrders를 순회하며 값을 매칭하는 방식으로 처리됩니다.