인덱스와 조인

기술노트

인덱스와 조인

💡 인덱스

  • 데이터를 빠르게 찾을 수 있는 하나의 장치
  • 인덱스를 설정하면 테이블 안에 내가 찾고자 하는 데이터를 빠르게 찾을 수 있다.
 - 일반적인 select 쿼리 실행 :
   1. 메모리의 database buffer cache를 체크
   2. buffer cache에는 자주 사용되는 테이블들이 캐싱되어 있어 여기에 데이터가 있을 경우 바로 찾아 출력하고 없을 경우 하드디스크에 있는 데이터 파일에서 데이터를 찾음
 - 인덱스를 사용하면 이런 과정을 거치지 않고 바로 주소를 통해 찾아간다.

💡 인덱스의 생성 원리

  • 먼저 해당 테이블을 모두 읽는다.
  • 인덱스를 만드는 동안 데이터가 변경되면 문제가 되므로 해당 데이터들이 못하도록 조치한 후 메모리(PGA의 Sort Area)에서 정렬

💡 인덱스의 동작 원리

  • 데이터 파일의 블록이 10만개가 있다고 가정할때 select문 실행시
  • 1. server process가 구문분석과정을 마친후 database buffer cache에 조건에 부합하는 데이터가 있는지 확인
  • 2. 해당 정보가 buffer cache에 없다면 디스크 파일에서 조건에 부합하는 블럭을 찾아 database buffer cache에 가져온 뒤 사용자에게 보여줌
  • 이 경우 index가 없으면 10만개 전부 database buffer cache로 복사한뒤 풀스캔으로 찾게됨
  • index가 있으면 where절의 조건의 컬럼이 index의 키로 생성되어있는지 확인한 뒤, 인덱스에 먼저 가서 조건에 부합하는 정보가 어떤 ROWID를 가지고 있는지 확인 후 ROWID에 있는 블럭을 찾아가 해당 블럭만 buffer cache에 복사

💡 B-트리

  • 인덱스는 보통 B-트리라는 자료 구조로 이루어져 있다.
  • `B-트리`는
 - `루트 노드`
   - 루트 노드와 리프 노드 사이에 있는 `브랜치 노드`
 - `리프 노드`

💡 인덱스가 효율적인 이유와 대수확장성

  • 인덱스가 효율적인 이유
 → 효율적인 단계를 거쳐 모든 요소에 접근할 수 있는 균형 잡힌 `트리 구조`와 `트리 깊이`의 대수확장성 때문이다.
  • 대수확장성 : 트리 깊이가 리프 노드 수에 비해 매우 느리게 성장하는 것
 - 기본적으로 인덱스가 한 깊이씩 증가할 때마다 최대 인덱스 항목의 수는 4배씩 증가한다.

💡 인덱스를 만드는 방법

💡 MySQL

  • 클러스터형 인덱스
 - 테이블당 하나를 설정 가능
 - `primary key` 옵션으로 기본키를 만들면 클러스터형 인덱스를 생성할 수 있고,
 - 기본키로 만들지 않고 `unique not null` 옵션을 붙이면 클러스터형 인덱스로 만들 수 있다.
  • 세컨더리 인덱스
 - 보조 인덱스로 여러 개의 필드 값을 기반으로 쿼리를 많이 보낼 때 생성해야 하는 인덱스
 - `create index...` 명령어를 기반으로 만들 수 있다.
 - 하나의 인덱스만 생성할 것이라면 클러스터형 인덱스를 만드는 것이 성능이 좋다.

> 예를 들어 age라는 하나의 필드만으로 쿼리를 보낸다면 클러스터형 인덱스만 필요하다. 하지만 age, name, email 등 다양한 필드를 기반으로 쿼리를 보낼 때는 세컨더리 인덱스를 사용해야 한다.

💡 MongoDB

  • 도큐먼트를 생성 시 자동으로 ObjectID가 형성
 → 해당 키가 기본키로 설정
  • 세컨더리키도 부가적으로 설정 가능
 → 기본키와 세컨더리키를 같이 쓰는 복합 인덱스 설정 가능!

💡인덱스의 장단점

💡 인덱스 최적화 기법

1. 인덱스는 비용이다

  • 인덱스는 두 번 탐색하도록 강요한다.
 → 인덱스 리스트, 그다음 컬렉션 순으로 탐색하기 때문이며, 관련 읽기 비용이 들게 된다.
  • 컬렉션이 수정되었을 때 인덱스도 수정되어야 한다.
 - 책의 본문이 수정되었을 때 목차나 찾아보기도 수정해야 하듯이!
 - 이때 B-트리의 높이를 균형 있게 조절하는 비용도 들고, 데이터를 효율적으로 조회할 수 있도록 분산시키는 비용도 들게 된다.
 - 그렇기 때문에 쿼리에 있는 필드에 인덱스를 무작정 다 설정하는 것은 좋지 않다.
  • 컬렉션에서 가져와야 하는 양이 많을수록 인덱스를 사용하는 것은 비효율적이다.

