티스토리 뷰

조회 최적화를 위한 인덱스

Intro. 왜 인덱스를 써야 할까?


우린 주로 데이터를 메모리와 디스크에 저장한다.
그 중에서도 일반적으로 데이터베이스의 저장된 데이터는 실제로 디스크에 저장 된다

모두가 알다시피 디스크는 메모리에 비해 느리지만, 영속성과 가격 측면에서 유리함을 가지고 있기 때문이다

우리가 고민해야 할 점은 결국 디스크에 데이터에 저장해야 하는데 어떻게 효율적으로 데이터를 읽고 쓰냐이다

디스크에 접근하는 방식에 따라서 크게 랜덤 I/O와 순차 I/O 2가지 방법으로 나뉩니다

순차 접근은 연속된 블록의 데이터를 가져오는 것이며, 랜덤 접근은 특정 위치에 있는 데이터를 가져오는 것입니다.

순차 접근은 데이터를 연속적으로 읽거나 쓰는 작업에서 효율적이지만, 랜덤 접근은 특정 위치의 데이터에 접근해야 하므로 성능이 저하될 수 있습니다.

디스크는 메모리에 비해 접근 속도가 느리기 때문에 랜덤 I/O 작업은 성능에 부담을 주는 요소입니다.

따라서 데이터베이스의 성능을 향상시키기 위해서는 랜덤 I/O를 최소화하는 것이 중요합니다.

그래서! Index(인덱스)를 사용합니다

인덱스는 데이터베이스에서 특정 컬럼(또는 컬럼들)에 대한 정렬된 데이터 구조로, 이를 통해 탐색 범위를 최소화 합니다

예를 들어, 특정 컬럼에 대한 조회 작업을 수행할 때 인덱스가 존재한다면, 데이터베이스는 인덱스를 이용하여 해당 컬럼의 값을 검색하여 랜덤 I/O 없이 원하는 데이터를 찾을 수 있습니다.

반면, 인덱스가 없다면 전체 데이터를 순차적으로 검색해야 하므로 랜덤 I/O가 발생하게 되고, 성능이 저하될 수 있습니다.

인데스의 자료 구조

인덱스의 핵심은 탐색 범위를 줄이는 것입니다.
이를 위한 자료구조들은 여러 가지가 있죠!

Hash Map

  • 단건 검색 속도 O(1)
  • 하지만, 범위 탐색은 O(N)
  • 전방 일치 탐색 불가 ex)like ‘AB%’
  • 메모리 데이터베이스에 사용 되기도 한다ㅓ

List

  • 정렬 되지 않은 리스트의 탐색은 O(N)
  • 정렬된 리스트의 탐색은 O(logN)
  • 정렬 되지 않은 리스트의 정렬 시간 복잡도는 O(N) ~ O(N * logN)

Tree

  • 트리 높이에 따라 시간 복잡도가 결정 된다
  • 트리의 높이를 최소화 하는 것이 중요하다
  • 한쪽으로 노드가 치우치지 않도록 균형을 잡아주는 트리를 사용한다
    • ex) Red-Black Tree, B+ Tree
    • 바이너리 검색을 한다는 조건 아래에서 높이에 따라 시간 복잡도가 겨 ㄹ정이 된다
      B+ Tree
  • 삽입/삭제시 항상 균형을 이룸
  • 하나의 노드가 여러 개의 자식 노드를 가질 수 있음
  • 리프 노드에만 데이터 존재, 즉 연속적인 데이터 접근 시 유리하다

예제. MySQL

MySQL 특징에 따른 사전 지식을 알고 예제를 맛있게 먹어보자!

사전 지식

  • 클러스터 인덱스는 데이터 위치를 결정하는 키 값이다
  • MySQL의 PK는 클러스터 인덱스다
  • MySQL에서 PK를 제외한 모든 인덱스는 PK를 가지고 있다

MySQL에서는 모든 PK (Primary Key)를 가지고 있습니다.
PK는 클러스터 인덱스로 설정되어 있으며, 따라서 인덱스는 PK를 가지고 있게 됩니다.

이는 세컨더리 인덱스만으로는 데이터를 찾아갈 수 없다는 것을 의미합니다. 항상 PK 인덱스를 검색해야 합니다.

이에 따른 장점으로 PK를 활용한 검색이 빠르다는 점이 있습니다.
특히 범위 검색에서 성능이 우수합니다.

또한, 세컨더리 인덱스들이 PK를 가지고 있기 때문에 커버링이 가능합니다.
이는 인덱스 테이블 단에서만 조회하여 요청이 가능하다는 것을 의미합니다.

또한, PK가 정렬된 곳에 데이터가 모여 있기 때문에, 공간적 캐시의 이점을 가집니다.
이는 데이터에 접근할 때 빠른 속도를 제공할 수 있습니다.

실습


위의 쿼리가 데이터가 매우매우 많아짐에 따라 어떻게 변화하는지 알아보자

먼저 많은 데이터를 입력해주자

  • Easy Random으로 입력할 파라미터를 생성한다
    public static EasyRandom get(Long memberId, LocalDate start, LocalDate end) {
        EasyRandomParameters parameter = getEasyRandomParameters();
        parameter
                .randomize(memberId(), () -> memberId)
                .randomize(createdDate(), new LocalDateRangeRandomizer(start, end));

        return new EasyRandom(parameter);
    }

    private static EasyRandomParameters getEasyRandomParameters() {
        return new EasyRandomParameters()
                .excludeField(named("id"))
                .stringLengthRange(1, 100)
                .randomize(Long.class, new LongRangeRandomizer(1L, 100000L));
    }
  • Test Code를 작성하고 테스트 데이터를 입력한다
