지금 진행하고 있는 프로젝트에서는 MySQL을 사용하고 있습니다.
공간 검색 기능에 대해 알아보던 중 다음 글을 발견했습니다.
https://steemit.com/kr-dev/@tmkor/db-2-mysql-vs-postgis-postgresql
해당 글의 결과만 보자면 다음과 같습니다.
- PostGIS의 gist index의 경우 MySQL의 spatial index에 비해 10x 이상의 성능 향상을 가져다 줍니다.
- 다만, PostGIS의 경우 지원 문서 및 사용자 수가 적으므로, 상황에 맞추어 선택을 하는 것이 좋습니다.
지금까지 프로젝트를 진행하면서 팀원 모두가 MySQL이 익숙하기 때문에 사용했습니다.
하지만 현재 진행하고 있는 프로젝트는 지도 관련 공간 검색의 비중이 매우 높습니다.
그렇기 때문에 팀원과 상의해본 결과 "PostgreSQL + PostGIS를 사용해서 성능이 10배 이상 빨라진다면 사용하지 않을 이유가 없다는 것"이 결론이였습니다.
하지만 글만 보고는 믿을 수가 없으니 직접 테스트를 해보겠습니다.
PostgreSQL와 PostGIS를 쓰면 정말 빠를지 스프링에서 처리 시간을 테스트해보겠습니다.
관련 코드는 다음 링크로 확인할 수 있습니다.
https://github.com/Jang990/Radius_Search
MySQL에서 반경 검색을 하는 방법은 2가지가 있습니다.
ST_Distance_Sphere
로 거리를 일일이 비교하기ST_Contains
또는 ST_Within
로 필터링을 하고서 ST_Distance_Sphere
로 거리를 비교하기단순하게 현재 위치를 기준으로 거리를 계산해서 3KM 이내의 데이터만을 뽑아내는 방식입니다.
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
...
// 1번 방법
@Query(value = "select c from Client c " +
"where ST_Distance_Sphere(c.location.location, :point) <= 3000")
List<Client> findMySQLST_Distance(@Param("point") Point point);
}
// 테스트 코드
@Test
void test1() {
ClientLocation location = new ClientLocation(35.001d, 125.001d);
List<Client> result = clientRepository.findMySQLST_Distance(location.getLocation());
// 약 475ms
}
1만 건의 데이터 기준 MySQL의 ST_Distance_Sphere
은 약 475ms
의 시간이 소요되었습니다.
사진과 같이 Polygon 사각형을 만들어 ST_Contains
P2, P4, P6를 가져옵니다.
ST_Distance
를 활용해서 P6를 제외합니다.
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
...
// 2번 방법
@Query(value = "select c from Client c where " +
"ST_Contains(:polygon, c.location.location) " +
"AND ST_Distance_Sphere(c.location.location, :point) <= :distanceMeter")
List<Client> findMySQLST_Contains(Polygon polygon, @Param("point") Point point, int distanceMeter);
}
@Test
void test2() {
ClientLocation location = new ClientLocation(35.001d, 125.001d);
Polygon polygon = createSquareAroundPoint(35.001d, 125.001d, 3);
List<Client> result = clientRepository.findMySQLST_Contains(polygon, location.getLocation(), 3000);
// 약 315ms
}
public Polygon createSquareAroundPoint(double latitude, double longitude, double radiusInKm) {
// GPT 도움... - 부정확할 수 있음
GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
double halfSideLength = radiusInKm / (111.32 * Math.cos(Math.toRadians(latitude)));
Coordinate[] coordinates = new Coordinate[5];
coordinates[0] = new Coordinate(longitude - halfSideLength, latitude - halfSideLength);
coordinates[1] = new Coordinate(longitude + halfSideLength, latitude - halfSideLength);
coordinates[2] = new Coordinate(longitude + halfSideLength, latitude + halfSideLength);
coordinates[3] = new Coordinate(longitude - halfSideLength, latitude + halfSideLength);
coordinates[4] = coordinates[0]; // Close the ring
Polygon square = geometryFactory.createPolygon(coordinates);
return square;
}
1만 건의 데이터 기준 MySQL의 ST_Contains
는 약 315ms
의 시간이 소요되었습니다.
PostgreSQL과 PostGIS 설치는 다음 글을 참고해주세요.
PostgreSQL + PostGIS설치하기(Windows 환경)
PostgreSQL + PostGIS에서도 2가지 방법이 있습니다.
1. PostGIS에서 제공하는 ST_DistanceSphere
로 거리를 일일이 비교하기
2. PostGIS에서 제공하는 ST_Dwithin
로 반경내의 값을 가져오기
MySQL과 마찬가지로 단순하게 현재 위치를 기준으로 거리를 계산해서 3KM 이내의 데이터만을 뽑아내는 방식입니다.
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
...
// 1번 방법
@Query(value = "select c from Client c " +
"where ST_DistanceSphere(c.location.location, :point) <= 3000")
List<Client> findPostgreSQLST_Distance(@Param("point") Point point);
}
@Test
void postdb_distance() {
double nowX = 125.001;
double nowY = 35.001;
ClientLocation location = new ClientLocation(nowY, nowX);
List<Client> result = clientRepository.findPostgreSQLST_Distance(location.getLocation());
// 1만 건 기준 약 400ms
}
1만 건의 데이터 기준 PostGIS의 ST_DistanceSphere
은 약 400ms
의 시간이 소요되었습니다.
PostGIS는 특정 거리 내의 데이터를 뽑아내주는 함수를 제공하고 있습니다.
ST_Dwithin이 바로 그 함수입니다.
해당 함수는 주어진 거리 내에 도형이 존재하면 true를 반환합니다.
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
...
// 2번 방법
@Query(value = "select c from Client c " +
"where ST_Dwithin(c.location.location, :point, 3000, false) = true")
List<Client> findTestST_Dwithin(@Param("point") Point point);
}
@Test
void postdb() {
double nowX = 125.001;
double nowY = 35.001;
ClientLocation location = new ClientLocation(nowY, nowX);
List<Client> result = clientRepository.findTestST_Dwithin(location.getLocation());
// 1만 건 기준 약 20ms
}
PostGIS의 ST_Dwithin
은 1만 건 기준 20ms
의 시간이 소요되었습니다.
PostgreSQL ST_Dwithin : 약 20ms
MySQL ST_Contains+ST_Distance_Sphere 함께 사용 : 약 315ms
PostgreSQL ST_DistanceSphere : 약 400ms
MySQL ST_Distance_Sphere 함께 사용 : 약 475ms
ST_Dwithin과 ST_Contains 성능 비율
20ms : 315ms = 1 : 16
PostGIS를 이용한 공간 검색의 성능 차이가 너무 압도적이여서
PostgreSQL로 DB를 변경하기로 결정했습니다.