2. 항상 테스팅하라

  • 인덱스 최적화 기법은 서비스 특징에 따라 달라진다.
 → 서비스에서 사용하는 객체의 깊이, 테이블의 양 등이 다르기 때문이다.
  • 따라서 항상 테스팅하는 것이 중요하다.
  • explain() 함수를 통해 인덱스를 만들고 쿼리를 보낸 이후에 테스팅을 하며 걸리는 시간을 최소화해야 한다.

3. 복합 인덱스는 같음, 정렬, 다중 값, 카디널리티 순이다

  • 보통 여러 필드를 기반으로 조회를 할 때 복합 인덱스를 생성한다.
 - 이 인덱스를 생성할 때는 순서가 있고 생성 순서에 따라 인덱스 성능이 달라진다.
 - 같음, 정렬, 다중 값, 카디널리티 순으로 생성해야 한다.

💡 조인

  • 하나의 테이블이 아닌 두 개 이상의 테이블을 묶어서 하나의 결과물을 만드는 것
  • MySQL에서는 JOIN이라는 쿼리, MongoDB에서는 lookup이라는 쿼리로 이를 처리할 수 있다.
  • _but, MongoDB를 사용할 때 lookup은 되도록 사용하지 말아야 한다. MongoDB는 조인 연산에 대해 관계형 데이터베이스보다 성능이 떨어진다고 여러 벤치마크 테스트에서 알려져 있다._
  • 따라서 여러 테이블을 조인하는 작업이 많을 경우 MongoDB보다는 관계형 데이터베이스를 써야 한다.

💡 조인의 종류

![image](https://user-images.githubusercontent.com/65716445/218720672-cf8a76ac-f826-41db-bd1c-c70ae90d8ea3.png)

💡 내부 조인

  • 왼쪽 테이블과 오른쪽 테이블의 두 행이 모두 일치하는 행이 있는 부분만 표기
두 테이블 간에 교집합
sql
SELECT * FROM table1 A
INNER JOIN table2 B ON
A.key = B.key

💡 왼쪽 조인

  • 테이블 B의 일치하는 부분의 레코드와 함께 테이블 A를 기준으로 완전한 레코드 집합을 생성, 만약 테이블 B에 일치하는 항목이 없으면 해당 값은 null 값.
왼쪽 테이블의 모든 행이 결과 테이블에 표기
sql
SELECT * FROM table1 A
LEFT JOIN table2 B ON
A.key = B.key

💡 오른쪽 조인

  • 테이블 A의 일치하는 부분의 레코드와 함께 테이블 B를 기준으로 완전한 레코드 집합을 생성, 만약 테이블 A에 일치하는 항목이 없으면 해당 값은 null 값.
왼쪽 테이블의 모든 행이 결과 테이블에 표기
sql
SELECT * FROM table1 A
RIGHT JOIN table2 B ON
A.key = B.key

💡 합집합 조인

  • 합집합 조인(완전 외부 조인)
  • 양쪽 테이블에서 일치하는 레코드와 함께 테이블 A와 테이블 B의 모든 레코드 집합을 생성, 이때 일치하는 항목이 없으면 누락된 쪽에 null 값.
 → 두 개의 테이블을 기반으로 조인 조건에 만족하지 않는 행까지 모두 표기
sql
SELECT * FROM table1 A
FULL OUTER JOIN table2 B ON
A.key = B.key

💡 조인의 원리

💡 중첩 루프 조인

  • 중첩 루프 조인(NLJ, Nested Loop Join)
  • 중첩 for 문과 같은 원리로 조건에 맞는 조인을 하는 방법
 → 랜덤 접근에 대한 비용이 많이 증가하므로 대용량의 테이블에서는 사용하지 않는다.
  • 중첩 루프 조인(BNL, Block Nested Loop)
 - 중첩 루프 조인에서 발전
 - 조인할 테이블을 작은 블록으로 나눠서 블록 하나씩 조인하는 방식

💡 정렬 병합 조인

  • 각각의 테이블을 조인할 필드 기준으로 정렬하고 정렬이 끝난 이후에 조인 작업을 수행하는 조인
  • 조인할 때 쓸 적절한 인덱스가 없고 대용량의 테이블들을 조인하며, 조인 조건으로 <, >등 범위 비교 연산자가 있을 때 쓴다.

💡 해시 조인

  • 해시 테이블을 기반으로 조인하는 방법
  • 동등(=)조인에서만 사용할 수 있다.
  • 두 개의 테이블을 조인할 때 하나의 테이블이 메모리에 온전히 들어간다면 보통 중첩 루프 조인보다 더 효율적 (메모리에 올릴 수 없을 정도로 크다면 디스크를 사용하는 비용이 발생된다.)