SQL 기초 — JOIN, 집합 연산, NULL과 키
80 SQL Interview Questions for Data Engineers (Real Asks, 2026) — DataVidhya blog Basic SQL & Joins, Q1-10 · Q19-20 (pp.2-6, 9-10)
이 모듈을 다 읽으면
- INNER/LEFT/CROSS/SELF JOIN의 차이와 각각의 사용 시나리오를 설명할 수 있다
- WHERE와 HAVING, UNION과 UNION ALL의 실행 시점과 비용 차이를 설명할 수 있다
- NULL이 비교 연산에서 특별하게 취급되는 이유와 안전한 처리 방법을 설명할 수 있다
- PRIMARY KEY, UNIQUE KEY, FOREIGN KEY의 차이를 구분할 수 있다
SQL 조인의 기본 유형과 WHERE/HAVING의 실행 순서, UNION 계열 연산자, NULL 비교의 함정, 그리고 키(PK/UK/FK)의 기본 개념을 다룬다. 데이터 엔지니어 인터뷰에서 가장 먼저 확인하는 기초 항목들이다.
JOIN의 종류와 선택 기준
INNER JOIN은 두 테이블에서 조인 조건에 매칭되는 행만 반환한다. 반면 LEFT JOIN(=LEFT OUTER JOIN)은 왼쪽 테이블의 모든 행을 그대로 유지하고, 오른쪽 테이블에 매칭되는 행이 없으면 오른쪽 컬럼들을 NULL로 채워 반환한다. 실무에서는 왼쪽 테이블이 분석의 '기준(spine)'이 될 때 LEFT JOIN을 쓴다 — 예를 들어 주문이 없는 고객도 포함해 전체 고객 목록을 보여줘야 할 때다.
CROSS JOIN은 두 테이블의 카티션 곱(Cartesian product)을 만든다. 10행짜리 테이블과 10행짜리 테이블을 CROSS JOIN하면 100행이 나온다. 의도치 않게 조인 조건을 빠뜨렸을 때 발생하는 '행 폭발' 버그의 정체가 바로 CROSS JOIN인 경우가 많으므로, 실행 계획에서 예상보다 훨씬 많은 행 수가 나오면 조인 조건 누락을 의심해야 한다.
SELF JOIN은 같은 테이블에 서로 다른 별칭(alias) 두 개를 부여해 자기 자신과 조인하는 기법이다. 대표적으로 employee 테이블에 manager_id가 같은 테이블의 employee_id를 가리키는 재귀적 관계(조직도, 상사-부하)를 표현할 때 사용한다.
핵심 포인트
- INNER JOIN은 매칭되는 행만, LEFT JOIN은 왼쪽 테이블의 모든 행을 유지하고 매칭 실패 시 NULL을 채운다.
- CROSS JOIN은 두 테이블의 카티션 곱을 만들어 행 수가 곱셈으로 늘어난다 — 조인 조건 누락 버그의 흔한 원인이다.
- SELF JOIN은 같은 테이블에 두 개의 별칭을 부여해 조직도 같은 재귀적 관계를 표현한다.
WHERE vs HAVING, UNION vs UNION ALL
WHERE 절은 GROUP BY로 그룹을 만들거나 집계 함수를 계산하기 전, 개별 행 단위로 필터링한다. HAVING 절은 GROUP BY 이후 집계된 결과(SUM, COUNT 등)를 필터링하며, WHERE에는 집계 함수를 쓸 수 없다. 예를 들어 `SELECT region, SUM(revenue) FROM sales WHERE date > '2026-01-01' GROUP BY region HAVING SUM(revenue) > 100000`에서 WHERE는 2026년 이전 행을 미리 제외하고, HAVING은 지역별 합계가 10만 미만인 그룹만 걸러낸다.
UNION은 두 결과 집합을 합친 뒤 중복을 제거한다(내부적으로 정렬 또는 해시 기반 중복 제거 단계가 추가된다). UNION ALL은 중복 제거 없이 단순히 이어 붙이므로 훨씬 빠르다. 데이터가 이미 중복이 없다고 확신하거나 중복을 허용해도 되는 상황이라면 기본적으로 UNION ALL을 쓰는 것이 실무 원칙이다 — UNION은 사실상 'UNION ALL + DISTINCT'와 같다.
핵심 포인트
- WHERE는 그룹핑/집계 이전에 개별 행을 필터링하고, HAVING은 GROUP BY 이후 집계 결과를 필터링한다.
- WHERE 절에는 집계 함수를 직접 쓸 수 없다.
- UNION ALL은 중복 제거 단계가 없어 UNION보다 빠르다 — 중복이 문제되지 않는다면 기본으로 UNION ALL을 선택한다.
NULL 비교의 함정과 중복 탐지
NULL은 '값이 없다'가 아니라 '알 수 없다(unknown)'는 의미다. 그래서 `= NULL`이나 `!= NULL` 같은 일반 비교 연산자는 항상 알 수 없음(unknown)으로 평가되어 원하는 결과를 주지 않는다. NULL 여부를 확인하려면 반드시 `IS NULL` / `IS NOT NULL`을 써야 하고, 여러 소스를 병합하며 누락값에 기본값을 채우고 싶다면 COALESCE(a, b, c)로 순서대로 첫 번째 NULL이 아닌 값을 취한다.
중복 행을 찾을 때는 기준 컬럼으로 GROUP BY 한 뒤 `HAVING COUNT(*) > 1`을 적용한다. 다만 이 방식은 '어떤 키 조합이 몇 번 중복됐는지'만 보여줄 뿐, 실제 중복 행 자체를 가려내 하나만 남기고 지우려면 `ROW_NUMBER() OVER (PARTITION BY 중복기준 ORDER BY 고유ID)`로 행 번호를 매긴 뒤 1보다 큰 행을 삭제하는 방식이 표준적이다(윈도우 함수는 다음 모듈에서 다룬다).
핵심 포인트
- NULL은 '없음'이 아니라 '알 수 없음'이라서 `=`, `!=` 비교가 항상 실패하며 IS NULL/IS NOT NULL을 써야 한다.
- COALESCE(a, b, c)는 인자를 순서대로 평가해 첫 NULL이 아닌 값을 반환한다.
- GROUP BY + HAVING COUNT(*) > 1은 중복 키를 찾고, 실제 중복 행 제거에는 ROW_NUMBER() 윈도우 함수가 필요하다.
PRIMARY KEY, UNIQUE KEY, FOREIGN KEY
PRIMARY KEY(기본 키)는 테이블의 각 행을 고유하게 식별하며 NULL을 허용하지 않고, 테이블당 하나만 지정할 수 있다. UNIQUE KEY(유니크 키)도 값의 유일성을 보장하지만 NULL을 허용하며(대부분의 엔진에서 NULL은 유일성 검사에서 제외되어 여러 개의 NULL이 가능) 한 테이블에 여러 개를 둘 수 있다.
FOREIGN KEY(외래 키)는 한 테이블의 컬럼이 다른 테이블의 기본 키를 참조하도록 해 참조 무결성(referential integrity)을 강제한다 — 즉 부모 테이블에 존재하지 않는 데이터를 가리키는 '고아 레코드(orphan record)'가 생기지 않도록 막는다. 이는 OLTP(운영계) 데이터베이스에서 특히 중요한 제약이며, 분석용 웨어하우스에서 FK를 다루는 방식(선언은 하되 강제하지 않는 경우가 많은 이유 등)은 07번 모듈(키 설계와 카디널리티)에서 더 자세히 다룬다.
핵심 포인트
- PRIMARY KEY는 NULL을 허용하지 않고 테이블당 하나만 존재하며 행을 유일하게 식별한다.
- UNIQUE KEY는 유일성은 보장하지만 NULL을 허용하고 한 테이블에 여러 개를 둘 수 있다.
- FOREIGN KEY는 다른 테이블의 PK를 참조해 참조 무결성을 강제하고 고아 레코드 생성을 막는다.