1. PostGIS란?
- PostGIS는 오픈소스 RDBMS인
PostgreSQL에 공간(지리) 데이터 처리 기능을 추가해주는 Extension임.
PostgreSQL + PostGIS = 위치/지도 데이터를 다루는 공간 데이터베이스.
GIS(Geographic Information System)에서 필요한 거리 계산, 영역 검색, 도형 연산 등을 SQL로 처리할 수 있으며, OGC(Open Geospatial Consortium)의 Simple Features for SQL 표준을 준수함.
2. 주요 특징
2-1. 다양한 공간 데이터 타입
| 타입 | 설명 |
|---|
Point | 단일 좌표 (위도/경도) — 건물 위치, GPS 좌표 |
LineString | 연결된 좌표의 선 — 도로, 강, 경로 |
Polygon | 닫힌 다각형 — 건물 외곽, 행정구역 경계 |
MultiPoint | 복수의 점 집합 |
MultiPolygon | 복수의 다각형 집합 — 섬이 여러 개인 지역 |
GeometryCollection | 여러 타입을 혼합한 집합 |
2-2. 200개 이상의 공간 함수
- 거리 계산, 면적 측정, 도형 교차 여부 등 GIS 연산을 SQL로 처리.
2-3. 좌표계(SRID) 지원
- SRID(Spatial Reference ID)로 다양한 좌표계를 지원함.
- SRID 4326: WGS84 (GPS 기본 좌표계, 전 세계 표준)
- SRID 5179: GRS80 (국내 지도 서비스에서 자주 사용)
2-4. GIST 공간 인덱스
- B-Tree 인덱스로는 처리할 수 없는 2D 공간 데이터에 특화된 인덱스로, 대규모 공간 쿼리도 빠르게 처리함.
2-5. 풍부한 생태계 연동
- QGIS, GeoServer, Mapbox, Leaflet 등 GIS 툴과 바로 연동할 수 있음.
3. 설치 및 초기 설정
3-1. 확장 활성화.
CREATE EXTENSION postgis;
SELECT PostGIS_Version();

Ex) 공간 컬럼이 있는 테이블 생성
CREATE TABLE cafes (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
location GEOMETRY(Point, 4326)
);
SRID 4326: WGS84(World Geodetic System 1984)라는 좌표계를 의미.
- SRID(Spatial Reference Identifier), 즉 좌표계의 고유 번호.
3-1-1. 오류 발생시.
CREATE EXTENSION postgis;
오류: "postgis" 이름의 확장 모듈을 사용할 수 없습니다
SQL state: 0A000 Detail: "C:/Program Files/PostgreSQL/17/share/extension/postgis.control"
확장 모듈 제어 파일 열기 실패: No such file or directory.
Hint: 해당 확장 모듈은 PostgreSQL 시작 전에 먼저 설치 되어 있어야합니다.

