본문 바로가기

(3)

어드민 화면 검색 성능 개선기

들어가며 가설을 세우고 실험과 분석을 반복하며 정신없이 보내던 3분기 말쯤, 생각지도 못한 시터 교육사업(이하 교육사업)의 유지보수 업무를 기존 업무 외에도 추가적으로 담당하게되었다. 그리고 올해 1월부터는 내가 소속된 패스파인더 파트는 시터사업팀에 속해서 교육사업 개발을 전담하게되었다. 시터사업팀에 합류한 시점에 교육사업팀은 기존의 ZOOM 라이브 교육대신에 새로운 버전의 VOD 교육을 준비하고있었고 기존 교육을 VOD 교육으로 바꾸기 위해서는 개발팀에서도 준비해야할 부분이 많았다. 먼저 기존 정책과는 맞지 않은 부분들을 새로운 정책에 맞춰 변경해야했고 한 회차에 30명 정원이었던 기존교육과는 달리 VOD 교육부터는 정원이 없어졌다. 한 회차에 몇백명이 교육을 신청할지 알 수 없었고 교육운영 파트분들의..

우아한테크코스 Lv4 - MYSQL 최적화 미션 (학습자료 정리)

Lv4 미션으로 쿼리 튜닝해보는 미션이 주어졌다. 먼저 본인의 쿼리 작성 능력을 테스트해볼 겸 실습 사이트에서 아래의 쿼리를 직접 작성해보자! 200개 이상 팔린 상품명과 그 수량을 수량 기준으로 내림차순으로 보이게 하라. 많이 주문한 순으로 고객 리스트(ID, 고객명), 총수량을 구하라. 많은 돈을 지출한 순으로 고객리스트를 구하라. 위 튜토리얼(?)에 대한 나의 쿼리는 아래와 같다. (주의 성능은 1도 생각안하고 결과만 제대로 나오는 쿼리다!) 1번 쿼리 SELECT p.ProductId '상품아이디', p.ProductName '상품이름', SUM(od.Quantity) '총수량' FROM Products p JOIN OrderDetails od ON od.ProductId = p.ProductId..

[MySQL] - JOIN의 종류와 사용예제

http://blog.naver.com/PostView.nhn?blogId=pjok1122&logNo=221542171040&parentCategoryNo=&categoryNo=21&viewDate=&isShowPopularPosts=true&from=search [MySQL] Inner Join(내부 조인)과 Outer Join(외부 조인) 조인에는 생각보다 다양한 조인이 존재합니다. 그 중에서 제일 대표적인 두 가지만 살펴보도록 합시다.​1.... blog.naver.com http://www.sqlprogram.com/Basics/sql-join.aspx 테이블 JOIN - SQL 프로그래밍 배우기 (Learn SQL Programming) 테이블 조인 (JOIN) SELECT문은 하나 이상의 테이..

성능개선

어드민 화면 검색 성능 개선기

728x90

들어가며

가설을 세우고 실험과 분석을 반복하며 정신없이 보내던 3분기 말쯤, 생각지도 못한 시터 교육사업(이하 교육사업)의 유지보수 업무를 기존 업무 외에도 추가적으로 담당하게되었다. 그리고 올해 1월부터는 내가 소속된 패스파인더 파트는 시터사업팀에 속해서 교육사업 개발을 전담하게되었다.

 

시터사업팀에 합류한 시점에 교육사업팀은 기존의 ZOOM 라이브 교육대신에 새로운 버전의 VOD 교육을 준비하고있었고 기존 교육을 VOD 교육으로 바꾸기 위해서는 개발팀에서도 준비해야할 부분이 많았다. 먼저 기존 정책과는 맞지 않은 부분들을 새로운 정책에 맞춰 변경해야했고 한 회차에 30명 정원이었던 기존교육과는 달리 VOD 교육부터는 정원이 없어졌다. 한 회차에 몇백명이 교육을 신청할지 알 수 없었고 교육운영 파트분들의 업무량이 늘어나는 것은 불보듯 뻔했다. 그래서 교육운영 파트분들의 업무효율을 높이기위해 어드민 개편 작업을 진행하기로 하였다.

 

