위치 정보를 DB에 저장하는 방법
위도 경도와 같은 정보를 DB에 어떻게 저장할까?
데이터베이스접근 방식
지도 정보를 데이터베이스에 저장하는 가장 일반적인 방법은 해당 지점의 위도와 경도 좌표를 저장하는 것이다.
크게 두 가지 기본 접근 방식이 있다.
- 숫자 컬럼 사용:
DECIMAL,DOUBLE같은 숫자 데이터 타입의 컬럼 두 개(e.g.latitude,longitude)에 위도와 경도를 각각 나누어 ****저장 - 공간 데이터 타입 사용: DB 자체에서 지원하는 ‘공간 데이터 타입’(Spatial Data Type)을 사용
예를 들어,
POINT라는 단일 컬럼에 (경도, 위도) 좌표 쌍을 하나의 값으로 저장
어떤 방식을 선택하는지에 따라 사용해야 할 DB 기능, 쿼리, 그리고 성능이 크게 달라진다.
MySQL에서 사용
MySQL 5.7부터 공간 데이터 기능을 지원하며, 8.0에서 관련 함수들이 대폭 개선됐다.
단순히 DECIMAL 타입으로 컬럼 두 개를 만드는 것보다, POINT 를 사용하는 것이 훨씬 효율적이다.
POINT 타입은 (X, Y) 좌표를 하나의 값으로 저장한다.
단, GIS 표준과 MySQL은 POINT(경도, 위도) 순서를 따른다. 우리가 흔히 말하는 “위도, 경도”의 반대이므로 주의한다.
-- 'locations'라는 이름의 장소 테이블 생성
CREATE TABLE locations (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
-- (경도, 위도) 순서로 좌표를 저장할 컬럼
-- SRID 4326은 GPS가 사용하는 표준(WGS84)을 의미합니다.
coords POINT NOT NULL SRID 4326
);
예제
강남역 (경도 127.0276, 위도 37.4979)을 기준으로 반경 5km 이내에 있는 모든 장소를 검색한다.
-- 1. 검색 기준이 될 '내 위치'를 POINT 객체로 설정합니다.
-- SRID 4326을 명시해줍니다.
SET @my_location = ST_GeomFromText('POINT(127.0276 37.4979)', 4326);
-- 2. 검색할 반경(미터 단위)
SET @radius_meters = 5000;
-- 3. 실제 검색 쿼리
SELECT
id,
name,
ST_AsText(coords) AS location_coords,
-- ST_Distance_Sphere는 두 지점 간의 구(지구) 표면 거리를 미터(m) 단위로 계산합니다.
ST_Distance_Sphere(coords, @my_location) AS distance_in_meters
FROM
locations
WHERE
-- ST_Distance_Sphere 함수로 계산한 거리가 @radius_meters보다 작거나 같은 것만 필터링
ST_Distance_Sphere(coords, @my_location) <= @radius_meters;
SG_GeomFromText로 텍스트를 POINT 객체로 변환한다.
ST_AsText는 POINT 객체를 텍스트로 다시 변환해 준다.
ST_Distance_Sphere는 두 POINT 간의 구면 거리를 미터로 반환한다.
위에서 작성한 쿼리는 기능적으로는 완벽하지만, locations 테이블에 데이터가 100만 건 있다면 정말 느릴 것이다.
성능 문제 원인 - Full Table Scan
인덱스가 없는 상태에서 위 쿼리를 실행하면, MySQL은 다음 과정을 거친다.
locations테이블의 **모든 행(100만 건)**을 하나씩 전부 읽어옴- 각 행마다
ST_Distance_Sphere함수를 실행 (지구 곡률을 계산하는 복잡한 삼각함수 연산 등 수행) - 값이 비싼 거리 계산을 100만 번 수행함
- 계산 결과가 5000 이하인 행만 최종 반환
인덱스
- 테이블의 검색(SELECT) 속도를 획기적으로 높이기 위한 자료구조
- 책에 색인을 등록하는 것과 같다
성능 문제 해결 - 공간 인덱스 (Spatial Index)
일반적인 인덱스(B-Tree)가 값의 대소 관계를 빠르게 찾는 데 특화되어 있다면, 공간 인덱스(주로 R-Tree)는 2D/3D 공간에서 어떤 영역 안에 포함되는지를 빠르게 찾는데 특화되어 있다.
-- coords 컬럼에 SPATIAL INDEX를 생성합니다.
ALTER TABLE locations ADD SPATIAL INDEX(coords);
공간 인덱스가 적용된 테이블에서 2번의 쿼리를 다시 실행하면, MySQL 옵티마이저는 훨씬 똑똑하게 동작한다.
- 쿼리가
ST_Distance_Sphere(...) <= 5000조건을 확인 - 옵티마이저는 5km 반경의 원을 완벽하게 감싸는 가장 작은 사각형(MBR, Minimum Bounding Rectangle) 영역을 먼저 계산
SPATIAL INDEX(R-Tree)를 사용하여, 이 사각형 영역(MBR) 안에 포함되는 행들만 빠르게 찾아냄- R-Tree 인덱스는 특정 사각 영역 내의 데이터를 찾는 데 매우 빠르다.
- 이 단계에서 테이블의 나머지 데이터는 아예 연산 대상에서 제외된다.
- 인덱스를 통해 걸러진 소수의 후보군에 대해서만
ST_Distance_Sphere함수를 실행하여, 사각형의 모서리에 있지만 실제 원 밖 5km를 벗어나는 데이터들을 최종적으로 탈락시킴 - 최종 결과 반환
B-Tree vs. R-Tree
- B-Tree (Balanced-Tree): 숫자, 문자열 처럼 1차원적인(1D) 데이터를 정렬하고 검색하는 데 사용
- R-Tree (Rectangle-Tree): 지도 좌표처럼 2차원(2D) 또는 3차원(3D)의 공간 데이터를 검색하는 데 사용
B-Tree
B-Tree는 이름 그대로 ‘균형 잡힌’ 트리 자료 구조이다. 일반적인 DB 인덱스의 표준이다.
특정 값 찾기, 범위 검색, 정렬이 주요 용도이다.
모든 데이터를 정렬된 상태로 유지하며, 마치 거대한 파일 캐비닛처럼 작동하는 계층을 가지고 있다.
- 루트 노드는 "A-G", "H-M", "N-S", "T-Z" 같은 큰 범위를 가리킨다.
- ‘K’로 시작하는 데이터를 찾으면, “H-M” 서랍을 연다.
- 그 안에는 "Ha-Hj", "Hk-Hz", "I-J", "Ka-Ko", "Kp-Kz", "L-M" 같은 더 세분화된 폴더가 있다.
- “Ka-Ko” 폴더를 열면 원하는 데이터가 있다.
여기서 균형이 잡혀있다는 것은, 어떤 데이터를 찾든 루트에서부터 특정 데이터까지의 탐색 깊이가 거의 일정하게 유지된다는 의미다.
이 덕분에 데이터가 아무리 많아도 항상 예측 가능하고 빠른 검색 속도(O(log N))을 보장한다.
R-Tree
R-Tree는 ‘사각형’ 트리이다. R-Tree는 정렬 대신 ‘그룹화’를 사용한다.
근처에 있는 데이터를 묶어서 그것들을 모두 포함하는 가장 작은 사각형(MBR)으로 표시한다. 그 다음, 이 ‘사각형’들을 다시 근처에 있는 것끼리 묶어서 더 큰 사각형으로 만든다. 이 과정이 반복되어, 루트 노드는 전체를 감싸는 거대한 사각형이 된다.
검색할 때는 R-Tree는 위 계층을 타고 내려간다.
예를 들어서 내 위치 반경 5km(검색 영역) 쿼리가 들어온다고 가정한다.
- 내 검색 영역이 ‘서울’ MBR과 겹치는가? → O
- ‘서울’ MBR의 자식 노드인 ‘강남구’, ‘송파구’, ‘마포구’ MBR 중 내 영역과 겹치는 것은? → ‘강남구’, ‘송파구’
- ‘마포구’ MBR은 내 영역과 겹치지 않으므로, ‘마포구’에 속한 수천 개의 모든 하위 데이터는 아예 검색 대상에서 제외한다.
- 이런 식으로 관련 없는 영역을 통째로 가지치기함으로써 엄청나게 빠른 공간 검색이 가능해진다.
공간 인덱스는 자동으로 적용되나?
-
공간 함수를 사용하면 공간 인덱스가 자동으로 적용되나?
- 모든 공간 함수가 인덱스를 활용하는 것은 아니다.
WHERE절에서 인덱스 친화적인 함수를 사용해야 쿼리 옵티마이저가 인덱스 사용을 고려한다.
- 모든 공간 함수가 인덱스를 활용하는 것은 아니다.
-
공간 인덱스가 사용되는 조건은?
- 대상 컬럼에
SPATIAL INDEX가 생성되어 있어야 함 WHERE절에서 인덱스 친화적인 함수를 사용해야 함- O (인덱스 사용 가능):
ST_Contains,ST_Intersects,ST_Within등 두 공간의 관계를 판단하는 함수 - X (인덱스 사용 불가):
ST_Distance,ST_Distance_Sphere등 거리를 계산하는 함수
- O (인덱스 사용 가능):
- 대상 컬럼에
-
ST_Distance_Sphere(거리 계산)는 왜 인덱스를 못 쓸까?- 인덱스는 특정 영역 내의 데이터를 빠르게 찾는 구조
WHERE ST_Distance_Sphere(location, point) <= 5000같은 조건은, DB가 모든 레코드와의 거리를 일단 계산해봐야 5000 이하인지 알 수 있으므로 인덱스를 활용하지 못하고 Full Table Scan이 발생
-
인덱스 친화적인 함수를 쓰면 100% 인덱스를 타나?
- 자동은 아니지만 쿼리 옵티마이저가 판단해서 사용
- 옵티마이저는 데이터의 양, 조회 범위(선택도) 등을 비용 기반으로 계산하여, 인덱스를 사용하는 것이 Full Table Scan보다 빠르다고 판단될 때 인덱스를 사용
- 대부분의 합리적인 상황(데이터가 많고, 좁은 범위를 조회)에서는 인덱스를 사용
-
인덱스 사용 여부 확인법은?
- 실행할 쿼리 앞에
EXPLAIN을 붙여 실행 - 결과 테이블의
key컬럼에 내가 만든 공간 인덱스의 이름이 표시되는지 확인 (type이ALL이 아니어야 함)
- 실행할 쿼리 앞에