6 연결고리 찾기
1930년 3월 남극 베이스캠프. Dyer 교수는 지난 3년간 수집된 모든 데이터를 책상에 펼쳐놓고 있었다. 개별적으로는 의미를 파악하기 어려웠던 측정값들이 이제 하나의 큰 그림을 그려내고 있었다.
“각 사이트의 좌표, 방문 날짜, 측정값… 이 모든 것들이 어떻게 연결되어 있을까?”
Lake 박사가 지도 위에 손가락으로 경로를 그려보며 말했다. “DR-1에서 시작해서 DR-3으로, 그리고 MSK-4까지… 우리의 이동 경로에는 분명 패턴이 있을 거야. Roerich의 이상한 측정값들도 특정 장소와 시간에 집중되어 있지 않나?”
이제 우리는 AI의 도움을 받아 탐사대가 미처 발견하지 못했던 연결고리들을 찾아보자.
6.1 왜 테이블을 나누어 저장할까?
일상생활에서 우리는 모든 정보를 한 곳에 모아두지 않는다. 예를 들어, 학교에서는 학생 정보와 성적 정보를 따로 관리한다. 학생의 이름, 학번, 생년월일은 ’학생 명부’에, 각 과목별 점수는 ’성적표’에 기록한다. 왜 이렇게 나누어 관리할까?
만약 모든 정보를 하나의 거대한 표에 넣는다면 어떨까? 한 학생이 10개 과목을 수강한다면, 그 학생의 이름과 학번이 10번 반복해서 저장된다. 이는 저장 공간의 낭비일 뿐만 아니라, 학생 정보를 수정할 때 10곳을 모두 찾아서 바꿔야 하는 번거로움이 생긴다.
이런 문제를 데이터베이스에서는 데이터 중복(Data Redundancy) 문제라고 한다. 더 심각한 것은 중복된 데이터가 서로 다르게 수정되어 데이터 불일치(Data Inconsistency) 문제가 발생할 수 있다는 점이다. 예를 들어, 한 학생의 이름을 변경할 때 10개 중 9개만 수정하고 1개를 빠뜨린다면, 같은 학생인데도 서로 다른 이름이 기록되는 상황이 생긴다.
Netflix가 수억 명의 사용자와 수십만 개의 콘텐츠를 관리할 수 있는 것도 이런 데이터 정규화(Data Normalization) 원칙 때문이다. 사용자 정보, 영화 정보, 시청 기록을 각각 별도 테이블로 관리하면서 필요할 때만 JOIN으로 연결하는 것이다. 만약 모든 정보를 하나의 테이블에 넣는다면, 한 사용자가 1000개 영화를 시청할 때마다 사용자의 이름과 이메일이 1000번 중복 저장될 것이다.
탐사대 데이터베이스도 같은 원리로 설계되었다. 각 테이블은 명확한 역할을 가지고 있다:
- Site 테이블: 탐사 지점의 위치 정보 (이름, 위도, 경도)
- Visited 테이블: 방문 기록 (언제, 어느 지점을 방문했는지)
- Person 테이블: 탐사대원 정보 (이름, 개인 식별 정보)
- Survey 테이블: 실제 측정 데이터 (누가, 언제, 무엇을, 어떤 값으로)
이렇게 분리된 정보들을 하나로 합쳐서 의미 있는 결과를 만드는 것이 바로 JOIN
의 역할이다.
6.2 JOIN 마법 - 퍼즐 맞추기
JOIN은 마치 퍼즐 조각을 맞추는 것과 같다. 각 테이블에는 다른 테이블과 연결할 수 있는 ’열쇠’가 있다. Site 테이블의 name
컬럼과 Visited 테이블의 site
컬럼이 바로 그 열쇠다.
JOIN의 내부 작동 원리를 이해해보자. 데이터베이스 엔진은 JOIN을 수행할 때 먼저 두 테이블의 카테시안 곱(Cartesian Product)을 만든다. 이는 첫 번째 테이블의 모든 행을 두 번째 테이블의 모든 행과 결합하는 것이다. Site 테이블에 3개 행이 있고 Visited 테이블에 8개 행이 있다면, 총 24개(3×8)의 조합이 만들어진다. 그 다음 ON
조건에 맞는 행들만 걸러내는 것이다.
이 과정에서 인덱스(Index)의 역할이 중요하다. JOIN 조건에 사용되는 컬럼에 인덱스가 있으면, 데이터베이스 엔진은 카테시안 곱을 만들지 않고도 효율적으로 매칭되는 행들을 찾을 수 있다. 실제 운영 환경에서 수백만 개의 행을 가진 테이블들을 JOIN할 때, 인덱스가 없으면 몇 시간이 걸릴 쿼리가 인덱스가 있으면 몇 초 만에 완료되기도 한다.
JOIN의 순서도 성능에 큰 영향을 미친다. 일반적으로 작은 테이블을 먼저 처리하고 큰 테이블을 나중에 처리하는 것이 효율적이다. 최신 데이터베이스 엔진들은 쿼리 옵티마이저(Query Optimizer)를 통해 최적의 JOIN 순서를 자동으로 결정하지만, 복잡한 쿼리에서는 개발자가 직접 힌트를 주어야 할 때도 있다.
6.2.1 INNER JOIN과 LEFT JOIN
데이터베이스에는 여러 종류의 JOIN이 존재하지만, 실무에서 가장 자주 마주치는 것은 INNER JOIN과 LEFT JOIN이다. 이 두 가지만 제대로 이해해도 대부분의 데이터 분석 작업을 수행할 수 있다.
JOIN 유형을 선택하는 것은 마치 사진을 찍을 때 프레임을 결정하는 것과 같다. INNER JOIN은 두 테이블이 완벽하게 겹치는 부분만 캡처하는 ’클로즈업 샷’이라면, LEFT JOIN은 왼쪽 테이블을 중심으로 전체 맥락을 담는 ’와이드 샷’이다. 어떤 JOIN을 선택하느냐에 따라 결과 데이터가 들려주는 이야기가 완전히 달라질 수 있다.
특히 데이터 분석에서 자주 발생하는 실수 중 하나가 잘못된 JOIN 유형 선택으로 인한 데이터 누락이다. INNER JOIN을 사용했다가 중요한 데이터가 빠진 것을 뒤늦게 발견하거나, LEFT JOIN을 사용했다가 불필요한 NULL 값들로 인해 집계가 왜곡되는 경우가 있다. 이제 각 JOIN이 어떻게 작동하는지 구체적으로 살펴보자.
INNER JOIN (교집합): 양쪽 테이블에 모두 존재하는 데이터만 결과에 포함한다. 탐사 연구에서는 “실제 관측이 기록된 실험 장비와 측정값의 관계”를 분석할 때 사용한다.
SELECT Site.name, Site.lat, Site.long, Visited.dated
FROM Site
INNER JOIN Visited ON Site.name = Visited.site;
LEFT JOIN (왼쪽 기준): 왼쪽 테이블의 모든 데이터를 유지하면서, 오른쪽 테이블에서 매칭되는 정보를 가져온다. “모든 실험 장비 목록을 유지하면서 관측 기록”을 함께 보고 싶을 때 활용한다.
SELECT Site.name, Site.lat, Site.long, Visited.dated
FROM Site
LEFT JOIN Visited ON Site.name = Visited.site;
INNER JOIN은 실제로 방문한 사이트만 보이고, LEFT JOIN은 방문하지 않은 사이트도 NULL 값과 함께 결과에 나타난다. 과학 연구에서 LEFT JOIN은 특히 중요한데, “데이터가 없는 것도 하나의 정보”이기 때문이다. 예를 들어, 어떤 지역에 측정 장비를 설치했지만 아직 데이터가 수집되지 않았다면, 이것 자체가 연구 계획 수정의 단서가 될 수 있다.
RIGHT JOIN과 FULL OUTER JOIN도 존재하지만, 실무에서는 INNER JOIN과 LEFT JOIN이 전체 사용량의 95% 이상을 차지한다.
JOIN에서 주의할 점은 NULL 값 처리다. NULL은 “값이 없음”을 의미하므로, 일반 비교 연산자로는 찾을 수 없다. WHERE column IS NULL
또는 WHERE column IS NOT NULL
을 사용해야 한다. 탐사대 데이터에서 752번 방문의 NULL 날짜처럼, NULL 값도 중요한 과학적 정보를 담고 있을 수 있다.
6.2.2 실제 JOIN 사용해보기
이제 실제로 Site와 Visited 테이블을 INNER JOIN으로 연결해보자. 먼저 어떤 결과가 나올지 예상해보자면, Site 테이블의 각 지점이 Visited 테이블의 방문 기록과 어떻게 매칭될까?
SELECT Site.name, Site.lat, Site.long, Visited.dated
FROM Site
INNER JOIN Visited ON Site.name = Visited.site;
name | lat | long | dated |
---|---|---|---|
DR-1 | -49.85 | -128.57 | 1927-02-08 |
DR-1 | -49.85 | -128.57 | 1927-02-10 |
DR-1 | -49.85 | -128.57 | 1932-03-22 |
DR-3 | -47.15 | -126.72 | - |
DR-3 | -47.15 | -126.72 | 1930-01-07 |
DR-3 | -47.15 | -126.72 | 1930-01-12 |
DR-3 | -47.15 | -126.72 | 1930-02-26 |
MSK-4 | -48.87 | -123.4 | 1932-01-14 |
결과를 자세히 분석해보면 흥미로운 패턴들이 발견된다. DR-1은 총 3번 방문되었는데, 1927년 초기 탐사 2회와 1932년 재방문 1회다. 5년이라는 긴 공백이 있었던 것을 보면, DR-1은 초기 탐사의 거점 역할을 했다가 나중에 검증 목적으로 다시 찾았을 가능성이 높다.
DR-3은 총 4번 방문으로 가장 활발한 활동을 보인다. 1930년 1월부터 2월까지 집중적으로 조사되었으며, 특히 날짜가 비어있는 한 번의 방문(우리의 752번 미스터리!)이 포함되어 있다. 이는 DR-3이 주요 연구 대상지였음을 시사한다.
반면 MSK-4는 단 한 번만 방문되었다. 1932년 1월이라는 시기를 보면, 이는 탐사 후반기의 새로운 지역 개척 차원으로 보인다. 한 번의 방문으로 충분한 정보를 얻었거나, 접근이 어려운 지역이었을 수 있다.
이제 LEFT JOIN 결과와 비교해보자. LEFT JOIN을 사용하면 Site 테이블의 모든 지점이 결과에 나타날 것이다:
SELECT Site.name, Site.lat, Site.long, Visited.dated
FROM Site
LEFT JOIN Visited ON Site.name = Visited.site;
실제로는 Site 테이블에 있는 모든 지점이 Visited 테이블에도 기록되어 있어서 INNER JOIN과 LEFT JOIN의 결과가 동일하다. 하지만 만약 Site 테이블에 방문하지 않은 지점이 있었다면, LEFT JOIN에서는 해당 지점도 NULL 값과 함께 결과에 나타났을 것이다.
INNER JOIN은 “확실한 연관성이 있는 데이터만 분석”할 때, LEFT JOIN은 “빠진 데이터나 연관성 없는 데이터도 파악”해야 할 때 사용한다. 탐사대의 경우 모든 사이트가 최소 한 번은 방문되었지만, 실제 과학 프로젝트에서는 계획된 실험 중 일부가 실행되지 않거나 데이터 수집에 실패하는 경우가 흔하다. 이런 “실패한 실험”도 중요한 정보이므로 LEFT JOIN으로 전체 그림을 파악하는 것이 필수다.
6.3 752번 미스터리 해결
이제 JOIN의 작동 원리를 이해했다. 하지만 이론만으로는 부족하다. 실제 데이터에서 숨겨진 이야기를 찾아내는 것이야말로 JOIN의 진정한 가치다.
남극 탐사대 기록 중에서 가장 수수께끼로 남은 것이 바로 752번 방문이다. Visited 테이블을 살펴보면, 이 방문 기록에는 날짜가 NULL로 되어 있다. 과학 탐사에서 날짜는 핵심 정보인데, 왜 이 기록만 비어있을까? 장비 고장? 기록 실수? 아니면 어떤 특별한 이유가 있었을까?
이제 네 개의 테이블을 하나씩 연결하며 752번 미스터리의 진실을 추적해보자. 마치 탐정이 단서를 조합하듯, JOIN을 통해 흩어진 정보를 하나로 모아보겠다.
SELECT v.id, v.site, v.dated, s.name, s.lat, s.long
FROM Visited v
INNER JOIN Site s ON v.site = s.name
WHERE v.id = 752;
id | site | dated | name | lat | long |
---|---|---|---|---|---|
752 | DR-3 | NULL | DR-3 | -47.15 | -126.72 |
JOIN 덕분에 우리는 752번 방문이 DR-3 사이트에서 이루어졌다는 것을 확실히 알 수 있다. 비록 정확한 날짜는 알 수 없지만, 위치는 명확하다: 남위 47.15도, 서경 126.72도.
6.3.1 완전한 그림 만들기
지금까지 두 테이블을 조합하는 JOIN을 통해 752번 방문의 기본 정보를 확인했다. 하지만 우리가 알아낸 것은 빙산의 일각에 불과하다. DR-3 사이트에서 언제인지 모르는 시점에 누군가가 무언가를 측정했다는 사실뿐이다. 과학 탐사에서 진짜 중요한 것은 구체적인 측정 데이터와 그 데이터를 누가 수집했는지다.
완전한 답을 얻기 위해서는 네 개의 테이블을 모두 연결해야 한다. Site 테이블에서 정확한 위치를, Visited 테이블에서 방문 기록을, Survey 테이블에서 실제 측정값을, 그리고 Person 테이블에서 측정자 정보를 가져와야 한다. 각 테이블이 퍼즐의 한 조각이라면, 네 조각을 모두 맞춰야 비로소 752번 방문의 전체 그림이 드러난다.
이런 다중 테이블 JOIN은 복잡해 보이지만, 실제로는 두 테이블 JOIN의 확장일 뿐이다. A와 B를 연결하고, 그 결과에 C를 연결하고, 마지막으로 D를 연결하는 단계적 과정이다. SQL은 이 모든 과정을 한 번의 쿼리로 처리해주며, 쿼리 최적화기가 가장 효율적인 연결 순서를 자동으로 결정한다.
이제 752번 방문과 관련된 모든 측정 데이터를 하나의 완전한 그림으로 만들어보자:
SELECT
id as 방문번호,
v.as 사이트명,
s.name as 위도,
s.lat long as 경도,
s.as 방문날짜,
v.dated || ' ' || p.family as 측정자,
p.personal as 측정종류,
su.quant as 측정값
su.reading FROM Visited v
INNER JOIN Site s ON v.site = s.name
INNER JOIN Survey su ON v.id = su.taken
INNER JOIN Person p ON su.person = p.id
WHERE v.id = 752;
방문번호 | 사이트명 | 위도 | 경도 | 방문날짜 | 측정자 | 측정종류 | 측정값 |
---|---|---|---|---|---|---|---|
752 | DR-3 | -47.15 | -126.72 | NULL | Anderson Lake | rad | 2.19 |
752 | DR-3 | -47.15 | -126.72 | NULL | Anderson Lake | sal | 0.09 |
752 | DR-3 | -47.15 | -126.72 | NULL | Anderson Lake | temp | -16.0 |
752 | DR-3 | -47.15 | -126.72 | NULL | Valentina Roerich | sal | 41.6 |
6.3.2 JOIN이 밝혀낸 752번 비밀
JOIN을 통해 흩어져 있던 정보 조각들을 하나로 모으자, 752번 방문의 전모가 드러났다. DR-3 사이트(남위 47.15도, 서경 126.72도)에서 Anderson Lake와 Valentina Roerich, 두 명의 탐사원이 함께 측정 작업을 진행했다. Lake는 방사선(2.19), 염분(0.09), 온도(-16.0도)를 정상적으로 기록했지만, Roerich의 염분 측정값은 41.6이라는 극단적인 수치를 보인다.
이 수치가 얼마나 이상한지 알려면 맥락이 필요하다. 일반 해수의 염분 농도가 약 3.5%인 것을 고려하면, Roerich의 41.6은 그 10배가 넘는 극한값이다. 같은 장소, 같은 시간에 Lake가 기록한 0.09와는 천지차이다. 이런 극단적 차이는 무엇을 의미할까? 지하 염수층을 발견했을 수도 있고, 특별한 지질학적 현상을 목격했을 수도 있다.
날짜가 누락된 것과 Roerich의 이상 측정값은 모두 752번 방문이 예상치 못한 특별한 순간이었음을 암시한다. 데이터가 완벽하지 않아도 JOIN을 통해 이렇게 흥미진진한 이야기를 재구성할 수 있다. 이것이 바로 현실 데이터 분석에서 JOIN의 진정한 가치다 - 불완전한 퍼즐 조각들로도 의미 있는 그림을 완성하는 것이다.
💭 생각해볼 점
우리는 이제 JOIN을 통해 탐사대의 미스터리를 해결했다. 흩어져 있던 데이터 조각들이 하나의 완전한 그림을 이루었다. 이것이 바로 현실에서 JOIN이 가져다주는 가치다 - 분산된 정보에서 의미 있는 인사이트를 발견하는 것이다.
JOIN은 단순한 기술이 아니라 데이터 스토리텔링의 핵심 도구다. Netflix의 추천 시스템, Amazon의 상품 분석, Google의 검색 알고리즘 모두 수많은 테이블들을 JOIN해서 만들어진다. 복잡해 보이는 비즈니스 인사이트도 결국은 기본적인 JOIN 연산들의 조합인 경우가 많다.
다음 장에서는 이렇게 수집된 데이터를 어떻게 새로운 테이블로 만들고 관리하는지 알아보겠다.