어떤 부분을 개선해야할지 감이 잡히지 않았는데 평소에 운영 파트분들이 불편함을 느끼는 화면과 기능을 정리해서 전달해주셨다. 전달받은 문서를 바탕으로 어떤 부분을 개선할지 대부분 화면 개선 혹은 편의기능(중요하지 않은 정보는 안보이도록 처리, 새 창이 뜨는 형태가 아니라 팝업 형식 변경, 복제 기능 etc)들이었고 우선순위가 높은 작업들을 우선적으로 처리했다.

 

어드민 개편작업을 하면서 교육 운영업무를 볼 때, 자주 사용하는 A 검색 페이지와 B 검색 페이지에서 검색 조회 속도가 느리다는 사실을 알게 되었다. 운영파트분들이 운영업무를 보실 때, 적어도 어드민 때문에 작업이 효율이 떨어지는 일은 없었으면 하는 마음으로 주어진 업무를 일정보다 빨리 끝낸 뒤, 이 거슬리는 조회 성능을 개선하는 작업을 시작하였다.

 

문제 상황

(두 페이지 모두 결이 비슷하고 동일한 문제가 있었기 때문에 A 검색 페이지를 중점으로 설명한다)

 

A 검색 페이지는 A를 검색하기위한 여러가지 검색 필터가 있고 각 필터에 검색요소를 추가하게되면 서버단에서는 동적인 쿼리를 날려서 결과를 얻어온다. 문제점은 아래와 같았다.

  1. 아무런 검색 조건 없이 검색했을 경우 API 응답속도가 현저히 느려지는 현상
  2. 많은 결과를 한 번에 가져옴으로써 Retool이 화면을 렌더링하는 시간이 폭발적으로 늘어나는 현상

인덱스 컬럼으로 조건을 걸고 검색하면 3초 정도가 걸렸고, 아무런 조건 없이 검색하면 조회하는데 걸리는 시간이 10초를 훌쩍넘겼다.

위에서 언급한 조회 시간들은 순수하게 API 요청과 응답 시간만봤을 때, 걸리는 시간이었는데 이와는 별개로 현재 재직중인 회사에서는 Retool로 Admin 페이지를 만드는데, Retool에서 API로 가져온 데이터를 화면단에 렌더링하는데 걸리는 시간도 3~4초 정도로 꽤 걸렸다.

Retool이란...?

더보기

ChatGPT의 답변 👍

ChatGPT의 답변

이 두 가지 문제점이 동시에 발생하여 A를 검색하는데 소요되는 시간이 너무나 컸다.

성능 개선 방식

아무리 조회 성능이 좋더라도 Retool이 화면을 렌더링하는데 걸리는 시간은 데이터량에 비례한다. 따라서 한 번에 모든 A를 조회하는 지금 방식은 결국 조회속도를 느리게 만든다. 따라서 한 번에 모든 A를 조회하는 대신에 일부만 조금씩 조회하는 페이징 방식으로 변경하는 것이 좋겠다고 판단했다. 어느정도 단위로 조회하는 것이 제일 적절한지는 감이 잘 안잡혀서 우선적으로 10으로 지정했고 운영 팀이 사용하면서 불편함을 느끼면 피드백을 달라고 요청해뒀다. (페이지 사이즈를 사용자가 직접 변경할 수 있도록 할 수도 있었지만 옵션이 많으면 오히려 운영팀이 혼란을 겪을 수도 있을 것 같아서 + 페이지 사이즈를 너무 크게 잡는다거나... 등의 예상치 못한 사용 등을 생각해서 구현하지 않았다)

 

일반적인 페이징 방식인 Offset 방식을 쓸 수도 있었겠지만 이번 작업은 No Offset 방식으로 개선하는 것이 좋다고 판단했다.

 

Offset 방식은 데이터량이 적을 때는 잘 동작하지만 데이터량이 많아졌을 때는 조회 성능이 점점 떨어진다. 특히 페이지의 후반부를 조회할 때는 거의 모든 데이터를 조회하는 것과 별반 다르지 않게된다. 이는 Offset 방식은 결국 결과로 보여줘야하는 데이터를 조회할 때까지 데이터를 순차적으로 읽어들이고 불필요한 데이터들은 버린 뒤에 필요한 데이터만 취해서 가져오기 때문이다. 즉 같은 데이터를 또 읽고 또 읽는 방식이기에 데이터가 누적되면 누적될수록 느려진다. A 테이블의 데이터는 매일 꾸준히 상당히 많은 데이터가 누적되고 있기에 (이 문제를 해결한 시점과 지금 시점에서도 총 row 수가 거의 2배가 되었다) Offset 방식으로 개선하는 것은 임시방편일 뿐이지 언제 터질지 모르는 시한폭탄과도 같다고 생각했다.

 

