쿠다의 공부 블로그

[+/KM]MySQL에서 공간 데이터 최적화하기 본문

Project/PlusKM

[+/KM]MySQL에서 공간 데이터 최적화하기

쿠다(빙응) 2025. 1. 20. 22:50



📝배경

 

이번에 개인 프로젝트를 진행하면서 공공데이터 포탈의 의료 데이터를 사용하게 되었습니다.

그래서 의료 데이터 중 병원, 약국의 정보를 저장하여 그 데이터 중에서 좌표를 활용해서 주변 약국, 병원을 조회하는 서비스를 만들기로 하였습니다. 

 

데이터 저장에 성공하고 임의로 좌표 조회를 한 순간 매우 높은 응답 시간이 나와 최적화하기로 하였습니다. 

 

 

📝MySQL 공간 데이터베이스 

지금 사용하는 MySQL에서는 공간 데이터베이스를 제공합니다.

MySQL :: MySQL 8.0 Reference Manual :: 13.4.1 Spatial Data Types

 

MySQL :: MySQL 8.0 Reference Manual :: 13.4.1 Spatial Data Types

13.4.1 Spatial Data Types MySQL has spatial data types that correspond to OpenGIS classes. The basis for these types is described in Section 13.4.2, “The OpenGIS Geometry Model”. Some spatial data types hold single geometry values: GEOMETRY POINT LIN

dev.mysql.com

 

MySQL에서 공간 정보를 활용하기 위해 3가지의 요소가 필요합니다.

  • 공간 데이터 타입
  • 공간 함수
  • 공간 관계 함수
 MySQL 공간 데이터 타입

 

공간 데이터 타입은 1~7까지 점점 커지는 의미로 1이 2를 구성한다 보면 됩니다. 우리는 좌표를 활용할 예정이므로 Point를 사용할 것입니다. 

MySQL 공간 함수 

이제 MySQL의 공간함수에 대해 알아봅시다. 우리는 좌표 평면에서 그 평면 기준으로 반경 ?KM를 조사할 것이기에 이에 적합한 함수를 결정해야 합니다.

 

1 ST_Intersection (g1 Geometry, g2 Geometry) : Geometry g1과 g2의 교집합인 공간 객체를 반환
2 ST_Union (g1 Geometry, g2 Geometry) : Geometry g1과 g2의 합집합인 공간 객체를 반환
3 ST_Difference (g1 Geometry, g2 Geometry) : Geometry g1과 g2의 차집합인 공간 객체를 반환
4 ST_Buffer (g1 Geometry, d Double ) : Geometry g1에서 d 거리만큼 확장된 공간 객체를 반환
5 ST_Envelope (g1 Geometry) : Polygon g1을 포함하는 최소 MBR인 Polygon을 반환
6 ST_StartPoint (l1 LineString) : Point l1의 첫 번째 Point를 반환
7 ST_EndPoint (l1 LineString) : Point l1의 마지막 Point를 반환
8 ST_PointN (l1 LineString) : Point l1의 n 번째 Point를 반환

 

이 표는 가장 통상적인 공간 함수를 나타냅니다. 여기서 제가 생각한 가장 적합한 함수ST_Buffer입니다.

 

ST_Buffer는 임의의 좌표를 기준으로 특정 거리만큼 확장된 영역을 생성하여 다른 조건과 비교할 수 있습니다. 이 특성은 KM 기반 거리 검색을 구현하는 저의 프로젝트에 매우 효과적일 것으로 예상됩니다.

 

공간 관계 함수

공간 관계 함수는 지리 정보 시스템(GIS)에서 공간 데이터를 처리하고, 분석하기 위해 사용됩니다.

이를 통해 두 개 이상의 공간 객체가 어떻게 상호 작용하는지 (예: 겹치는지, 포함되는지, 교차하는지 등)를 확인할 수 있습니다.

1 ST_Equals (g1 Geometry, g2 Geometry)
: Boolean
g1 g2가 동일하면 True를 반환하고 상이하다면 False를 반환
2 ST_Disjoint (g1 Geometry, g2 Geometry)
: Boolean
g1 g2가 겹치는 곳 없다면 True를 반환하고, 겹치는 곳이 있으면 False를 반환
3 ST_Within (g1 Geometry, g2 Geometry)
: Boolean
g1 g2 영역 안에 포함된 경우 True를 반환하고 그렇지 않은 경우 False를 반환
4 ST_Overlaps (g1 Geometry, g2 Geometry)
: Boolean
g1 g2 영역 중 교집합 영역이 존재하는 경우 True를 반환하고 존재하지 않는 경우 False를 반환
5 ST_Intersects (g1 Geometry, g2 Geometry)
: Boolean
g1 g2 영역 간에 교집합이 존재하는 경우 True를 반환하고 그렇지 않은 경우 False를 반환
6 ST_Contains (g1 Geometry, g2 Geometry)
: Boolean
g2 g1 영역 안에 포함된 경우 True를 반환하고 그렇지 않은 경우 False를 반환
7 ST_Touches (g1 Geometry, g2 Geometry)
: Boolean
g1 g2가 경계 영역에서만 겹치는 경우 결과 값으로 True를 반환하며 경계 영역 외에서 겹치거나 겹치는 곳이 없다면 False를 반환
8 ST_Distance (g1 Geometry, g2 Geometry) Double g1 g2간의 거리를 반환

