1. 실시간 tsvector 계산의 한계
pg_search 젬을 기본 설정으로 사용할 경우, 검색 쿼리는 내부적으로 to_tsvector() 함수를 호출하여 매번 텍스트를 처리합니다.
- 순차 스캔(Sequential Scan) 강제: 쿼리 시점에 계산되는 값에는 일반적인 인덱스를 적용할 수 없으므로, PostgreSQL은 모든 행을 일일이 검사해야 합니다.
- CPU 리소스 낭비: 검색할 때마다 텍스트 분리, 스테밍(Stemming), 불용어 제거 연산을 반복 수행합니다.
- 확장성 문제: 데이터 양이 늘어날수록 검색 시간은 선형적으로 증가하며, 동시 사용자가 많아질수록 서버 부하가 기하급수적으로 커집니다.
2. 최적화 솔루션: Persistent TSVector
성능 문제를 해결하기 위해서는 검색용 벡터 데이터를 미리 계산하여 테이블에 저장하고 인덱스를 생성해야 합니다.
A. 데이터베이스 스키마 개선
새로운 tsvector 타입의 컬럼을 추가하고, 이를 위한 GIN(Generalized Inverted Index) 인덱스를 생성합니다. GIN 인덱스는 개별 토큰을 인덱싱하여 해당 토큰이 포함된 행을 즉시 찾을 수 있게 해줍니다.
B. 데이터 자동 동기화 (Trigger)
원본 텍스트가 변경될 때마다 tsvector 컬럼이 자동으로 업데이트되도록 데이터베이스 트리거를 설정합니다. 이는 Rails 애플리케이션 계층이 아닌 데이터베이스 계층에서 처리되므로 데이터 정합성을 보장합니다.
sql
CREATE TRIGGER pg_search_documents_content_vector_update
BEFORE INSERT OR UPDATE ON pg_search_documents
FOR EACH ROW
EXECUTE FUNCTION tsvector_update_trigger(content_vector, 'pg_catalog.english', content);
C. pg_search 설정 변경
pg_search가 실시간 계산 대신 미리 생성된 컬럼을 사용하도록 설정을 업데이트합니다.
ruby
PgSearch.multisearch_options = {
using: {
tsearch: {
dictionary: 'english',
tsvector_column: 'content_vector',
prefix: true
}
}
}
3. 성능 개선 결과 분석
10만 건의 데이터를 기준으로 테스트한 결과, 성능 차이는 극명하게 나타났습니다.
- 최적화 전: 약 283ms 소요.
Parallel Seq Scan이 발생하며 5만 개 이상의 행을 필터링하는 병목 현상이 관찰되었습니다. - 최적화 후: 약 2.4ms 소요.
Bitmap Index Scan을 통해 필요한 39개의 행만 즉시 조회했습니다. - 개선 효과: 약 118배의 속도 향상 및 99% 이상의 실행 시간 단축을 달성했습니다.
4. 핵심 교훈
- EXPLAIN ANALYZE 활용: 쿼리 성능이 저하될 때는 반드시 실행 계획을 분석하여
Seq Scan여부를 확인해야 합니다. - 인덱스 전략: 대량의 텍스트 검색에는
GIN인덱스가 필수적이며, 이를 위해서는 검색 대상이 컬럼 형태로 존재해야 합니다. - 라이브러리 이해:
pg_search와 같은 편리한 도구도 대규모 환경에서는 기본 동작 방식을 조정해야 할 필요가 있음을 인지해야 합니다.