Offset 방식이 아닌 No Offset 방식을 취하게 될 경우에는 화면단의 변경도 강제되는데, 우리가 일반적으로 아는 게시판에서 페이지 숫자를 클릭해서 한 번에 특정 페이지로 이동하는 아래와 같은 화면단을 사용할 수 없다.

페이지 번호 방식 (Offset)

No Offset을 사용하게되면 페이지 번호가 표기되는 방식이 아니라 아래와 같이 더보기(or 무한 스크롤) 방식을 사용할 수 밖에 없다.

image

무한 스크롤 방식 (No Offset)

A 검색 페이지를 더보기 방식으로 변경하는 것이 운영하는데 더 불편하다면 조금 조회시간이 걸리더라도 Offset 방식을 택하는게 더 나을 수도 있다. 하지만 A 검색 페이지의 대다수의 작업은 최신 데이터만 조회하여 처리하는 경우가 많기 때문에 과거의 데이터를 보는 경우도 많지 않았다. 따라서 더보기 방식으로 변경하는데 큰 불편함이 없을거라고 판단했고 Retool로 구현된 A 검색 페이지를 페이지 번호 방식에서 더 보기 방식으로 변경하였다.

어려웠던 점

No Offset 방식에 대한 설명은 동욱님의 블로그에 잘 정리되어있어서 백엔드 단에서는 작업이 어렵지 않았다. 오히려 Retool이 테이블의 더 보기 방식을 지원하지 않은 탓에 직접 구현해야했는데, 이 프론트 작업(?)이 생각보다 쉽지 않았지만 어찌저찌 의도한대로 더보기 방식으로 A 데이터들을 조회하는데 성공했다. 다음 블로그 글감으로는 'Retool에서 더보기 방식 구현하기'를 해볼까 싶다. 🤔

 

결과

'조회 시간'은 Retool에서 결과로 보여준 값이고 'API 응답시간' + 'data 변환 시간' + '화면을 렌더링하는 시간'을 포함한 값이다.

(1)A 조회 성능 개선

총 A 테이블 row 수: 약 17,000

항목 AS IS TO BE
조회 시간 image image
예시 화면 페이지 번호 방식 더보기 방식

(2) B 조회 성능 개선

총 B 테이블 row 수: 약 9,000 건

항목 AS IS TO BE
조회 시간 image image
화면 방식 페이지 번호 방식 더보기 방식
728x90
카테고리 없음

우아한테크코스 Lv4 - MYSQL 최적화 미션 (학습자료 정리)

728x90

Lv4 미션으로 쿼리 튜닝해보는 미션이 주어졌다.

먼저 본인의 쿼리 작성 능력을 테스트해볼 겸 실습 사이트에서 아래의 쿼리를 직접 작성해보자!

  1. 200개 이상 팔린 상품명과 그 수량을 수량 기준으로 내림차순으로 보이게 하라.
  2. 많이 주문한 순으로 고객 리스트(ID, 고객명), 총수량을 구하라.
  3. 많은 돈을 지출한 순으로 고객리스트를 구하라.

위 튜토리얼(?)에 대한 나의 쿼리는 아래와 같다. (주의 성능은 1도 생각안하고 결과만 제대로 나오는 쿼리다!)

1번 쿼리

SELECT p.ProductId '상품아이디', p.ProductName '상품이름', SUM(od.Quantity) '총수량'
FROM Products p
JOIN OrderDetails od 
ON od.ProductId = p.ProductId
JOIN Orders o
ON o.OrderId = od.OrderId
GROUP BY p.ProductId
HAVING SUM(od.Quantity) >= 200
ORDER BY SUM(od.Quantity) DESC

1번 결과

image

2번 쿼리

SELECT c.CustomerID 'ID', c.CustomerName '고객이름', SUM(od.Quantity) '주문량'
FROM OrderDetails od
JOIN Orders o ON od.OrderId = o.OrderId
LEFT JOIN Customers c ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerId
ORDER BY SUM(od.Quantity) DESC

2번 결과

주의 해당 쿼리 결과는 틀렸다! 총 91건의 데이터가 나와야한다.

image

총 주문량이 0건인 고객도 있기 때문에 고객을 기준으로 JOIN을 해줘야한다. (고객 테이블이 왼쪽에 있도록해서 LEFT JOIN 시켰다.)

