✅ 조인이 필요한 이유
현재 테이블이 users
products
orders
(사용자, 상품, 주문) 테이블이 있다고 가정
여기서 최근 주문현황의 고객 이름과 상품명을 포함해서 보고서로 만들어달라는 요구사항이 생김
주문현황이기 때문에 orders
테이블을 조회했더니 아래와 같이 나온다
order_id | user_id | product_id | order_date | quantity | status |
---|---|---|---|---|---|
1 | 1 | 1 | 2025-06-10 | 10:00:00 | 1 |
1 | 1 | 4 | 2025-06-10 | 10:05:00 | 2 |
하지만 위 표에서는 고객 이름과 상품명이 없다
user_id 가 1인 고객이 누구인지, product_id 가 1인 상품이 무엇인지 알 수 없다
그러면 orders
테이블 안에 모든 데이터를 저장하면 되는것이 아닌가?
order_id | order_date | user_name | user_email | product_name | price | 데이터 많아서 생략... |
---|---|---|---|---|---|---|
1 | 2025-06-10 | 션 | sean@... | 삼성키보드 | 1 | 데이터 많아서 생략... |
이런식으로 하면 당장 조회할 때는 편하지만 많은 문제점이 있다
1
: 데이터 중복
션이라는 사람이 상품을 100번 주문하면 그 사람의 이메일과 이름 등이 100번이나 불필요하게
반복 저장된다.
2
: 갱신 이상
션이라는 사람이 이메일 주소를 변경하면, 션이 주문한 모든 데이터를 모두 찾아서
이메일 정보를 일일이 새로운 정보로 변경해야한다.
하지만 만약 실수로 하나라도 누락된다면 주문마다 주소가 다르기 때문에 데이터의 일관성이 깨진다.
3
: 삽입 이상
쇼핑몰에 아직 아무도 주문하지 않은 노트북을 등록하려고 하는데, 위 테이블 구조에서는
주문이 발생해야만 데이터를 추가할 수 있다. 하지만 주문한 사람이 없어서
상품 정보조차 등록할 수 없는 상황이 발생된다
4
: 삭제 이상
션이 주문한 기록 하나를 삭제한다고 가정하자. 만약 이 주문 기록을 삭제하면
션이라는 고객의 이름, 이메일, 주소 정보까지 삭제되는 현상이 발생한다
이러한 문제들 때문에 데이터베이스를 설계할 때 정규화
라는 과정을 거친다
💡 정규화
-> 데이터의 일관성을 해치는 '이상 현상'들을 방지하기 위해 데이터를 논리적인 단위로 분리하는 과정
위에서 users
products
orders
테이블로 나눈 것이 정규화의 예시이다
이렇게 분리한 테이블에서 흩어진 데이터에서 내가 필요한 데이터들을 다시 갖고오려면 (통합)
조인
을 사용해야한다.
✅ 내부조인
내부 조인( INNER JOIN )은 두 테이블을 연결할 때
양쪽 테이블에 모두 공통으로 존재하는 데이터만을 결과로 보여줌
* (전체)
로 조회하면 조인한 모든 테이블의 데이터들이 합쳐져서 모두 출력된다
WHERE
조건 뒤에 적을때 or 조회대상 컬럼은 컬럼 앞에 테이블명을 정확하게 명시해주는것이 좋다
ex) WHERE P.PRODUCT_ID = 1
SELECT O.ORDER_ID ....
SELECT 컬럼1, 컬럼2, ...
FROM 테이블A // FROM : 기준이 되는 첫 번째 테이블을 지정
INNER JOIN 테이블B // INNER JOIN : 연결할 두 번째 테이블을 지정
ON 테이블A.연결컬럼 = 테이블B.연결컬럼; // ON : 두 테이블을 어떤 조건으로 연결할지 명시하는 연결고리
JOIN
을 통해 여러 테이블을 먼저 합친 가상의 테이블을 만든 후
WHERE
절의 조건에 따라 필요한 행을 걸러내고 최종적으로 원하는 필드를 SELECT
하는 순서로 동작
💡 FROM/JOIN (테이블결합) -> WHERE(조건 필터링) -> SELECT (컬럼선택)
내부 조인
은 벤 다이어그램에서 둘의 겹친 영역 (내부영역) 인 교집합 영역을 의미한다
결국 두 테이블의 교집합
을 찾는 것과 같다
두 집합(테이블) 에서 연결 컬럼의 값이 일치하는 데이터만을 결과로 반환한다
A집합과 B집합에 모두 포함된 값이 해당하는 데이터만을 결합하여 보여줌
ex) orders 테이블 : {1,2,3,4,5} users 테이블 : {1,2,3,4,5,6}
사진에서 보는 것처럼 users 테이블에는 6번 유저가 있지만, 한번도 주문한 적이 없어서
orders 테이블에는 6이 존재하지 않는다. 따라서 6번 유저는 INNER JOIN (내부조인) 결과에서 제외됨
내부 조인은 양방향
이기 때문에
A -> B 조인하든, B -> A로 조인하든 그 결과는 항상 동일하다
어떤 순서로든 조인해도 상관없지만 실무에서는 가독성을 높이기 위해서 다음과 같이 순서를 정한다
주문 목록
을 중심으로 고객 정보를 추가하고 싶다면 -> FROM orders JOIN users
고객 목록
을 중심으로 주문 정보를 조회하고 싶다면 -> FROM users JOIN orders
즉 쿼리를 읽는 사람의 입장에서 어떤 데이터가 중심이 되는가에 따라서 정하면 된다
가독성을 높이려면 테이블 별칭
과 컬럼 별칭
을 사용하는 것이 좋다
실무에서는 테이블 별칭
은 AS를 생략하고, 컬럼 별칭
은 AS를 써주는것이 좋다
INNER JOIN으로 풀네임으로 적지 않고 JOIN 으로만 작성해주는것이 편하고 좋다
SELECT
u.user_id AS u_user_id
o.user_id AS o_user_id
o.order_date
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
내부조인으로는 주문기록이 없는 고객을 찾을 수 없다
그 이유는 내부조인은 양쪽 테이블에서 짝이 맞는 데이터들을 연결하기 때문이다
그럴때는 외부조인을 사용해야한다
✅ 외부조인 (OUTER JOIN)
한쪽에만 데이터가 있고, 다른 한쪽에는 없는 데이터까지 모두 포함해서 보고싶을 때 외부조인을 사용
쉽게말해서 외부조인은 한쪽 테이블에만 존재하는 데이터를 결과에 포함시킬 수 있다
기준이 되는 테이블이 어느쪽이냐에 따라서 LEFT OUTER JOIN
과 RIGHT OUTER JOIN
으로 나눈다
교지합 영역 + 기준이 되는 테이블의 영역을 합친것이 결과에 포함된다
양쪽 모두를 사용하는 풀 외부조인
이라는 방법도 있는데, 실무에서는 잘 사용하지 않으며
UNION ALL를 사용하는 편이다
풀 외부조인
은 MYSQL 에서는 지원하지 않는다
✅ LEFT OUTER JOIN 과 RIGHT OUTER JOIN
사용할 때는 OUTER
명령어는 생략하는것을 권장한다
LEFT OUTER JOIN
-> LEFT JOIN
RIGHT OUTER JOIN
-> OUTER JOIN
LEFT JOIN
LEFT JOIN
구문의 왼쪽(FROM 절)에 있는 테이블이 기준이 된다RIGHT JOIN
은 위 방법과 반대로 동작한다
위에서 주문기록이 없는 고객을 찾을 때 내부조인으로 찾지 못했는데
OUTER JOIN으로는 찾을 수 있게되었다
SELECT *
FROM users u
LEFT JOIN orders o // JOIN을 기준으로 왼쪽 테이블이 기준 (유저 테이블이 기준)
ON u.user_id = o.user_id;
아래 사진과 같이 레오나르도 다빈치는 주문기록이 없지만, USERS 테이블이 기준이기 때문에
모든 행들을 포함하고 주문기록이 없기 때문에 NULL로 채워져 있다
만약 위 예제를 RIGHT 조인을 사용하면 테이블의 위치만 변경해주면 동일한 결과가 생긴다
실무에서는 LEFT JOIN
이 RIGHT JOIN
보다 훨씬 더 많이 사용된다
그 이유는 분석의 기준이 되는 테이블을 먼저 사용하고 (사람은 왼쪽에서 -> 오른쪽으로 글을 읽음)
필요한 정보를 담은 테이블들을 LEFT JOIN
으로 하나씩 붙여나가는 방식으로 쿼리를 작성하는 것이 직관적이기 때문이다
조인을 하다보면 행이 늘어날 때도 있는데, 각 테이블의 관계에 따라 다르다
행 개수 유지
자식테이블
JOIN 부모테이블
행 개수 증가 가능
부모테이블
JOIN 자식 테이블
✅ 셀프조인
JOIN
기법의 명령어가 아님EX) 조직도, (카테고리, 서브카테고리), 게시판 원본글과 답변글 같은 계층형
데이터를 다룰 때 사용함
다음 EMPLOYEES
테이블 예시를 보자
이 테이블에는 모든 직원의 정보가 들어있다. 각 직원의 상사를 찾으려고 하는데
각 직원의 상사도 직원이다. 이럴 경우 자기 자신의 테이블 조인하면 된다
select e.name as '직원이름',
m.name as '상사이름'
from employees e // 하나의 테이블에 서로 다른 별칭을 두개 부여하여, 마치 두개의 테이블인것처럼 인식
join employees m on e.manager_id = m.employee_id;