[산림 공간정보 실무] 대용량 임상도 공간DB 조회가 느려질 때, 공간인덱스부터 다시 봐야 하는 이유

전국 단위로 임상도를 받아서 PostGIS 테이블 하나에 몰아넣고 나면, 처음 며칠은 괜찮다가 어느 순간부터 조회 속도가 눈에 띄게 떨어지는 경험을 한 번쯤 해봤을 것이다. 속성 조건 하나 걸고 특정 시군 범위로 잘라내는 단순한 쿼리인데도 몇 분씩 걸리고, QGIS에서 레이어를 불러오는 것조차 버벅거린다. 이럴 때 제일 먼저 의심받는 건 서버 사양이지만, 실제로 원인을 들여다보면 공간인덱스가 제대로 일하고 있지 않은 경우가 훨씬 많다.

인덱스는 있는데 왜 느린가

GiST 인덱스를 geom 컬럼에 걸어두면 공간 쿼리가 당연히 빨라질 거라고 생각하기 쉽다. 하지만 인덱스를 생성만 해두고 ANALYZE를 돌리지 않으면 쿼리 플래너가 통계를 갖지 못해서 인덱스 대신 전체 스캔을 선택하는 일이 흔하다. 대량으로 데이터를 적재한 직후에는 꼭 VACUUM ANALYZE를 한 번 돌려주는 습관을 들여야 한다. 별것 아닌 것 같아도 이 한 줄을 빼먹어서 몇 시간을 삽질한 사례를 현장에서 여러 번 봤다.

진짜 까다로운 문제는 따로 있다. 임상도처럼 시군 단위로 병합된 폴리곤은 경계가 복잡해서 하나의 피처가 수만 개의 꼭짓점을 가지는 경우가 드물지 않다. GiST 인덱스는 결국 바운딩 박스 기준으로 후보를 추려내는 구조인데, 폴리곤 하나가 너무 크고 복잡하면 바운딩 박스 자체가 거의 쓸모없어진다. 산림 전체를 덮는 거대한 폴리곤의 바운딩 박스는 사실상 시군 전체와 다를 바 없으니, 인덱스가 걸러주는 효과가 사라지고 결국 정밀 비교(exact intersection test) 단계에서 시간을 다 잡아먹는 것이다.

ST_Subdivide로 거대 폴리곤 쪼개기

이런 경우에는 원본 지오메트리를 그대로 쓰지 말고 ST_Subdivide로 일정 꼭짓점 수 이하로 잘라낸 보조 테이블을 하나 더 만드는 방식이 실무에서 효과가 좋다. 예를 들어 다음과 같이 분석용 테이블을 별도로 구성한다.

CREATE TABLE forest_type_sub AS SELECT gid, category, ST_Subdivide(geom, 128) AS geom FROM forest_type_raw;
그 다음 이 테이블에 다시 GiST 인덱스를 걸고 ANALYZE까지 실행한다. 원본 폴리곤 하나가 수백 개의 작은 조각으로 쪼개지면서 바운딩 박스가 실제 형상에 훨씬 가까워지고, 공간 조인이나 중첩분석 속도가 체감될 정도로 달라진다. 다만 분석 결과를 다시 원본 단위로 집계해야 하는 작업(예: 면적 합산)이라면 gid 기준으로 GROUP BY를 걸어 원래 피처 단위로 되돌리는 절차를 잊지 말아야 한다. 이 부분을 놓치면 조각난 폴리곤을 그대로 개별 피처처럼 세어버리는 실수가 나온다.

쿼리 작성 습관이 인덱스를 무력화시키는 경우

인덱스도 멀쩡하고 통계도 갱신했는데 여전히 느리다면, 쿼리 자체를 의심해볼 차례다. 가장 흔한 함정은 geom 컬럼을 그대로 비교하지 않고 매번 ST_Transform으로 좌표계를 바꿔가며 비교하는 습관이다. WHERE ST_Intersects(ST_Transform(a.geom, 5186), b.geom) 같은 구문은 매 행마다 좌표 변환 연산이 들어가기 때문에 인덱스를 거의 못 쓰고, 결과적으로 CRS 혼용 문제와는 또 다른 층위에서 성능을 깎아먹는다. 좌표계는 애초에 적재 단계에서 통일해두고, 쿼리에서는 변환 없이 그대로 비교하는 쪽이 훨씬 낫다.

또 하나 자주 보는 패턴은 속성 조건과 공간 조건을 같이 거는데 속성 인덱스가 없어서 공간인덱스만 믿고 가는 경우다. 임소반 코드나 수종 코드로 자주 필터링한다면 해당 컬럼에 별도 B-tree 인덱스를 추가하거나, PostgreSQL 11 이상에서 지원하는 복합 인덱스를 구성해서 두 조건이 동시에 걸리는 상황을 대비하는 게 좋다. EXPLAIN ANALYZE로 실제 실행 계획을 찍어보면 인덱스가 쓰이고 있는지, 어느 단계에서 시간이 새는지 바로 확인할 수 있으니 느리다고 느껴질 때마다 추측 대신 이 명령부터 돌려보는 습관을 들이는 편이 시행착오를 줄인다.

현장에서 점검해볼 체크리스트

  • 대량 적재 직후 VACUUM ANALYZE를 실행했는가
  • geom 컬럼에 GiST 인덱스가 실제로 걸려 있는지 \\d 테이블명으로 확인했는가
  • 거대하고 복잡한 폴리곤이 섞여 있다면 ST_Subdivide로 분석용 테이블을 분리했는가
  • 쿼리 안에서 매번 ST_Transform을 돌리고 있지는 않은지, 좌표계를 적재 시점에 통일했는지
  • 자주 쓰는 속성 필터에 별도 인덱스가 있는지, EXPLAIN ANALYZE로 실행 계획을 확인했는지

공간DB 튜닝은 한 번 설정해두고 끝나는 일이 아니다. 데이터가 계속 갱신되고 쌓이는 구조라면 주기적으로 ANALYZE를 걸어주는 배치를 돌리는 편이 안전하고, 분기마다 느려진 쿼리가 없는지 EXPLAIN으로 한 번씩 점검해보는 정도의 루틴만 잡아둬도 나중에 전체를 뜯어고치는 수고를 크게 줄일 수 있다. 서버를 증설하기 전에, 인덱스와 쿼리부터 다시 들여다보는 쪽이 비용 대비 효과가 훨씬 크다는 걸 현장에서 여러 번 확인했다.