@SpringBootTest
public class PostBulkInsertTest {
    @Autowired
    private PostRepository postRepository;
    @Test
    public void bulkInsert() {
        var easyRandom = PostFixtureFactory.get(
                4L,
                LocalDate.of(1970, 1, 1),
                LocalDate.of(2022, 2, 1)
                );
        int _1만 = 10000;
        var posts = IntStream.range(0, _1만 * 1)
                .parallel()
                .mapToObj(i -> easyRandom.nextObject(Post.class))
                .toList();
        postRepository.bulkInsert(posts);
    }
}

쿼리를 날려봅시다


입력 된 데이터를 조회하면 총 300만 개의 데이터가 입력 되었음을 알 수 있다

이제 id가 3인 것으로 조회했을 때, 성능이 얼마나 나오는지 확인해보자

explain select createdDate,memberId ,count(id) as count  
from POST  
where memberId=3 and createdDate between '1900-01-01' and '2023-12-31'  
group by createdDate,memberId;


rows를 보면, id가 3인 유저를 찾기 위해 299만개의 데이터를 조회 했음을 알 수 있다.
사실 상 거의 모든 데이터를 조회 한 것이다. 효율은 꽝이다

실제 서비스에서 쿼리 하나에 2초 걸리며, CPU가 100% 찍는 것은 사고이다

이제 인덱스를 추가해보자

추가한 인덱스 사용을 강제하면 성능이 좋아질까?


결과는 그렇지 않았다. 인덱스를 주니까 시간이 더 걸려서 10초나 걸렸다
이는 탐색 범위를 줄일 수 없으니 효과가 없다.
데이터의 분포도에 따라 걸리는 성능의 차이가 크다

결론

순차적으로 인덱스를 타기 때문에, 조건에 따라 빠르기도하고 느리기도 하다.
인덱스를 잡을 때 그래서 데이터 분포도와 어떻게 group by 조건을 설정해야 하는지 유의해야 한다.

  • TIP : explain으로 꼭 인덱스 접근을 어떻게 하는지 확인하라

인덱스 사용시 주의 사항

  1. 인덱스 필드의 가공은 수정할 수 없다.
    • 인덱스는 필드의 가공 결과를 저장하는 것이 아니라, 필드의 원본 값을 사용하여 검색하는 구조입니다.
    • 따라서 인덱스 필드를 가공하거나 변환한 경우, 해당 필드를 수정하려면 인덱스를 재생성해야 합니다.
  2. 복합 인덱스를 사용할 때 주의해야 합니다.
    • 복합 인덱스는 여러 개의 컬럼을 조합하여 생성하는 인덱스입니다.
    • WHERE 절에 어떤 컬럼이 들어오느냐에 따라 인덱스를 탈 수 있거나 탈 수 없는 경우가 있습니다.
    • 따라서 복합 인덱스를 사용할 때는 어떤 컬럼이 선두에 위치하느냐가 중요하며, 인덱스의 구성을 신중하게 결정해야 합니다.
  3. 옵티마이저는 하나의 쿼리에 하나의 인덱스만을 사용합니다.
    • 옵티마이저는 쿼리를 실행할 때 하나의 인덱스만을 선택하여 사용합니다.
    • 따라서 여러 개의 인덱스가 존재할 경우, 옵티마이저가 가장 적합한 인덱스를 선택하도록 인덱스를 설계해야 합니다.
    • 예를 들어, WHERE 절에 있는 조건과 ORDER BY 절에 있는 필드가 다르다면, 옵티마이저는 WHERE 절에 해당하는 인덱스를 탄 후 ORDER BY 절에 해당하는 인덱스를 다시 조회해야 합니다.
  4. 배포 환경과 프로덕션 환경의 인덱스 설정을 확인해야 합니다.
    • 개발 환경과 프로덕션 환경은 데이터의 양과 트래픽 등이 다를 수 있습니다.
    • 따라서 인덱스의 효율성은 환경에 따라 다를 수 있으며, 반드시 배포 환경에서 테스트하여 인덱스의 성능을 확인해야 합니다.
    • 또한, 인덱스를 사용하면 쓰기 작업의 성능이 저하될 수 있으므로, 인덱스를 사용해야 하는지를 신중하게 판단해야 합니다.
  5. 인덱스를 사용할 때는 탐색 범위를 식별할 수 있는 범위가 넓은 필드를 인덱스로 사용하는 것이 좋습니다.
    • 인덱스는 탐색 범위를 좁히는 용도로 사용됩니다.
    • 따라서 식별할 수 있는 범위가 넓은 필드를 인덱스로 사용하면 효과적입니다.
    • 예를 들어, 성별 필드보다는 생년월일 필드를 인덱스로 사용하는 것이 더 효율적입니다.

레퍼런스

부족한 점이나 잘못 된 점을 알려주시면 시정하겠습니다 :>

728x90

'DEV > Backend' 카테고리의 다른 글

오프셋 기반 페이지네이션  (0) 2023.06.09
페이지네이션  (0) 2023.06.09
DTO(Data Transfer Object)  (0) 2023.06.02
정규화  (0) 2023.06.01
소프트 파싱과 하드 파싱  (0) 2023.06.01