큰 테이블을 나눴는데도, 느린 조회는 남아 있었다. G2BMarket에서 월별 파티셔닝을 진행하고 나면 대용량 데이터를 다루는 부담이 많이 줄어들 거라고 기대했다. 실제로 구조를 나눈 의미는 있었지만, 그것만으로 모든 쿼리가 빨라지지는 않았다.

10월 초 검색 색인 작업을 확인했을 때도 그랬다. 본공고를 처리한 뒤 사전공고를 가져오는 SQL에서 시간 초과가 발생했다. 데이터를 검색 엔진에 넣기도 전에, DB에서 읽어오는 단계에서 막힌 것이다.

이번에는 파티션을 어떻게 옮겼는지보다, 이미 나뉜 테이블에서 왜 한 조회가 여전히 느렸고 무엇을 바꾸었는지에 집중해보려 한다.

검색 데이터를 가져오는 단계에서 막혔다

G2BMarket의 검색 색인 작업은 DB에서 공고를 읽어 검색용 문서로 만든다. 사전공고 조회도 그 준비 단계 중 하나였다. 최근 자료 중 조건에 맞는 공고를 날짜순으로 가져오는 작업이다.

2026년 10월 1일 조사 기록에는 사전공고 SQL 시간 초과가 남아 있다. 별도 검증에서도 기존 쿼리가 3~5초 제한을 넘긴 시도가 있었다. 같은 데이터를 놓고 비교한 표본에서는 SELECT에 약 4,990ms가 걸렸다.

검색 엔진의 쓰기 속도를 의심하기 전에, 읽어오는 쿼리부터 살펴봐야 했다. “색인 작업이 느리다”는 현상을 실제로 시간이 걸린 단계까지 좁히니, 고칠 대상도 분명해졌다.

파티셔닝은 쿼리를 대신 고쳐주지 않았다

월별로 나눈 테이블이라고 해서 최근 데이터를 찾는 쿼리가 자동으로 최근 파티션만 읽는 것은 아니다. PostgreSQL의 파티션 프루닝은 조회 조건으로 불필요한 파티션을 제외할 수 있을 때 작동한다. PostgreSQL 공식 문서

이 조회에는 날짜에 대한 업무 규칙도 있었다. 접수 시각이 있으면 그것을 쓰고, 없으면 처리 날짜, 그것도 없으면 생성 시각을 사용했다. SQL에서는 COALESCE(receipt_datetime, process_date::timestamp, created_at)으로 표현한 조건이었다.

보기에는 날짜 하나를 고르는 짧은 식이다. 하지만 어느 컬럼을 기준으로 찾을지와 결과를 어떤 순서로 가져올지가 그 안에 함께 들어 있었다. 테이블을 나누는 것과 별개로, 이 조건을 조회하기 좋은 형태로 풀어볼 필요가 있었다.

여기서 저장 월 조건을 강제로 추가하는 지름길은 쓰지 않았다. 자료를 최근 것으로 판단하는 날짜와 실제 저장된 파티션의 월은 같은 의미라고 보장할 수 없었다. 빠르게 가져오되, 원래 찾아야 할 공고가 빠지면 안 됐다.

날짜를 고르는 규칙을 세 갈래로 풀었다

해결 방향은 날짜 규칙을 없애는 것이 아니라, 그 규칙을 유지한 채 조회 경로를 나누는 것이었다.

  • 접수 시각이 있는 자료는 접수 시각으로 찾는다.
  • 접수 시각이 없으면 처리 날짜로 찾는다.
  • 두 날짜가 모두 없으면 생성 시각으로 찾는다.

앞선 날짜가 비어 있다는 조건을 명시해 세 갈래가 서로 겹치지 않게 했다. 각 갈래에서 최근 조건과 기존 마감 조건을 적용하고, 정렬한 뒤 필요한 수만 먼저 추렸다. 이를 UNION ALL로 합쳐 원래 날짜 기준과 ID 순서로 최종 정렬하고, 전체 결과 수를 제한했다.

최종 화면이나 검색 문서에 필요한 만큼을 가져오면서, 그 전에 다루는 후보를 줄이도록 쿼리 형태를 바꾼 것이다. 날짜가 비어 있을 때의 대체 규칙과 오래된 저장 위치의 자료도 보존했다.

이번 변경에는 새 인덱스를 만드는 작업이 없었다. 모든 파티션이 탐색에서 제외되거나 모든 조회가 인덱스만 읽게 된 것도 아니다. 파티션 구조를 다시 만드는 대신, 같은 데이터를 가져오는 쿼리의 작업 방식을 바꾼 개선이었다.

약 5초에서 668ms, 결과도 그대로였다

변경 후에는 실행시간보다 먼저 결과가 같은지 확인했다. 쿼리가 빨라졌더라도 자료가 빠지거나 순서가 달라졌다면 같은 기능의 개선이라고 말하기 어렵기 때문이다.

10월 1일 검증은 읽기 전용의 동일 스냅샷에서 두 쿼리를 실행했다. 두 쿼리 모두 JIT를 끄고 SQL 시간 제한을 5초로 맞춘 표본이다.

확인 항목기존 쿼리변경 쿼리
SELECT 실행시간4,990.2ms668.36ms
결과 행 수1,522행1,522행
전체 내용과 순서비교 기준정확히 동일

내게 중요한 결과는 시간이 줄었다는 것과 같은 1,522행을 같은 순서로 가져왔다는 것이 함께 확인됐다는 점이었다. 처리 대상을 덜어내서 빠르게 보이게 만든 것이 아니라, 결과를 유지한 채 읽는 방식을 바꿀 수 있었다.

다만 이 수치는 해당 SELECT의 제한된 비교 결과다. 검색 엔진에 실제로 쓰는 시간이나 색인 전체 완료시간, 사용자의 검색 응답시간까지 측정한 것은 아니다. 첫 조회와 반복 조회, 동시 부하에 따른 차이도 별도로 봐야 한다.

테이블을 나누는 것과 덜 읽게 만드는 것은 달랐다

처음에는 데이터가 커졌으니 구조부터 나누어야 한다고 생각했다. 그것도 필요한 작업이었다. 하지만 이번 문제에서는 “테이블이 얼마나 큰가”에서 한 걸음 더 들어가, “이 쿼리가 같은 결과를 만들기 위해 무엇을 하고 있는가”를 봐야 했다.

장애가 난 단계를 좁히고, 날짜의 업무 의미를 지키면서 조회 형태를 바꾸고, 결과와 실행시간을 같이 비교했다. 그 과정을 거치니 파티셔닝과 쿼리 개선이 서로 다른 역할이라는 점이 더 분명해졌다.

파티셔닝은 데이터를 나누는 일이었고, 이번 해결은 쿼리가 필요한 결과를 더 적은 부담으로 찾게 만드는 일이었다. 구조를 바꾸었다는 사실보다 실제로 같은 결과를 얼마나 효율적으로 가져오는지 확인하는 것이 중요했다.

이 글은 2026년 10월 1일의 조사와 제한 검증 기록을 바탕으로 작성했다. 전체 서비스 장애 해결이나 현재 운영 성능을 보증하는 결과는 아니다.