- 해당 프로그램을 통해
PostGIS를 설치해야됨.
4. 사용법.
4-1. 데이터 삽입
ST_GeomFromText()로 WKT(Well-Known Text, 공간 데이터를 텍스트로 표현하는 표준 형식) 문자열을 공간 데이터로 변환.
- 좌표 순서 주의
- PostGIS는
POINT(경도 위도) 순서임.
INSERT INTO cafes (name, location) VALUES
('스타벅스 강남점', ST_GeomFromText('POINT(127.0276 37.4979)', 4326)),
('블루보틀 성수점', ST_GeomFromText('POINT(127.0553 37.5445)', 4326)),
('메가커피 종로점', ST_GeomFromText('POINT(126.9894 37.5700)', 4326));
ST_SetSRID(ST_MakePoint(경도, 위도), 4326) 방식에서도 순서가 동일.
INSERT INTO cafes (name, location) VALUES
('투썸 홍대점', ST_SetSRID(ST_MakePoint(126.9234, 37.5563), 4326));
ST_GeomFromText는 문자열을 파싱해서 변환하는 과정이 있어서 상대적으로 느림.
- WKT 문자열을 직접 다루거나 가독성이 중요할 때 사용.
ST_MakePoint는 숫자를 바로 받아서 도형을 생성하기 때문에 더 효율적
- 애플리케이션에서 좌표값을 변수로 넘길 때 사용.
4-2. 거리 계산
| 함수 | 반환 타입 | 설명 |
|---|
ST_Distance(a, b) | float | 두 도형 사이의 거리 |
SELECT
name,
ROUND(
ST_Distance(
location::geography,
ST_GeomFromText('POINT(126.9784 37.5665)', 4326)::geography
)::numeric
) AS distance_m
FROM cafes
ORDER BY distance_m;
::: 형변환(type casting) 연산자.
::geography: geometry 타입 -> geography 타입으로 변환.
- 변환하는 이유 -> 지구 곡률을 반영한
실제 거리(미터)를 계산함.
- 기본
geometry 타입에서 ST_Distance를 쓰면 단위가 도(degree)로 나와서 거리를 알기 어려움.
geography로 변환하면 PostGIS가 지구가 둥글다는 것을 계산에 반영하여 단위를 미터(m)로 출력해줌.
4-3. 반경 내 검색
| 함수 | 반환 타입 | 설명 |
|---|
ST_DWithin(a, b, r) | boolean | 반경 r 이내 여부 |
SELECT name
FROM cafes
WHERE ST_DWithin(
location::geography,
ST_GeomFromText('POINT(126.9784 37.5665)', 4326)::geography,
3000
);
4-4. 특정 지역 내 포함 여부
- 특정 점(Point)이 지정한 다각형(Polygon, 영역) 안에 완전히 들어가 있는지 확인.
- 좌표의 마지막은 반드시 첫 번째 좌표와 같아야 합니다(도형을 닫아야 함).
SELECT ST_Contains(
ST_GeomFromText('POLYGON((
127.01 37.49, 127.07 37.49,
127.07 37.52, 127.01 37.52,
127.01 37.49
))', 4326),
ST_GeomFromText('POINT(127.0276 37.4979)', 4326)
) AS is_in_gangnam;
4-5. 자주 쓰는 공간 함수
| 함수 | 반환 타입 | 설명 |
|---|
ST_Distance(a, b) | float | 두 도형 사이의 거리 |
ST_DWithin(a, b, r) | boolean | 반경 r 이내 여부 |
ST_Contains(a, b) | boolean | a가 b를 완전히 포함하는지 |
ST_Intersects(a, b) | boolean | 두 도형이 겹치는지 |
ST_Area(geom) | float | 면적 계산 (geography → m²) |
ST_Length(geom) | float | 선의 길이 |
ST_Centroid(geom) | geometry | 도형의 중심점 |
ST_Buffer(geom, r) | geometry | 반경 r의 버퍼 도형 생성 |
ST_Intersection(a, b) | geometry | 두 도형의 교집합 |
ST_Union(a, b) | geometry | 두 도형의 합집합 |
ST_AsText(geom) | text | WKT 형식 문자열로 변환 |
ST_AsGeoJSON(geom) | text | GeoJSON 형식으로 변환 |
6. 공간 인덱스 (GIST Index)
6-1. 왜 공간 인덱스가 필요한가?
- 일반 B-Tree 인덱스는 숫자/문자열 비교에 최적화되어 있음.
- 2D 공간 데이터의
겹침·포함 연산에는 사용할 수 없음.
PostGIS는 이를 위해 GIST(Generalized Search Tree)라는 인덱스를 사용함.
GiST는 내부적으로 R-Tree(Rectangle Tree) 구조를 사용하여 공간 데이터를 관리.
6-2. 인덱스 생성.
기본 GIST 공간 인덱스
CREATE INDEX idx_cafes_location
ON cafes USING GIST(location);
- location 컬럼이
GEOMETRY 타입이므로 평면 좌표 기준으로 인덱스를 만듦.
CREATE INDEX idx_cafes_location_geo
ON cafes USING GIST(location::geography);
- location 컬럼을
geography로 형변환한 상태로 인덱스를 만듦.
6-2-1. 인덱스 사용.
- PostgreSQL은 쿼리의 타입과 인덱스의 타입이 일치할 때만 인덱스를 사용함.
- 형변환을 하면 PostgreSQL 입장에서는 다른 타입으로 인식하기 때문.
WHERE ST_DWithin(location, '...', 5000)
WHERE ST_DWithin(location::geography, '...', 5000)
WHERE ST_DWithin(location, '...', 5000)
WHERE ST_DWithin(location::geography, '...', 5000)
| 상황 | 권장 |
|---|
| 서비스범위가 좁음 (특정 도시 내) | geometry 인덱스 |
| 전국/글로벌 서비스, 실거리 계산 필요 | geography 인덱스 |
| 두 타입 쿼리가 혼재 | 둘 다 생성 |
- 대부분의 위치 기반 서비스는
::geography로 실거리를 계산하니까 geography 인덱스 하나만 만드는 경우가 많음.
6-3. 인덱스 팁
- 대용량 데이터 삽입 후에는
VACUUM ANALYZE로 통계 정보를 갱신.
::geography 형변환 쿼리가 많다면 geography 기반 인덱스를 별도로 생성.
- 일반적인 공간 검색에는 GIST 인덱스를 사용하는 것이 표준.
7. geometry vs geography
| 구분 | geometry | geography |
|---|
| 기준 | 평면 좌표계 (2차원 평면(가로, 세로) 위에서 취급) | 지구 구면체 (둥근 지구(구면체) 위에 있는 위치로 취급) |
| 거리 단위 | 도 (degree) | 미터 (m) |
| 정확도 | 좁은 지역은 정확, 넓으면 오차 큼 | 실제 거리 정확 |
| 연산 속도 | 빠름 | 상대적으로 느림 |
| 권장 상황 | 소규모 영역 (도시 내) | 전국/글로벌 서비스 |
8. Ex) 배달앱 주변 가게 검색
CREATE TABLE restaurants (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(50),
location GEOMETRY(Point, 4326)
);
INSERT INTO restaurants (name, category, location) VALUES
('맛있는 한식당', '한식', ST_SetSRID(ST_MakePoint(126.988439, 37.532301), 4326)),
('차이나타운', '중식', ST_SetSRID(ST_MakePoint(126.962202, 37.546571), 4326)),
('도쿄식당', '일식', ST_SetSRID(ST_MakePoint(126.995426, 37.579222), 4326)),
('브런치카페', '양식', ST_SetSRID(ST_MakePoint(127.006637, 37.536760), 4326)),
('쫄면천국', '분식', ST_SetSRID(ST_MakePoint(126.972778, 37.532645), 4326)),
('할리스', '카페', ST_SetSRID(ST_MakePoint(126.958142, 37.566886), 4326)),
('교촌치킨', '치킨', ST_SetSRID(ST_MakePoint(126.944311, 37.544816), 4326)),
('미스터피자', '피자', ST_SetSRID(ST_MakePoint(126.989192, 37.569736), 4326)),
('쉐이크쉑', '버거', ST_SetSRID(ST_MakePoint(126.958272, 37.572927), 4326)),
('명품족발', '족발/보쌈', ST_SetSRID(ST_MakePoint(127.000679, 37.530968), 4326)),
('참나물 밥집 본점', '한식', ST_SetSRID(ST_MakePoint(127.000419, 37.580766), 4326)),
('차이나타운 본점', '중식', ST_SetSRID(ST_MakePoint(126.966898, 37.541695), 4326)),
('도쿄식당 본점', '일식', ST_SetSRID(ST_MakePoint(127.011319, 37.554735), 4326)),
('브런치카페 본점', '양식', ST_SetSRID(ST_MakePoint(126.949078, 37.537464), 4326)),
('쫄면천국 본점', '분식', ST_SetSRID(ST_MakePoint(127.003420, 37.573968), 4326)),
('메가커피 본점', '카페', ST_SetSRID(ST_MakePoint(127.000513, 37.583041), 4326)),
('교촌치킨 본점', '치킨', ST_SetSRID(ST_MakePoint(126.981008, 37.600564), 4326)),
('미스터피자 본점', '피자', ST_SetSRID(ST_MakePoint(126.969654, 37.570247), 4326)),
('쉐이크쉑 본점', '버거', ST_SetSRID(ST_MakePoint(127.002117, 37.575033), 4326)),
('명품족발 본점', '족발/보쌈', ST_SetSRID(ST_MakePoint(127.004443, 37.572069), 4326)),
('고향 식당 강남점', '한식', ST_SetSRID(ST_MakePoint(126.993129, 37.533799), 4326)),
('차이나타운 강남점', '중식', ST_SetSRID(ST_MakePoint(126.958809, 37.551336), 4326)),
('도쿄식당 강남점', '일식', ST_SetSRID(ST_MakePoint(126.948145, 37.547261), 4326)),
('브런치카페 강남점', '양식', ST_SetSRID(ST_MakePoint(126.949672, 37.550514), 4326)),
('쫄면천국 강남점', '분식', ST_SetSRID(ST_MakePoint(126.988169, 37.556768), 4326)),
('스타벅스 강남점', '카페', ST_SetSRID(ST_MakePoint(126.969053, 37.545585), 4326)),
('교촌치킨 강남점', '치킨', ST_SetSRID(ST_MakePoint(126.961622, 37.597939), 4326)),
('미스터피자 강남점', '피자', ST_SetSRID(ST_MakePoint(126.989059, 37.574357), 4326)),
('쉐이크쉑 강남점', '버거', ST_SetSRID(ST_MakePoint(126.954722, 37.582997), 4326)),
('명품족발 강남점', '족발/보쌈', ST_SetSRID(ST_MakePoint(126.954165, 37.557821), 4326)),
('맛있는 한식당 종로점', '한식', ST_SetSRID(ST_MakePoint(127.013646, 37.576580), 4326)),
('차이나타운 종로점', '중식', ST_SetSRID(ST_MakePoint(126.982500, 37.579792), 4326)),
('도쿄식당 종로점', '일식', ST_SetSRID(ST_MakePoint(127.003085, 37.586372), 4326)),
('브런치카페 종로점', '양식', ST_SetSRID(ST_MakePoint(126.958891, 37.532811), 4326)),
('쫄면천국 종로점', '분식', ST_SetSRID(ST_MakePoint(126.965113, 37.549777), 4326)),
('할리스 종로점', '카페', ST_SetSRID(ST_MakePoint(126.957591, 37.598389), 4326)),
('교촌치킨 종로점', '치킨', ST_SetSRID(ST_MakePoint(127.005498, 37.553157), 4326)),
('미스터피자 종로점', '피자', ST_SetSRID(ST_MakePoint(126.989592, 37.558985), 4326)),
('쉐이크쉑 종로점', '버거', ST_SetSRID(ST_MakePoint(127.008247, 37.563537), 4326)),
('명품족발 종로점', '족발/보쌈', ST_SetSRID(ST_MakePoint(126.961471, 37.548257), 4326)),
('참나물 밥집 홍대점', '한식', ST_SetSRID(ST_MakePoint(126.982819, 37.549417), 4326)),
('차이나타운 홍대점', '중식', ST_SetSRID(ST_MakePoint(126.984490, 37.595143), 4326)),
('도쿄식당 홍대점', '일식', ST_SetSRID(ST_MakePoint(126.971157, 37.546291), 4326)),
('브런치카페 홍대점', '양식', ST_SetSRID(ST_MakePoint(127.014223, 37.567186), 4326)),
('쫄면천국 홍대점', '분식', ST_SetSRID(ST_MakePoint(126.948945, 37.533892), 4326)),
('메가커피 홍대점', '카페', ST_SetSRID(ST_MakePoint(126.950295, 37.575676), 4326)),
('교촌치킨 홍대점', '치킨', ST_SetSRID(ST_MakePoint(126.999430, 37.560896), 4326)),
('미스터피자 홍대점', '피자', ST_SetSRID(ST_MakePoint(126.946974, 37.557977), 4326)),
('쉐이크쉑 홍대점', '버거', ST_SetSRID(ST_MakePoint(127.014121, 37.568596), 4326)),
('명품족발 홍대점', '족발/보쌈', ST_SetSRID(ST_MakePoint(127.012318, 37.592476), 4326));
CREATE INDEX idx_restaurants_location
ON restaurants USING GIST(location);
SELECT
name,
category,
ROUND(ST_Distance(
location::geography,
ST_GeomFromText('POINT(126.9784 37.5665)', 4326)::geography
)::numeric) AS distance_m
FROM restaurants
WHERE ST_DWithin(
location::geography,
ST_GeomFromText('POINT(126.9784 37.5665)', 4326)::geography,
2000
)
ORDER BY distance_m
LIMIT 20;