PostGIS 시작하기 — CREATE EXTENSION 한 줄이 바꾸는 것

파일은 질문에 답하지 않는다
지난 편에서 우리는 한국 전체 지도를 파일 하나로 손에 넣었습니다. 그런데 여기서 "강남역 반경 500m 안의 편의점" 을 뽑으려면 어떻게 할까요? 파일을 열어 노드 3,800만 개를 처음부터 끝까지 훑는 수밖에 없습니다. 하나 물어볼 때마다요. 파일은 데이터를 담을 뿐, 질문에 답하지 않으니까요. 답하게 만들려면 데이터베이스가 필요한데 — 놀랍게도 그건 새 데이터베이스를 설치하는 일이 아닙니다. 평범한 PostgreSQL에 한 줄을 실행하는 일이에요.
Phase 3에서 포맷을, Phase 4에서 데이터를 손에 넣었습니다. 이제 Phase 6 — PostGIS입니다. 오늘은 그 CREATE EXTENSION postgis; 한 줄이 정확히 무엇을 바꾸는지 카탈로그를 세어 확인하고, 새로 생긴 geometry 타입이 실제로 어떤 바이트인지 뜯어봅니다.
파일로는 왜 안 되나
우리가 지나온 포맷들을 떠올려 보죠. FlatGeobuf는 공간 인덱스를 품어 화면에 보이는 조각만 가져왔습니다. 꽤 영리했어요. 그런데 그건 "이 사각형 범위의 데이터를 다오" 였지, "이 점에서 500m 안에 있는 편의점 중 24시간 영업하는 곳을 가까운 순으로" 가 아니었습니다.
파일이 못 하는 게 이겁니다.
| 파일 | 데이터베이스 | |
|---|---|---|
| 질의 | 범위 읽기 정도 | 조건 조합·정렬·집계 |
| 동시 쓰기 | 사실상 1명 | 여럿 |
| 인덱스 | 있어도 고정 | 필요한 만큼 추가 |
| 수정 | 대개 통째로 다시 굽기 | UPDATE 한 줄 |
GeoPackage 편에서 "여럿이 동시에 편집하려면 PostGIS의 영역"이라고 미뤄 뒀던 그 지점에 드디어 도착했습니다.
30초 만에 PostGIS 띄우기
가장 빠른 길은 Docker입니다. 이미지 안에 PostgreSQL과 PostGIS가 이미 들어 있어요.
docker run --name gis -e POSTGRES_PASSWORD=postgres \
-p 5432:5432 -d postgis/postgis:17-3.5
끝입니다. 17-3.5는 PostgreSQL 17 + PostGIS 3.5 라는 뜻이에요. 접속은 이렇게 합니다.
docker exec -it gis psql -U postgres
💡 직접 설치하고 싶다면 — Windows는 EDB 설치 프로그램으로 PostgreSQL을 깔고 함께 실행되는 Stack Builder에서 PostGIS 번들을 고르면 됩니다. macOS는
brew install postgis, 데비안·우분투 계열은apt install postgresql-17-postgis-3이에요. 다만 이 글의 실험은 완전히 빈 데이터베이스가 필요해서 컨테이너로 진행합니다. 이미 쓰던 DB엔 확장이 켜져 있을 수 있거든요.
CREATE EXTENSION 전후를 재본다
자, 본론입니다. 그 한 줄이 대체 뭘 하길래 데이터베이스가 지도를 이해하게 될까요? 세어 보면 됩니다.
PostgreSQL은 자기가 아는 함수·타입·연산자를 전부 시스템 카탈로그라는 테이블에 적어 둡니다. 그러니 확장을 켜기 전후로 이 테이블들을 세면 무엇이 늘었는지 정확히 알 수 있어요.
먼저 갓 만든 빈 데이터베이스에서 세어 봅니다.
CREATE DATABASE gis_test;
\c gis_test
SELECT count(*) FROM pg_proc; -- 함수
SELECT count(*) FROM pg_type; -- 타입
SELECT count(*) FROM pg_operator; -- 연산자
SELECT count(*) FROM pg_opclass; -- 인덱스 연산자 클래스
그리고 문제의 한 줄을 실행합니다.
CREATE EXTENSION postgis;
CREATE EXTENSION
Time: 367.795 ms
0.37초. 다시 세어 봤습니다.
| 카탈로그 | 실행 전 | 실행 후 | 증가 |
|---|---|---|---|
함수 (pg_proc) | 3,330 | 4,106 | +776 |
타입 (pg_type) | 318 | 330 | +12 |
연산자 (pg_operator) | 799 | 851 | +52 |
인덱스 연산자 클래스 (pg_opclass) | 177 | 191 | +14 |
public 테이블 | 0 | 1 | +1 |
public 뷰 | 0 | 2 | +2 |
함수 776개가 0.37초 만에. 이게 그 한 줄의 정체입니다.
증가분이 진짜 PostGIS 몫인지 확인하는 방법도 있어요. PostgreSQL은 각 객체가 어느 확장에 속하는지를 pg_depend에 기록하거든요.
SELECT count(*) FROM pg_depend d
JOIN pg_proc p ON p.oid = d.objid
WHERE d.refobjid = (SELECT oid FROM pg_extension WHERE extname='postgis')
AND d.deptype = 'e';
776
정확히 일치합니다. 연산자도 52개로 같고요. 여기에 집계 함수 22개가 덤으로 붙습니다.
새로 생긴 것들
타입 12개 전부를 뽑으면 이렇습니다.
box2d, box2df, box3d, geography, geography_columns,
geometry, geometry_columns, geometry_dump, gidx,
spatial_ref_sys, spheroid, valid_detail
주인공은 geometry 와 geography 예요. 나머지는 조연입니다 — box2d·box3d는 경계 상자, gidx·box2df는 인덱스 내부용이고요.
💡 조연이라고 안 쓰이는 건 아닙니다. 각자 자기 편이 따로 있어요.
box2d·box3d·geometry_dump— 공간 인덱스가 왜 어떤 순간엔 안 먹는지 파헤칠 때(Phase 7). 인덱스는 도형을 통째로 비교하지 않고 경계 상자로 먼저 거릅니다.gidx·box2df— 인덱스가 도형 대신 들고 다니는 압축된 경계 상자입니다. 인덱스가 왜 도형을 통째로 안 들고 있는지는 Phase 9에서 다룹니다.valid_detail— 깨진 지오메트리를 진단할 때(Phase 7). "이 폴리곤이 왜 유효하지 않은가"를 담아 돌려주는 타입입니다.spheroid— 조금 뒤geography절에서 만납니다. 지구를 타원체로 볼 때 쓰는 그 타원체거든요.
테이블 1개와 뷰 2개도 생겼습니다.
테이블 : spatial_ref_sys ← 좌표계 사전
뷰 : geometry_columns ← 어느 테이블의 어느 칼럼이 지오메트리인가
geography_columns
geometry_columns는 지금은 텅 비어 있지만, 다음 편에서 데이터를 적재하고 나면 "제대로 들어갔는지" 가장 먼저 확인할 창구가 됩니다. SRID를 빠뜨렸거나 타입이 어긋났으면 여기서 드러나거든요.
연산자 52개는 이런 것들입니다.
&& geometry, geometry 경계 상자가 겹치는가
<-> geometry, geometry 두 도형 사이 거리 (KNN 정렬용)
@ geometry, geometry 왼쪽이 오른쪽에 포함되는가
<-> 가 특히 재미있어요. ORDER BY 위치 <-> '내 좌표' 처럼 쓰면 가장 가까운 것부터 정렬되는데, 이게 인덱스를 그대로 타고 돕니다. 나중에 "가장 가까운 소방서 5곳"을 구할 때 쓸 무기예요.
&& 도 기억해 두세요. 이름값과 달리 "두 도형이 겹치는가"가 아니라 "두 도형의 경계 상자가 겹치는가" 입니다. 대충 거르는 것처럼 보이지만, 사실 모든 공간 쿼리가 이 값싼 검사를 먼저 통과한 뒤에야 진짜 도형 비교로 넘어가요. 인덱스가 빠른 이유의 절반이 여기 있고, 반대로 이 필터가 안 걸리는 순간 쿼리가 무너집니다. 그 이야기는 Phase 7에서 따로 합니다.
그래서 '확장'이 뭔데
여기서 한 발 물러나 봅시다. 왜 이런 게 가능할까요?
보통 라이브러리는 "함수 몇 개를 더해주는 것"입니다. 그런데 PostGIS는 타입을 만들고, 연산자를 만들고, 심지어 인덱스가 그 타입을 다루는 방법까지 등록했어요. 앞 표에서 슬쩍 지나간 pg_opclass +14 가 그 마지막 조각인데, 이게 제일 흥미로운 부분이라 따로 풀어보겠습니다.
도형은 줄을 세울 수가 없다
인덱스가 빠른 원리는 책 뒤의 찾아보기와 같습니다. 전부 뒤지지 않고 바로 그 페이지로 가죠. 흔히 쓰는 B-tree가 그 일을 하는데, B-tree가 돌아가려면 필요한 게 딱 하나뿐이에요.
"a가 b보다 작니?"
이 질문에만 답할 수 있으면 됩니다. 숫자든 문자열이든 날짜든, 줄을 세울 수 있으면 정렬해두고 반씩 잘라 찾을 수 있으니까요.
그런데 도형은요? POLYGON(강남구)와 POLYGON(서초구) 중 뭐가 "더 작을까요?"
- 면적으로 세우면? "면적 300 이상"은 찾겠지만, "이 점을 품는 구" 는 못 찾습니다
- x좌표로 세우면? y가 통째로 무시됩니다 — FlatGeobuf 편에서 본 그 함정이죠
도형은 1차원 줄로 세울 수가 없습니다. 그래서 B-tree를 못 씁니다.
그래서 '비교 방법을 받는 인덱스'가 있다
이 문제는 사실 우리가 자바스크립트에서 매일 푸는 것과 같습니다.
arr.sort() // 기본 비교 — 숫자·문자열만 됨
arr.sort((a, b) => a.age - b.age) // 비교 "방법"을 내가 넘겨줌
sort의 알고리즘은 그대로입니다. 바뀌는 건 "뭐가 더 앞이냐"를 판단하는 콜백뿐이죠. 덕분에 sort 하나로 어떤 객체든 정렬할 수 있고요.
인덱스에도 똑같은 물건이 있습니다. GiST(Generalized Search Tree) — 1995년 논문에서 나온, 비교 방법을 밖에서 받는 인덱스예요.
sort 세계 | DB 세계 |
|---|---|
sort 알고리즘 | GiST — 트리를 만들고 유지하는 뼈대 |
넘겨주는 (a,b) => … | 연산자 클래스 — 그 타입을 어떻게 다룰지 알려주는 함수 묶음 |
그래서 PostGIS가 한 일은 "도형용 인덱스를 새로 만든 것"이 아닙니다. 이미 있는 GiST에게 geometry를 다루는 방법을 콜백으로 제출한 것이죠. pg_opclass +14가 그 제출 기록입니다.
💡
+14는 GiST용만이 아니에요. GiST 3개, SP-GiST 4개, BRIN 4개, B-tree 2개, 해시 1개 — 인덱스 방식마다 따로 냈습니다. 콜백이 구체적으로 무엇을 요구하는지(요약·합치기·쪼개기…), 그 결과로 만들어지는 트리가 왜 FlatGeobuf에서 본 R-tree와 같은 모양이 되는지는 Phase 9(공간 인덱스 편) 에서 제대로 뜯어봅니다.
쓸 때는 한 줄
원리는 그렇고, 실제로 인덱스를 만들 땐 이렇게 씁니다.
CREATE INDEX ON t USING GIST (geom);
평소 쓰던 것과 나란히 놓으면 뭐가 다른지 바로 보여요.
CREATE INDEX ON users (age); -- 방식 생략 = B-tree (기본값)
CREATE INDEX ON t USING GIST (geom); -- 도형은 B-tree가 안 되니 명시
USING GIST는 "이 칼럼은 GiST 방식으로 색인해라" 는 지시입니다. 이 한 줄을 실행하면 PostgreSQL이 "geometry + GiST 조합의 연산자 클래스가 등록돼 있나?" 를 찾아보고, 있으니까 그 콜백들을 불러가며 트리를 세웁니다.
⚠️
USING GIST를 빠뜨려도 에러가 안 납니다.geometry용 B-tree 연산자 클래스도 등록돼 있어서 인덱스가 멀쩡히 만들어져요. 그런데 그건 정렬·동등비교용이라 공간 검색에는 전혀 쓰이지 않습니다. "인덱스를 걸었는데 왜 여전히 느리지?" 의 흔한 범인입니다.
그리고 PostGIS 혼자 하는 것도 아니다
여기까지가 PostgreSQL 쪽 이야기라면, 실제 계산은 또 다른 데서 옵니다.
SELECT postgis_full_version();
POSTGIS="3.5.2" [EXTENSION] PGSQL="170"
GEOS="3.9.0-CAPI-1.16.2"
PROJ="7.2.1"
LIBXML="2.9.10" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0"
이름들이 눈에 익지 않나요? GEOS는 도형 연산(교차·합집합·버퍼)을, PROJ는 좌표계 변환을 맡습니다. 좌표 변환 편에서 만난 그 PROJ 맞아요.
뒤집어 말하면 — 도형 계산은 PostGIS가 하지 않습니다. ST_Buffer를 부르면 PostGIS는 geometry 바이트를 GEOS가 아는 형태로 바꿔 넘기고, 돌아온 답을 다시 포장할 뿐이에요. Python의 Shapely도, QGIS도 같은 GEOS를 씁니다. 셋의 계산 결과가 똑같은 이유죠.
그럼 PostGIS가 한 일은 뭐냐 — geometry라는 타입을 새로 정의한 것입니다. 앞에서 센 pg_type +12가 그 기록이고요. 계산 능력은 20년 묵은 GEOS에서 빌려오되, integer·text와 동등한 자격을 갖춰 칼럼에 담기고 WHERE에 걸리고 인덱스를 세울 수 있는 값으로 만든 건 PostGIS 몫입니다. 그게 없으면 도형은 그냥 문자열이고, 전체 스캔 말곤 방법이 없어요.
geometry 타입의 정체 — 25바이트를 뜯어보자
새로 생긴 geometry가 대체 뭔지 하나 만들어 볼 텐데, 그 전에 도형을 글자로 적는 법부터 알아야 합니다.
도형을 글자로 — WKT
지난 포맷 편에서 "PostGIS에서 매일 보게 될 물건"이라며 얼굴만 익혀 둔 게 있죠. WKT(Well-Known Text) 입니다.
POINT(126.9779 37.5663)
LINESTRING(126.97 37.56, 127.02 37.49)
POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))
도형 종류를 쓰고 괄호 안에 좌표를 나열하는 게 전부입니다. 딱 하나만 조심하면 돼요.
⚠️ 한 점 안에서 x와 y는 공백으로, 점과 점 사이는 쉼표로 구분합니다.
POINT(126.9779, 37.5663)처럼 쉼표를 넣으면 이렇게 됩니다.textERROR: parse error - invalid geometry HINT: "POINT(126.9779, 3" <-- parse error at position 17
WKT는 국제 표준이라 좌표계 정보를 담지 않습니다. 그래서 PostGIS는 앞에 한 조각을 덧붙인 EWKT(Extended WKT) 를 씁니다.
SRID=4326;POINT(126.9779 37.5663)
└───┬───┘ └────────┬────────────┘
좌표계 표준 WKT
이제 만들어 봅시다.
SELECT 'SRID=4326;POINT(126.9779 37.5663)'::geometry;
💡
::가 낯설다면 —CAST(값 AS 타입)의 PostgreSQL식 축약입니다. 우리가 늘 쓰던 것과 같은 문법이에요.sqlSELECT '42'::integer; -- 문자열 → 정수 SELECT '2026-08-12'::date; -- 문자열 → 날짜 SELECT 'POINT(126.9779 37.5663)'::geometry; -- 문자열 → 도형TypeScript의
as와 달리 실제로 변환 함수가 돌아갑니다. 그래서 형식이 틀리면 위처럼 에러가 나요.뒤에 나올 문법 둘도 미리 짚어둘게요.
WITH 이름 AS (…)는 결과에 임시 이름을 붙여 두고 아래에서 갖다 쓰는 것 — SQL의const라고 보면 됩니다. 같은 긴 식을 여섯 번 쓰지 않으려고 씁니다. 그리고\c(데이터베이스 갈아타기)·\d(테이블 구조 보기)는 SQL이 아니라 psql 전용 명령입니다.
돌아오는 건 이겁니다.
0101000020E61000008AB0E1E995BE5F40C6DCB5847CC84240
16진수 50글자 = 25바이트. PBF·FlatGeobuf 때 했던 것처럼 잘라 봅시다. 이 형식을 EWKB(Extended Well-Known Binary) 라고 불러요.
| 바이트 | 값 | 뜻 |
|---|---|---|
[0] | 01 | 바이트 오더 — 01이면 리틀엔디언 |
[1..4] | 01000020 | 타입 — 리틀엔디언으로 읽으면 0x20000001 |
[5..8] | E6100000 | SRID — 0x10E6 = 4326 |
[9..16] | 8AB0E1E995BE5F40 | X — IEEE 754 double = 126.9779 |
[17..24] | C6DCB5847CC84240 | Y — 같은 방식 = 37.5663 |
타입 필드를 눈여겨보세요. 0x20000001을 쪼개면 끝자리 1이 "점(Point)" 이고, 앞의 0x20000000이 "SRID가 붙어 있다"는 깃발입니다. 실제로 SRID 없이 만들면 그 깃발과 4바이트가 통째로 사라집니다.
SELECT 'POINT(126.9779 37.5663)'::geometry;
01010000008AB0E1E995BE5F40C6DCB5847CC84240 ← 21바이트 (4바이트 짧다)
↑
01000000 — SRID 깃발이 없다
좌표가 문자열이 아니라 8바이트 실수라는 게 핵심입니다. 126.9779라는 열 글자가 아니라 8AB0E1E995BE5F40 이라는 고정 8바이트죠. PBF가 정수로 눌러 담았다면, PostGIS는 배정밀도 실수를 그대로 씁니다 — 계산 정확도가 우선이니까요.
물론 사람이 볼 땐 되돌리면 됩니다.
SELECT ST_AsText(g), ST_AsEWKT(g), ST_AsGeoJSON(g) …
ST_AsText : POINT(126.9779 37.5663)
ST_AsEWKT : SRID=4326;POINT(126.9779 37.5663)
ST_AsGeoJSON: {"type":"Point","coordinates":[126.9779,37.5663]}
마지막 줄, 낯익죠? GeoJSON 편에서 본 그 구조 그대로입니다. PostGIS는 안에선 바이너리로 들고 있다가, 요청하면 원하는 표현으로 꺼내 줍니다.
spatial_ref_sys — EPSG가 테이블로 들어 있다
아까 생긴 테이블 하나, spatial_ref_sys를 열어 봅시다.
SELECT count(*) FROM spatial_ref_sys;
8500
좌표계 8,500개. EPSG 코드 편에서 "좌표계마다 번호가 있다"고 배운 그 번호들이, 행으로 들어 있는 겁니다.
SELECT auth_name, count(*) FROM spatial_ref_sys GROUP BY 1 ORDER BY 2 DESC;
EPSG 6184
ESRI 2315
spatialreferencing.org 1
그리고 우리의 5179를 찾아보면 —
SELECT proj4text FROM spatial_ref_sys WHERE srid = 5179;
+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000
+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs
좌표 변환 편에서 "proj4js는 한국 좌표계를 몰라서 이 정의를 직접 등록해줘야 한다"고 했던, 바로 그 문자열입니다. PostGIS는 8,500개를 처음부터 들고 시작해요. 그래서 ST_Transform(geom, 5179) 한 줄이면 끝나는 거고요.
💡 이 테이블이 7MB입니다. PostGIS를 켠 데이터베이스가 빈 상태에서도 15MB쯤 되는데, 그 절반이 좌표계 사전인 셈이에요.
첫 공간 쿼리, 그리고 단위 함정
이제 진짜 질문을 던져 봅시다. 서울시청에서 강남역까지 얼마나 될까요?
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(126.9779, 37.5663), 4326),
ST_SetSRID(ST_MakePoint(127.0276, 37.4979), 4326)
);
0.08455
0.08. 그런데 0.08 뭘까요? 미터도 킬로미터도 아닙니다. 단위가 안 나옵니다.
여기가 함정입니다. ST_Distance는 좌표계의 단위를 그대로 씁니다. 그리고 4326은 위경도 좌표계고, 위경도의 단위는 미터가 아니라 도(degree) 예요. 그러니 저 숫자는 0.08도입니다.
게다가 계산 방식도 우리가 기대한 게 아니에요. 경도 차이와 위도 차이를 그냥 피타고라스로 더한 값이거든요. 그런데 위도 1도는 어디서나 약 111km인 반면 경도 1도는 적도에서 111km, 서울에서 88km, 극지방에선 0에 수렴합니다. 서로 길이가 다른 두 자를 섞어 더한 셈이라, 이 숫자에 뭘 곱해도 정확한 거리가 나오지 않습니다.
⚠️ "도에 몇을 곱하면 km" 같은 어림셈으로는 이 숫자를 되살릴 수 없습니다.
text0.08455 × 111 = 9.39 km ← 너무 길다 0.08455 × 88 = 7.44 km ← 너무 짧다 진짜 답 = 8.77 km ← 둘 사이 어딘가어떤 수를 곱해도 안 맞습니다.
0.08455는 이미 경도 차이(0.0497도)와 위도 차이(0.0684도)를 하나로 합쳐버린 값이라, 서로 다른 두 환산율을 뒤늦게 갈라 적용할 방법이 없거든요.순서를 뒤집으면 맞습니다 — 축별로 먼저 환산하고, 그다음 합치면 돼요.
text가로 : 0.0497 × 88.3 = 4.389 km 세로 : 0.0684 × 111.0 = 7.591 km → √(4.389² + 7.591²) = 8.769 km (실측 8.771km, 오차 2m)그러니 도 단위로 나온 결과는 되살릴 수 없게 망가진 값입니다. 재고 나서 고치는 게 아니라 재기 전에 단위를 맞춰야 하는 이유예요.
미터로 받으려면 방법이 둘 있습니다. 두 점을 a·b로 묶어두고 세 가지를 한 번에 재 봅시다.
WITH pts AS (
SELECT
ST_SetSRID(ST_MakePoint(126.9779, 37.5663), 4326) AS a, -- 서울시청
ST_SetSRID(ST_MakePoint(127.0276, 37.4979), 4326) AS b -- 강남역
)
SELECT
ST_Distance(a, b) AS deg,
-- ① geography로 캐스팅 — 지구를 타원체로 보고 표면을 따라 잰다
ST_Distance(a::geography, b::geography) AS m_geography,
-- ② 미터 단위 좌표계로 변환 — 한국이면 5179
ST_Distance(ST_Transform(a, 5179), ST_Transform(b, 5179)) AS m_5179
FROM pts;
deg | m_geography | m_5179
----------+-------------+--------
0.084550 | 8770.9 | 8767.6
정리하면 이렇습니다.
| 방법 | 결과 | 단위 |
|---|---|---|
geometry (4326) 그대로 | 0.084550 | 도 ← 함정 |
::geography | 8770.9 | 미터 |
ST_Transform(…, 5179) | 8767.6 | 미터 |
약 8.77km. 실제 거리와 맞습니다.
⚠️ 이게 PostGIS 입문자가 가장 자주 밟는 지뢰입니다. 에러가 나지 않아요.
0.08455라는 그럴듯한 숫자가 조용히 돌아올 뿐이죠.ST_DWithin(a, b, 500)처럼 "500m 안"을 의도한 쿼리가 500도 안을 뜻하게 되면 지구 전체가 걸려듭니다. 좌표 변환 편의 함정이 DB에서 이렇게 재현돼요.
geography와 5179가 3.3m 차이 나는 것도 이유가 있습니다. geography는 지구를 타원체로 보고 표면을 따라 재고, 5179는 그 지역을 평면에 펼친 뒤 자로 재거든요. 8.7km에서 3m면 0.04%니 대부분의 용도엔 무의미하지만, "왜 값이 다르지?" 싶을 때 떠올리면 됩니다.
geometry vs geography
방금 둘이 나왔으니 정리하고 갑시다.
geometry | geography | |
|---|---|---|
| 세계관 | 평면 | 타원체(둥근 지구) |
| 거리 단위 | 좌표계를 따름 (4326이면 도) | 항상 미터 |
| 속도 | 빠름 | 느림 |
| 함수 수 | 776개 대부분 | 일부만 |
| 좋은 경우 | 좁은 지역, 미터 좌표계로 변환해 씀 | 대륙·전 지구 규모, 변환하기 귀찮을 때 |
기본은 geometry 입니다. 한국처럼 좁은 범위를 다룬다면 5179 같은 미터 좌표계로 변환해 쓰는 게 빠르고 함수도 다 쓸 수 있어요. 지구 반대편까지 재야 하거나 좌표계 변환이 번거로울 때 geography를 꺼냅니다.
아까 미뤄 둔 spheroid 타입이 여기 있습니다. geography가 "타원체 위에서 잰다"고 할 때, 그 타원체의 모양을 담는 게 spheroid 예요. 지오이드와 타원체 편에서 본 GRS80·WGS84가 바로 이런 값들이고요. 평소엔 PostGIS가 알아서 WGS84를 쓰지만, ST_DistanceSpheroid처럼 타원체를 직접 지정하는 함수에선 이 타입이 겉으로 드러납니다.
정리 — 한 문장으로
PostGIS는 별도의 데이터베이스가 아니라, PostgreSQL의 열린 타입 시스템에
geometry타입과 함수 776개·연산자 52개를 등록하는 확장이다. 그 한 줄에 0.37초가 걸리고, 그때부터 DB는 "가까운 순으로 정렬"을 이해한다.
파일 (.osm.pbf 등) | PostGIS | |
|---|---|---|
| 정체 | 바이트 묶음 | PostgreSQL 확장 |
| 질문 | 못 함 | WHERE·ORDER BY·JOIN |
| 좌표계 | 파일마다 따로 | spatial_ref_sys 8,500개 내장 |
| 표현 | 포맷 고정 | WKT·GeoJSON·바이너리 자유 변환 |
| 함정 | — | 단위(도 vs 미터) |
이제 데이터베이스가 준비됐습니다. 그런데 아직 텅 비어 있어요. ST_MakePoint로 점 두 개 찍어 거리를 잰 게 전부죠.
다음 편에서 채웁니다. 먼저 Shapefile부터예요 — shp2pgsql과 ogr2ogr, 길이 여럿인데 그 길목마다 인코딩(CP949)과 SRID 지정이라는 함정이 기다립니다. 한국 행정동 데이터를 넣었더니 글자가 깨져 있는, 그 익숙한 장면부터 시작합니다.
우리가 만든 .osm.pbf는 그다음 편의 몫입니다. 도구도 osm2pgsql로 다르고, Node·Way·Relation을 테이블로 옮기는 스키마 설계 자체가 아예 다른 이야기거든요.
참고
- https://postgis.net/documentation/
- https://postgis.net/docs/postgis_installation.html
- https://postgis.net/docs/using_postgis_dbmanagement.html
- https://www.postgresql.org/docs/17/sql-createextension.html
- https://www.postgresql.org/docs/17/extend-extensions.html
- https://www.postgresql.org/docs/17/catalogs.html
- https://libgeos.org/specifications/wkb/
- https://libgeos.org/
- https://proj.org/
- https://registry.hub.docker.com/r/postgis/postgis
- https://epsg.io/