테이블 간 조인
테이블 간 조인 (Joins Between Tables)
지금까지 배운 쿼리는 한 번에 테이블 하나만 다뤘어요. 그런데 실제 업무에서는 여러 테이블의 데이터를 한 번에 엮어서 보고 싶을 때가 훨씬 많아요. 두 테이블의 행을 조건에 맞춰 짝지어 조회하는 방법, 바로 **조인(join)**을 배워볼게요. 예제는 앞선 챕터에서 만든 weather(날씨)와 cities(도시) 테이블을 그대로 사용해요.
조인이란 (What is a join)
지금까지 우리 쿼리는 한 번에 테이블 하나씩만 접근했어요. 하지만 쿼리는 여러 테이블을 동시에 접근할 수도 있고, 심지어 같은 테이블을 두 번 접근해 한 테이블의 여러 행을 동시에 처리하게 만들 수도 있어요. 이렇게 한 번에 여러 테이블(또는 같은 테이블의 여러 인스턴스)을 접근하는 쿼리를 **조인 쿼리(join query)**라고 불러요.
조인은 첫 번째 테이블의 행과 두 번째 테이블의 행을, "어떤 행끼리 짝지을지"를 지정하는 표현식(조건)에 따라 결합해요. 예를 들어, 모든 날씨 기록과 그 도시의 위치를 함께 보여주고 싶다면, 데이터베이스는 weather 테이블의 각 행의 city 컬럼과 cities 테이블의 모든 행의 name 컬럼을 비교해서, 값이 일치하는 행의 짝을 골라야 해요. 그건 이런 쿼리로 만들 수 있어요:
SELECT * FROM weather JOIN cities ON city = name;
결과는 이렇게 나와요. weather의 모든 컬럼 뒤에 cities의 컬럼이 이어붙은 형태랍니다:
city | temp_lo | temp_hi | prcp | date | name | location
---------------+---------+---------+------+------------+---------------+-----------
San Francisco | 46 | 50 | 0.25 | 1994-11-27 | San Francisco | (-194,53)
San Francisco | 43 | 57 | 0 | 1994-11-29 | San Francisco | (-194,53)
(2 rows)
결과 집합에서 두 가지를 눈여겨보세요:
- Hayward에 대한 결과 행이 없어요.
cities테이블에 "Hayward"와 일치하는 항목이 없기 때문이에요. 그래서 조인은weather테이블의 짝이 없는 행을 그냥 무시해요. 이 문제를 어떻게 해결하는지는 잠시 후에 볼게요. - 도시 이름이 담긴 컬럼이 두 개나 있어요.
weather테이블과cities테이블의 컬럼 목록이 그대로 이어붙었으니 당연한 결과예요. 다만 실제로는 이렇게 중복되는 게 좋지 않으니,*대신 출력할 컬럼을 직접 나열하는 편이 좋아요:
SELECT city, temp_lo, temp_hi, prcp, date, location
FROM weather JOIN cities ON city = name;
모든 컬럼 이름이 서로 달랐기 때문에 파서(parser)가 각 컬럼이 어느 테이블에 속하는지 자동으로 찾아냈어요. 만약 두 테이블에 같은 이름의 컬럼이 있다면, 어느 쪽을 뜻하는지 명확히 하기 위해 컬럼 이름을 테이블 이름으로 한정(qualify)해줘야 해요:
SELECT weather.city, weather.temp_lo, weather.temp_hi,
weather.prcp, weather.date, cities.location
FROM weather JOIN cities ON weather.city = cities.name;
조인 쿼리에서는 모든 컬럼 이름을 테이블로 한정해서 쓰는 것이 널리 좋은 스타일로 여겨져요. 나중에 어느 한 테이블에 같은 이름의 컬럼이 추가돼도 쿼리가 깨지지 않으니까요.
지금까지 본 조인 쿼리는 다음과 같은 형태로도 쓸 수 있어요:
SELECT *
FROM weather, cities
WHERE city = name;
이 문법은 SQL-92에서 도입된 JOIN/ON 문법보다 더 오래된(이전) 문법이에요. 테이블을 FROM 절에 단순히 나열하고, 비교 표현식을 WHERE 절에 넣는 방식이죠. 이 오래된 암시적 문법과 새로운 명시적 JOIN/ON 문법의 결과는 완전히 같아요. 하지만 쿼리를 읽는 사람 입장에서는 명시적 문법이 의미를 더 이해하기 쉬워요. 조인 조건이 그 자체의 키워드로 시작하니까요. 반면 이전 방식은 조건이 WHERE 절에 다른 조건들과 섞여 버려요.
외부 조인 (Outer Join)
이제 Hayward의 기록을 다시 살려내는 방법을 알아볼게요. 우리가 원하는 건 weather 테이블을 훑으면서 각 행마다 일치하는 cities 행(들)을 찾는 거예요. 만약 일치하는 행이 없다면 cities 테이블의 컬럼에 "빈 값"을 대신 넣어주는 거죠. 이런 종류의 쿼리를 **외부 조인(outer join)**이라고 불러요. 지금까지 본 조인들은 **내부 조인(inner join)**이었어요. 명령은 이렇게 생겼답니다:
SELECT *
FROM weather LEFT OUTER JOIN cities ON weather.city = cities.name;
city | temp_lo | temp_hi | prcp | date | name | location
---------------+---------+---------+------+------------+---------------+-----------
Hayward | 37 | 54 | | 1994-11-29 | |
San Francisco | 46 | 50 | 0.25 | 1994-11-27 | San Francisco | (-194,53)
San Francisco | 43 | 57 | 0 | 1994-11-29 | San Francisco | (-194,53)
(3 rows)
이 쿼리를 **왼쪽 외부 조인(left outer join)**이라고 불러요. 조인 연산자 왼쪽에 있는 테이블은 자기 행이 출력에 최소한 한 번은 등장하고, 오른쪽 테이블은 왼쪽 테이블의 어떤 행과 일치하는 행만 출력되기 때문이에요. 왼쪽 테이블의 행 중 오른쪽과 일치하는 게 없을 때는, 오른쪽 테이블의 컬럼 자리에 빈 값(null)이 대신 들어가요.
연습 문제: 오른쪽 외부 조인(right outer join)과 전체 외부 조인(full outer join)도 있어요. 각각 어떤 동작을 하는지 직접 찾아보면서 익혀보세요.
자기 조인 (Self Join)
테이블을 자기 자신과 조인할 수도 있어요. 이를 **자기 조인(self join)**이라고 해요. 예를 들어, 다른 날씨 기록의 온도 범위 안에 들어오는 날씨 기록을 모두 찾고 싶다고 해볼게요. 그러려면 각 weather 행의 temp_lo와 temp_hi를 다른 모든 weather 행의 temp_lo, temp_hi와 비교해야 해요. 다음 쿼리로 가능해요:
SELECT w1.city, w1.temp_lo AS low, w1.temp_hi AS high,
w2.city, w2.temp_lo AS low, w2.temp_hi AS high
FROM weather w1 JOIN weather w2
ON w1.temp_lo < w2.temp_lo AND w1.temp_hi > w2.temp_hi;
city | low | high | city | low | high
---------------+-----+------+---------------+-----+------
San Francisco | 43 | 57 | San Francisco | 46 | 50
Hayward | 37 | 54 | San Francisco | 46 | 50
(2 rows)
여기서는 조인의 왼쪽과 오른쪽을 구분하기 위해 weather 테이블을 w1, w2로 다시 이름붙였어요. 이런 별칭(alias)은 조인뿐 아니라 다른 쿼리에서도 타이핑을 줄이려고 쓸 수 있어요:
SELECT *
FROM weather w JOIN cities c ON w.city = c.name;
이런 축약 스타일은 아주 흔하게 마주치게 될 거예요.
더 알아보기 (Learn more)
- 집계 함수 (Aggregate Functions) — 다음 단계로 GROUP BY와 함께 쓰는 집계를 배워요
- The SQL Language 전체 문서 — 조인을 포함한 SQL 전체를 더 깊게
- tutorial-advanced: 뷰 (Views) — 자주 쓰는 조인 쿼리를 뷰로 감싸기