수정 후 (총 91건의 데이터)

SELECT c.CustomerID 'ID', c.CustomerName '고객이름', SUM(od.Quantity) '주문량'
FROM Customers c 
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
LEFT JOIN OrderDetails od ON od.OrderId = o.OrderId
GROUP BY c.CustomerId
ORDER BY SUM(od.Quantity) DESC

3번 쿼리

SELECT c.CustomerID 'ID', c.CustomerName '고객이름', SUM(od.Quantity) '총수량', SUM(od.Quantity * p.Price) '지출금액'
FROM Customers c 
LEFT OUTER JOIN Orders o ON c.CustomerID = o.CustomerID
LEFT OUTER JOIN OrderDetails od ON od.OrderId = o.OrderId
LEFT OUTER JOIN Products p ON p.ProductID = od.ProductId
GROUP BY c.CustomerId
ORDER BY SUM(od.Quantity * p.Price) DESC

3번 결과

image

SQL 기본

기본 내용 중에서 잘 모르는 내용만 기록

SELECT * FROM Orders
JOIN OrderDetails ON Orders.OrderID = OrderDetails.OrderID
WHERE Orders.OrderID IN (1, 30)
  • OrderID이 1, 30인 데이터를 검색하기 위해 Orders 테이블을 먼저 찾아본다.
    • 그 이유는 테이블에는 동시에 접근할 수 없기 때문이다.
    • 이처럼 먼저 접근하는 테이블을 드라이빙 테이블, 검색 결과를 통해 뒤늦게 검색하는 테이블을 드리븐 테이블이라고 한다.
  • 가능하면 적은 결과가 반환될 것으로 예상되는 테이블을 드라이빙 테이블로 선정해야 한다. 드라이빙 테이블의 추출 건수가 곧 드리븐 테이블의 액세스 반복 횟수가 되기 때문이다.

DB 최적화 대상은...?

DB에서 최적화를 할 요소는 총 세가지가 있는데, Client, Database Engine, Filesystem이다.

  • Client
    • 복수 건의 레코드를 한 번의 호출로 처리하거나, 여러개의 쿼리를 한 쿼리로 통합 처리함으로써 호출 수를 줄일 수 있다.
    • Java 진영에서 DB를 조작하는 JDBC Statement는 쿼리 문장 분석, 컴파일, 실행의 단계를 캐싱한다.
      • 반면에 PreparedStatement는 처음 한 번만 세 단계를 거친 후 캐시에 담아서 재사용한다. (따라서 가급적이면 PreparedStatement를 쓰는게 좋다)
    • DB Connection Pool을 사용하여 객체를 생성하는 부분에서 발생하는 대기시간을 줄이고 네트워크 부담을 줄일 수 있다.
    • Fetchsize 조정하거나 Paging을 활용한다.
  • Database Engine
    • 파일시스템에 저장된 데이터가 조회되면 해당 데이터를 메모리에 저장해 이후 동일 데이터 조회 시 파일시스템의 물리적인 입출력이 발생하지 않도록 한다.
    • 서버 파라미터를 튜닝한다.
  • FileSystem
    • SSD를 사용
    • SQL을 최적화하여 필요이상의 데이터 블록을 읽는 것을 방지한다.
      • SQL 튜닝이란 읽는 블록 수를 줄여주는을 의미한다.

참고해볼 자료: 쿼리 최적화: 빠른 쿼리를 위한 7가지 체크리스트

그렇다면 SQL 최적화의 대상은 무엇인가? 🤔

쿼리 동작 방식

  1. Query Caching
    SQL문이 Key, 쿼리의 실행결과가 Value인 Map이다. 정해진 확인절차를 거쳐 쿼리 캐시의 결과를 반환한다.

확인절차

  • 요청된 쿼리 문장이 쿼리 캐시에 존재하는지
  • 해당 사용자가 그 결과를 볼 수 있는 권한이 있는지
  • 트랜잭션 내에서 실행된 쿼리라면 가시 범위 내에 있는 결과인지
  • 호출 시점에 따라 결과가 달라지는 요소(RAND, CURRENT_DATE 등)가 있는지
  • 캐시가 만들어지고 난 이후 해당 데이터가 다른 사용자에 의해 변경되지 않았는지
  • 쿼리에 의해 만들어진 결과가 캐시하기에 너무 크지 않은지
  1. Parsing
    사용자로부터 요청된 SQL을 잘게 쪼개서 서버가 이해할 수 있는 수준으로 분리한다.
  1. Preprocessor
    쿼리가 문법적으로 틀리지 않은지 확인하여 부정확한 경우 여기서 처리를 중단한다. (batch와 같은 일괄처리 내에 있다면 일괄처리 전체를 중단)
  1. Optimization
    실행계획(Execution Plan)이란, 이 단계에서의 출력을 의미한다.