이 프로젝트는 범위 안에 포함되어있는 포함 관계를 확인하기에 ST_Within를 사용하기로 했습니다. 

 

 

 

📌정한 쿼리 실험하기 

 

이제 한번 정리해봅시다. 

  • 공간 데이터 : Point
  • 공간 함수 : ST_Buffer
  • 공간 관계 함수 : ST_Within
explain analyze
SELECT *
FROM place
WHERE 
    ST_Within(
        coordinate, 
        ST_Buffer(
            ST_GeomFromText('POINT(37.5665 126.9780)', 4326), 
            10000 
        )
    );

이 쿼리는 서울 중구 기준으로 1km 반경으로 ST_Buffer 연산을 한 것입니다. 

또한 ST_Within는 한 기준점 범위 내에 포함되는지를 검색하는 함수입니다. 

실행 결과 Extra가 Using where이 나왔습니다. 인덱스를 사용하고 있긴 하지만 부분적으로 사용하고 있습니다.

 

📝공간 조회 쿼리 최적화하기 

앞의 결과로 보면 ST_Buffer는 성능 문제를 안고 있습니다.

  • ST_Buffer반경을 지정하여 원을 그리는 함수로, 이 원을 정확하게 계산하기 위해 내부적으로 많은 계산을 필요로 합니다.
  • DB 엔진 입장에서 특정 좌표가 영역 안에 포함되는지(ST_Within) 판별할 때, 32각형 안의 점인지 검사해야 합니다.

 

그렇기에 ST_Buffer 대신에 직접 범위를 만드는 것을 선택했습니다. 

 

이제 테스트를 진행해봅시다. 테스트는 서비스 최대 탐색 범위인 10KM 기준으로 작성했습니다.

 

explain analyze
SELECT *
FROM place 
WHERE ST_Within(coordinate, 
    ST_GeomFromText('POLYGON((37.5396 126.9511, 37.5934 126.9511, 37.5934 127.0049, 37.5396 127.0049, 37.5396 126.9511))', 4326));
'-> Filter: st_within(place.coordinate,<cache>(st_geomfromtext(\'POLYGON((37.4775 126.8890,
37.4775 127.0670, 37.6555 127.0670, 37.6555 126.8890, 37.4775 126.8890))\',4326)))  (cost=112
rows=248) (actual time=0.243..757 rows=14997 loops=1)\n
	-> Index range scan on place using idx_place_coordinate over 
	(coordinate unprintable_geometry_value)  (cost=112 rows=248)
    (actual time=0.153..93.8 rows=15002 loops=1)\n'

위의 결과를 보시면 총 757ms가 걸린 것을 알 수 있습니다.

 

그렇다면 전의 쿼리는 얼마나 걸렸을까요?

explain analyze
SELECT *
FROM place
WHERE 
    ST_Within(
        coordinate, 
        ST_Buffer(
            ST_GeomFromText('POINT(37.5665 126.9780)', 4326), 
            10000 
        )
    );
'-> Filter: st_within(place.coordinate,<cache>(st_buffer(st_geomfromtext(\'POINT(37.5665
126.9780)\',4326),10000)))  (cost=144 rows=319) (actual time=0.736..1681 rows=14414 loops=1)
\n    -> Index range scan on place using idx_place_coordinate over 
	(coordinate unprintable_geometry_value)  (cost=144 rows=319)
	(actual time=0.208..149 rows=17988 loops=1)\n'

해당 쿼리는 1681ms가 걸린 것을 알 수 있습니다. 

그 이유는 인덱스 범위 스캔을 사용하였지만 ST_Buffer에 의한 원형 다각형 계산이 인덱스를 최적화하지 못해 성능이 저하되었습니다.

개선 전 개선 후
1681ms 757ms

 

📝로직 변경으로 인한 오류

 

10KM 검색 시 사각형의 꼭짓점은 10KM보다 먼거 아닌가요?

 

틀린 말은 아닙니다. 이것도 해결 방법이 있습니다. 바로 직사각형으로 필터링 후 원 범위 탐색을 진행하는 것입니다.

SELECT *
FROM place 
WHERE ST_Within(coordinate, ST_GeomFromText('POLYGON(...)', 4326)) -- 1차: 사각형 인덱스 스캔 (빠름)
  AND ST_Distance_Sphere(coordinate, ST_GeomFromText('POINT(37.5665 126.9780)', 4326)) <= 10000; -- 2차: 실제 반경 10km 이내 정확도 보정

 

📝결론

이번 쿼리 최적화 작업을 통해, ST_Buffer 함수를 사용하는 기존 방식에서 직접 정의된 POLYGON을 사용하는 방식으로 전환함으로써 성능을 크게 개선할 수 있었습니다.

기존의 ST_Buffer는 인덱스를 타긴 하지만, 원형 다각형(32각형)의 포함 여부를 계산하는 데 막대한 CPU 연산 리소스를 소모하여 병목이 발생했습니다. 반면 4각형인 POLYGON은 기하학적 연산 비용이 극도로 낮아 속도가 획기적으로 단축되었습니다.

최적화 전: 1681ms
최적화 후: 757ms

따라서 ST_Buffer 대신 POLYGON을 사용한 방법이 더 효율적이며, 공간 데이터를 다룰 때 성능 향상을 위해 이러한 접근 방식을 채택하는 것이 매우 중요하다는 결론을 내릴 수 있습니다.