[PostgreSQL] 인덱스와 최적화 전략
PostgreSQL에서 인덱스를 통해 최적화를 하는 법 등을 정리해본다.
예시는 PostgreSQL이지만 RDB 전반에 해당되는 내용들이다.
인덱스의 구조
RDB의 기본 인덱스 구조는 B-Tree다. 이게 요즘 유행하는 소위 NoSQL DB들과 가장 다른 부분 중 하나다.
당연히 Tree답게 O(Log N)이라서 데이터 규모가 커질수록 조회 성능이 더 완만하게 향상된다.
https://blog.naver.com/sssang97/223232264453
인덱스를 쓰는 것이 항상 빠른가?
그렇지는 않다.
인덱스 스캔은 기본적으로 순차 스캔(풀스캔)보다 최소 몇배는 느리다.
인덱스를 쓰는 이유는 전체 데이터셋에서 "적은 수의 데이터"를 가져오는 것이 효율적이기 때문이다.
그래서 가져오려는 데이터 분포에 따라서 인덱스의 효율성이 판가름난다.
예를 들어, 100개 중에 1개만 가져온다고 하면 인덱스가 효율적일 수 있지만, 100개 중에 40개를 가져온다면 풀스캔이 효율적일 수 있다.
그래서 인덱스가 걸려있더라도 DB는 실제 데이터 통계를 기반으로 해서 인덱스를 쓸지 말지를 결정한다.
스캔 방식 참조
https://blog.naver.com/sssang97/222066082736
Partial Index
내가 생각하기에 PostgreSQL에서 가장 강력한 기능 중 하나다.
특정 조건에 대해서만 인덱스를 생성하도록 유도할 수 있는데, 이를 기반으로 인덱스의 크기를 줄이고 전반적인 성능을 더 끌어올릴 수 있다.
게다가 단순 성능 향상에만 이점이 있는 것이 아니라, 전반적인 데이터 정합성에 있어서도 활용도가 높다. Unique Index와 결합하면 조건적인 제약을 강하게 걸 수 있기 때문이다.
별도 포스트 참조
https://blog.naver.com/sssang97/223498028986
문자열 검색과 인덱스
문자열 검색은 많은 사용사례에서 성능 병목 지점이 되는 사례다.
문자열도 기본적으로 동등 검색(=)이나 대소비교 등에 대해 인덱스를 적당히 잘 탄다.
문제는 와일드카드(%) 등을 활용해서 텍스트에 대한 포함 검색을 하는 경우다.
일단, startsWith 패턴의 경우에는 일반 인덱스로도 커버가 가능하다.
(LIKE '검색어%') 같은 경우 말이다.
문제가 되는 지점은 특정 단어의 contains 패턴 검색을 하는 경우다. => (예: LIKE '%검색어%')
이런건 구조적으로 인덱스를 거는게 불가능하다.
후술할 GIN Index와 pg_trgm 같은 확장을 사용하면 인덱스를 타게 할 수는 있는데, 이것도 1,2글자인 경우에는 타지 않는다.
배열과 Gin 인덱스
대부분의 타입과 사용사례에는 기본 BTree 인덱스로도 충분하지만, 그것으로는 부족할 때도 있다.
대표적인 경우 중 하나가 배열 같은 다차원 값 타입이다. 배열 필드를 기반으로 인덱스를 걸고 빠른 성능을 내게 하고 싶다면 Gin을 써야한다.
혹은, 문자열에 대해 contains 검색을 인덱싱하고 싶다고 해도 Gin을 써야 한다.
https://blog.naver.com/sssang97/222151523170
대신 이건 다른 일반 필드들과 함께 커버링 인덱스를 거는게 불가능하다는 단점이 있다.
커버링 인덱스
인덱스를 컬럼마다 대충 하나씩 걸어도 사용사례가 단순하다면 잘 동작할 수도 있다.
하지만 데이터 규모가 커지고 비즈니스 로직이 이것저것 들어가다보면 특정 사용사례에 최적화된 인덱스 구성이 필요해지기 시작한다.
DB 활용에서 가장 중요한 부분 중 하나다.
https://blog.naver.com/sssang97/223828946151
문제: 복잡한 필터 조건에서의 최적화
RDB는 대부분의 사용사례에서 중간 이상의 성능과 효율성을 보여주지만, 그게 모든 부분에서 해당되는 것은 아니다. 약한 부분도 존재한다.
대규모 데이터셋에 대해서 복잡한 필터 조건이 존재하는 경우가 바로 그것이다.
문제 현상에 대해서는 별도 포스트 참조.
https://blog.naver.com/sssang97/223981963685
그래도 데이터 단위가 N십만 단위나 그 미만일 경우에는 인덱스를 잘 타지 않아도, 적당히 스캔 후 필터로도 괜찮은 성능과 리소스 소모를 감수할 수 있다.
하지만 기반 데이터가 N백만개나 N천만개를 넘어가면 컴팩트한 인덱스 없이는 일반적인 성능을 기대하기 어렵다. DB 스펙을 올린다고 해서 단순하게 해결되는 문제도 아니다.
그러면 인덱스 조합을 대체 어떻게 만들어야할까? 모든 경우의 수를 조합해서 인덱스를 수십개수백개씩 만드는 것은 사실상 불가능하다.
애매한 타협
다만 성능에 아주 민감하지 않다면 적당히 타협하는 식으로 처리를 할 수는 있다.
컬럼 2-3개 정도 조합의 커버링 인덱스를 카디널리티가 높은 필드 기반으로 잘 잡고, 카디널리티가 비교적 낮은 필드는 스캔 이후 필터로 적당히 처리되게 하는 것이 나을 수도 있다.
하지만 모든 경우를 커버하기는 어려울 것이다. 조합에 따라, 데이터 분포에 따라 느린 필터 조건이 여전히 존재할 수 있다.
파티션
이런 난감한 상황에 대처하는 고전적인 방법 중 하나가, 아예 데이터를 물리적으로 쪼개는 것이다.
허용 가능한 사이즈까지 줄여서 데이터 단위를 자르고, 각각의 파티션을 독립적인 스캔 단위로 분리한다.
이러면 특정 파티션 내에서만 접근할 경우에는 꽤 안정적인 성능을 보장할 수 있다는 장점이 있다.
대신 기능의 손실과 제약을 감안해야한다.
조회가 반드시 단일 파티션 수준으로 이루어져야만 효과적인 방안이기 때문이다.
전체 파티션이나 상당수의 파티션을 조회해야한다면 대체로 파티션을 안하는 것보다 못하다.
실제로 오래된 커뮤니티 사이트들 보면 게시글이 많이 쌓이면 어느 시점을 기점으로 검색이나 페이지가 뚝뚝 단절되거나, 아예 게시판을 이전하는 경우도 있다.
이런게 고전적인 파티션 접근법의 부작용이다.
vs Column Base Database
그리고 사실 RDB 같은 Row Base Database들은 이런 사용사례에 잘 맞지 않는다. 이런 광범위한 데이터셋에 대한 복잡한 필터링 처리는 RDB보단 Column Base 구조를 갖고 있는 데이터베이스들이 잘 하는 것이다.
대표적으로 clickhouse 같은 통계용 column base 데이터베이스가 이에 속하고, 검색엔진으로 알려진 Elasticsearch 또한 column base 구조와 캐싱을 통해 이런 것을 잘 처리하는 편이다.
PostgreSQL의 경우에도 citus라고 해서 column base 최적화용 서드파티 플러그인이 있긴 하다.
구현 수준은 잘 모르겠다.
이런 데이터베이스들의 접근법이 궁금하다면 별도 포스트를 참조한다.
https://blog.naver.com/sssang97/223652951126
https://blog.naver.com/sssang97/223640540316