* 쿼리 분석: Where 절의 검색 조건인지, Join 조건인지 판단
* 인덱스 선택: 각 테이블에 사용된 조건과 인덱스 통계 정보를 이용해 사용할 인덱스를 결정
* 조인 처리: 여러 테이블의 조인이 있는 경우 어떤 순서로 테이블을 읽을지 결정 
  1. Handler (Storage Engine)
    MySQL 실행엔진의 요청에 따라 데이터를 디스크로 저장하고 디스크로부터 읽어오는 역할을 담당. MySQL 엔진에서는 스토리지 엔진으로부터 받은 레코드를 조인하거나 정렬하는 작업을 수행

Sequential Access vs Random Access

Index Range Scan vs Table Full Scan

DB 테이블에서 데이터를 찾는 방법은 두 가지가 있다.

  • 테이블 전체를 스캔 (= Table Full Scan)
    • Sequential Access와 Multiblock I/O 방식으로 디스크를 읽어 한 블록에 속한 모든 레코드를 한번에 읽는다.
  • 인덱스를 이용 (= Index Range Scan)
    • Random Access와 Single Block I/O로 레코드 하나를 읽기 위해 매번 I/O가 발생한다.

위 두 특징을 통해서 알 수 있는 사실은 읽을 데이터가 일정량을 넘으면 Index 보다 Table Full Scan이 유리하다는 것이다.

그렇다면 우리의 목표는...?

  • Index를 사용할 경우 Random I/O 횟수를 줄이는 것이다.

인덱스 탐색과정은 스캔 시작지점을 찾는 수직적 탐색과 데이터를 찾는 수평적 탐색, 두 가지로 나눌 수 있다. (cf. MySQL InnoDB의 경우 B-Tree 인덱스 구조가 기본이다)

(이미지 참조: https://simpledb.tistory.com/22)

수직적 탐색

  • 인덱스 수직적 탐색은 루트 노드에서부터 시작한다.
  • 루트 노드와 브랜치 노드는 인덱스 키노드 정보로 구성된 페이지 단위이다.
  • 수직적 탐색 과정에 찾고자 하는 값보다 크거나 같은 값을 만나면, 바로 직전 레코드가 가리키는 하위 노드로 이동한다.

수평적 탐색

  • 수직적 탐색을 통해 스캔 시작점을 찾았으면 수평적 탐색을 통해 데이터를 찾는다.
  • 인덱스 리프노드끼리는 양방향 LinkedList이므로 서로 앞뒤 블록에 대한 주소값을 갖는다.
    • 필요한 컬럼을 인덱스가 모두 갖고있는 경우 인덱스만 스캔하고 끝난다.
    • 그렇지 않은 경우에는 테이블도 액세스해야한다.
    • 이때, ROWID가 필요하다.
      • ROWID는 데이터블록 주소 + 로우 번호(블록내 순번)으로 구성된다.
      • cf. InnoDB의 경우, Primary Key가 ROWID 역할을 한다. (PK는 Clustered Index로 순차적으로 저장되어있어 Data Record의 물리적인 위치를 알 수 있기 때문)
      • ROWID가 가리키는 데이터 페이지를 버퍼풀에서 먼저 찾아보고 못찾을 때만 디스크에서 블록을 읽는다. (읽은 후에는 버퍼풀에 적재한다)
        • 잘 생각해보면 이 요소는 JPA의 영속성 컨텍스트와 유사해보인다. 아니 사실은 JPA 영속성 컨텍스트가 DB에 유사하게 구현됐다는 표현이 맞는 듯 하다.

인덱스 튜닝

인덱스 튜닝은 크게 2가지로 인덱스 스캔 효율화랜덤 엑세스 초기화이 있다.
(랜덤 엑세스 초기화는 인덱스 스캔 후 테이블 레코드를 액세스할 때, 랜덤 I/O 횟수를 줄이는 것을 의미한다.)
랜덤액세스 최소화 튜닝이 더 중요하다.

인덱스 손익분기점

1건을 조회하든, 1000만건을 다 조회하든 Table Full Scan의 성능은 일정하게 유지된다.
하지만 인덱스를 이용해 테이블을 액세스할 경우에는 랜덤 액세스 때문에 점점 느려진다.
Table Full Scan은 Sequantial Acess인 반면, 인덱스 ROWID를 이용한 테이블 액세스는 랜덤 액세스 방식이기 때문이다.

(테이블 데이터가 10~100만건 이내의 경우 5 ~ 20% 가량에서 손익분기점이 형성된다고 함)
조회건수가 늘수록 데이터를 버퍼캐시에서 찾을 가능성이 낮아진다.

DB 서버 튜닝

  • 메모리 튜닝
    • Thread
      • thread_cache_size
    • Caching
      • innodb_buffer_pool_size
  • 커넥션 튜닝
    • connect_timeout
    • interactive_timeout
    • wait_timeout
    • max_connections
    • back_log

참고자료

우아한테크코스 Lv4 - 조회 성능 개선하기 수업자료
[파일구조]- 레코드 접근과 성능
MySQL 다시 공부하기(1)

728x90
Infra/DataBase

[MySQL] - JOIN의 종류와 사용예제

728x90

http://blog.naver.com/PostView.nhn?blogId=pjok1122&logNo=221542171040&parentCategoryNo=&categoryNo=21&viewDate=&isShowPopularPosts=true&from=search

 

[MySQL] Inner Join(내부 조인)과 Outer Join(외부 조인)

조인에는 생각보다 다양한 조인이 존재합니다. 그 중에서 제일 대표적인 두 가지만 살펴보도록 합시다.​1....

blog.naver.com

http://www.sqlprogram.com/Basics/sql-join.aspx

 

테이블 JOIN - SQL 프로그래밍 배우기 (Learn SQL Programming)

테이블 조인 (JOIN) SELECT문은 하나 이상의 테이블로부터 데이타를 가져올 수 있다. 복수의 테이블로부터 데이타를 가져오기 위해서는, FROM절 뒤에 복수의 테이블을 나열하면 된다. 이때, 테이블들이 서로 연관을 맺고 이들로부터 특정 조건의 레코드셋들을 선별하게 되는데, 이를 테이블 조인이라 한다. 테이블 조인에는 Cross Join, Inner Join, Outer Join 등이 있으며, Cross Join은 테이블A의 모든 레코드들에 대해 테이

www.sqlprogram.com

위 글들을 참고하였습니다. 

현재 진행중인 프로젝트에서 테이블을 3개, 4개 join하라는 요구조건이 있었다. SQL로 간단한 CRUD 정도만 진행하다보니 join의 개념을 잊고있었다. (또 이렇게 되새김질 하는 것 아니겠는가? 부디 이번에는 오랫동안 기억에 남기를 바란다

테이블 조인(Table Join)

SELECT문은 하나 이상의 테이블로부터 데이터를 가져올 수 있다. 복수의 테이블로부터 데이타를 가져오기 위해서는, FROM절 뒤에 복수의 테이블을 나열하면 된다. 이때, 테이블들이 서로 연관을 맺고 이들로부터 특정 조건의 레코드셋들을 선별하게 되는데, 이를 테이블 조인이라 한다. 테이블 조인에는 Cross Join, Inner Join, Outer Join 등이 있으며, Cross Join테이블A의 모든 레코드들에 대해 테이블B의 모든 레코드가 맴핑되는 것으로, 테이블A에 10개 레코드가 있고, 테이블 B에 5개 있다면, 총 10 x 5 = 50개의 레코드를 출력한다. Inner Join테이블A의 특정 컬럼값테이블B의 지정 컬럼값일치하는 레코드만을 선별하는 경우에 사용된다. Outer JoinInner Join과 마찬가지로 조인 조건을 만족하는 레코드들을 출력하지만, 추가로 일치하지 않는 레코드들도 함께 출력해 준다.

 

Inner Join

Inner Join은 복수 테이블들이 조인 조건을 모두 만족하는 레코드들만 선별하는 조인이다.
아래와 같이 두가지 방법으로 표현할 수 있다. 

(1) Inner Join...On 문
FROM TabA INNER JOIN TabB ON TabA.Col = TabB.Col

(2) FROM...WHERE절에서 조인 조건식 사용
FROM TabA,TabB WHERE TabA.Col = TabB.Col

 

예제

더보기

-- Orders 테이블과 OrderDetails 테이블을
-- Order ID로 조인한 예
-- 두 테이블에 동일 컬럼이 있는 경우
-- 테이블명을 명시적으로 사용한다.
SELECT Orders.ID, OrderDetails.*
FROM Orders INNER JOIN OrderDetails
ON Orders.ID = OrderDetails.ID
WHERE Orders.OrderDate >= '1/1/2012'

-- 테이블 Alias를 사용하여
-- 위의 문장을 간략히 다음처럼 표시
SELECT o.ID, d.*
FROM Orders o INNER JOIN OrderDetails d
ON o.ID = d.ID
WHERE o.OrderDate >= '1/1/2012'

 

Cross Join

Cross Join은 복수 테이블의 모든 레코드를 M x N 식으로 모두 출력하는 것으로, 만약 테이블A가 10개의 레코드가 있고, 테이블B가 5개의 레코드가 있다면, 10 x 5 레코드 즉 총 50개의 조합을 출력한다. Cross Join은 특별한 조인 조건을 지정하지 않고, 복수 테이블명을 FROM절에 나열하면 된다. (이해가 쉽다고 판단하여 따로 예제는 안들겠다)

SELECT * FROM Class, Score

 

Outer Join

복수 테이블에 모든 조인 조건이 만족되지 않더라도 한 테이블의 모든 데이터는 출력되게 하려면 Outer Join을 사용한다. 매칭 데이터가 없는 부분은 NULL로 표시될 것이다. Outer Join은 모든 데이터를 출력하는 테이블의 위치에 따라 Left Outer Join, Right Outer Join, 그리도 양쪽 테이블의 모든 데이터가 기본적으로 출력되는 Full Outer Join이 있다.

예제

더보기

-- City명은 매출여부와 상관없이
-- 출력. 매출없는 경우 합계란은 NULL
--
SELECT c.Name, SUM(s.Total) '합계'
FROM City c LEFT OUTER JOIN Sales s
ON c.Code = s.CityCode
GROUP BY c.Name


-- Outer Join 결과 예 --

Name                 합계
-------------------  -----------
서울                   264
부산                   238
대전                   232
제주                   NULL

 

Self Join

(위에서 언급하지는 않았지만 이런 Join도 존재한다)
Self Join이란 FROM절에 하나의 테이블을 Table Alias를 통하여 두번 참조한 후, 자신의 테이블을 마치 다른 테이블인 것 처럼 조인하는 것이다. 아래 예제는 Self Join을 통해 Emp 테이블에서 Boss가 Kim인 모든 종업원을 출력하는 예이다.

예제

더보기

SELECT e.EmpID, e.Name, b.Name AS Boss
FROM Emp e, Emp b 
WHERE e.MgrId = b.EmpId
AND b.Name = 'Kim'

 

 

3개 이상의 Table을 Join 해야할 경우 (다중 테이블 join)

SELECT ST.STADIUM_NAME, SC.STADIUM_ID, SCHE_DATE, HT.TEAM_NAME, AT.TEAM_NAME, HOME_SCORE, AWAY_SCORE 
FROM SCHEDULE SC 
        JOIN STADIUM ST 
                ON SC.STADIUM_ID = ST.STADIUM_ID 
        JOIN TEAM HT 
                ON SC.HOMETEAM_ID = HT.TEAM_ID 
        JOIN TEAM AT 
                ON SC.AWAYTEAM_ID = AT.TEAM_ID 
WHERE HOME_SCORE >= AWAY_SCORE +3

(테이블에 대한 구체적인 소개는 따로 안하겠다. 그냥 3개의 테이블(SCHEDULE, STADIUM, TEAM)이 있다는 사실만 파악하면 된다 마지막에 self join을 써서 같은 테이블을 사용했다)

 

원래 WHERE 조건절로 각 테이블을 연결시켜주는 column의 동등 조건을 나열하면서 (수동적인) join을 진행하려고 하였는데... 지나가면서 본 내용중에 성능적인면에서 WHERE로 join 결과를 만들어 내는 것은 조회 횟수가 많기 때문에(결국 모든 데이터를 조회 해야하기 때문이라고 했던 것 같다) 성능적인 측면에서 join이 더 좋다고 했던 것 같다. (나를 너무 신뢰하지는 말아줬으면 한다) 

728x90