Ken
PostGIS는 25년에 걸쳐 만들어진 풀스택 vector GIS다. 그것을 다른 엔진으로 대체한다는 발상은 보통 무모하다 — 함수 커버리지만 보면. PostGIS의 약 80여 개 주요 함수 중 ClickHouse가 직접 대응하는 건 6%에 불과하다.하지만 실제 프로덕션 분석 워크로드의 80% 이상이 그 6% 안에서 끝난다.
ST_Distance,ST_DWithin, point 기준의ST_Contains, 그리고 좌표 추출 — 모빌리티·IoT·광고테크·풋트래픽 분석이 매일 호출하는 것은 거의 다 이 작은 집합이다. 함수 매트릭스의 비대칭성이 이 글의 출발점이다.이 글은 PostGIS 79개 주요 함수 전체 비교표를 그대로 보존한 뒤, 분석 워크로드를 PostGIS에서 ClickHouse로 옮기는 관점에서 — 어디가 1:1 replace 가능한지, 어디는 H3 패턴으로 재설계해서 replace 가능한지, 어디는 PostGIS를 남겨야 하는지 — 를 정리한다.
pg_clickhouse는 fallback이 아니라 마이그레이션 경로로 다룬다: 기존 PostGIS SQL을 그대로 유지하면서 데이터가 ClickHouse로 이전되게 하는 도구 (이전 글 pg_clickhouse 깊이 보기 참고).
1. PostGIS를 왜 대체하려는가 — 스케일의 함정
PostGIS 자체는 견고하다. 문제는 PostgreSQL의 행 기반 OLTP 엔진 위에 얹혀 있다는 것. 분석 워크로드가 커지면 다음 순서로 막힌다:
PostGIS 워크로드가 분석으로 가는 순간 row-store가 발목을 잡는다 — 5억 행 위에서 SELECT count(*) GROUP BY ST_GeoHash(geom)가 메모리에 다 안 들어가서 디스크 스필 → 분 단위 쿼리. 바로 이 지점이 ClickHouse가 들어올 자리다.
핵심 질문은 단순하다 — "내 PostGIS 워크로드 중 얼마만큼이 ClickHouse로 1:1 옮겨질 수 있고, 얼마만큼은 패턴을 재설계해야 하며, 얼마만큼은 PostGIS를 남겨야 하는가?" 다음 매트릭스가 그 답이다.
2. PostGIS ↔ ClickHouse 함수 매트릭스 (전체)
다음 표는 PostGIS의 주요 79개 함수에 대해 ClickHouse 대응 함수가 있는지 / 부분적인지 / 없는지를 카테고리별로 정리한 것. 이 표가 글의 가장 중요한 reference다.
범례:
- ✅ Supported — 동등하거나 거의 동등한 ClickHouse 함수 존재 (직접 replace 가능)
- ⚠️ Workaround / Point Only / WGS84 only — 제한 조건 하에 replace 가능 (조건 충족 시 OK)
- ❌ Not Supported — 직접 대응 없음 (H3 패턴으로 재설계하거나 PostGIS 유지)
2.1 Distance & Measurement
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_Distance | geoDistance(lon1, lat1, lon2, lat2) | ✅ Supported | Calculates distance between two points in meters (WGS84) | Proximity search, delivery radius, nearest neighbor, store locator |
ST_Length | greatCircleDistance() | ✅ Supported | Calculates great circle distance between points | Flight paths, shipping routes, long-distance planning |
ST_Area | (H3 approximation) | ⚠️ Workaround | Calculates polygon area | Land parcel sizing, coverage analysis, territory calculations |
ST_Perimeter | — | ❌ Not Supported | Calculates polygon boundary length | Fence estimation, boundary surveying, coastal measurement |
ST_Azimuth | — | ❌ Not Supported | Returns bearing angle between two points | Navigation direction, compass heading, directional analysis |
ST_Angle | — | ❌ Not Supported | Returns angle between three points | Turn angle calculation, road geometry, path direction |
ST_ClosestPoint | — | ❌ Not Supported | Returns closest point on geometry to another | Nearest point on road, snapping, shortest path start |
ST_ShortestLine | — | ❌ Not Supported | Returns shortest line between geometries | Distance visualization, connecting features, gap analysis |
ST_LongestLine | — | ❌ Not Supported | Returns longest line between geometries | Maximum extent, polygon diameter, coverage span |
ST_MaxDistance | — | ❌ Not Supported | Returns maximum distance between geometries | Polygon diameter, maximum separation, extent calculation |
ST_3DDistance | — | ❌ Not Supported | Calculates 3D distance with elevation | Aviation, drone paths, 3D building analysis, terrain |
ST_FrechetDistance | — | ❌ Not Supported | Measures curve similarity | GPS track comparison, route similarity, trajectory matching |
ST_HausdorffDistance | — | ❌ Not Supported | Measures shape distance | Shape similarity, polygon matching, change detection |
2.2 Spatial Relationships
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_Contains | pointInPolygon((x,y), polygon) | ⚠️ Point Only | Tests if geometry contains another | Geofencing, delivery zones, territory assignment |
ST_Within | pointInPolygon((x,y), polygon) | ⚠️ Point Only | Tests if geometry is within another | Service area check, indoor positioning, asset tracking |
ST_Intersects | (H3/S2 approximation) | ⚠️ Workaround | Tests if geometries share space | Route overlap, coverage analysis, network intersection |
ST_Overlaps | — | ❌ Not Supported | Tests if geometries overlap | Territory conflict, overlapping coverage, collision detection |
ST_Touches | — | ❌ Not Supported | Tests if geometries touch at boundary | Adjacent parcel detection, neighboring regions, boundary validation |
ST_Equals | — | ❌ Not Supported | Tests if geometries are equal | Duplicate detection, geometry comparison, data validation |
ST_Disjoint | — | ❌ Not Supported | Tests if geometries do not intersect | Exclusion zones, non-overlapping validation |
ST_Crosses | — | ❌ Not Supported | Tests if geometries cross each other | Road/river crossings, line intersection, path crossing |
ST_Covers | — | ❌ Not Supported | Tests if geometry covers another | Full coverage validation, containment with boundary |
ST_CoveredBy | — | ❌ Not Supported | Tests if geometry is covered by another | Subset validation, within with boundary |
ST_DWithin | geoDistance() < X | ✅ Supported | Tests if geometries are within distance | Proximity queries, nearby search, radius filtering |
ST_Relate | — | ❌ Not Supported | Returns DE-9IM intersection matrix | Complex spatial relationship analysis |
2.3 Geometry Constructors
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_MakePoint | (lon, lat) tuple | ✅ Supported | Creates a point geometry | Creating point data, coordinate storage |
ST_MakeLine | — | ❌ Not Supported | Creates a line from points | Route creation, path building, track generation |
ST_MakePolygon | — | ❌ Not Supported | Creates polygon from shell and holes | Area definition, boundary creation, zone building |
ST_MakeEnvelope | — | ❌ Not Supported | Creates rectangular polygon from bounds | Bounding box, viewport rectangle, extent definition |
ST_Collect | — | ❌ Not Supported | Collects geometries into collection | Grouping geometries, multi-geometry creation |
ST_BuildArea | — | ❌ Not Supported | Creates area from linework | Polygon construction from lines, topology building |
2.4 Geometry Accessors
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_GeometryType | — | ❌ Not Supported | Returns geometry type name | Type checking, data validation, conditional processing |
ST_NumPoints | — | ❌ Not Supported | Returns number of points | Complexity analysis, simplification decisions |
ST_NumGeometries | — | ❌ Not Supported | Returns number of geometries in collection | Multi-geometry counting, collection analysis |
ST_NRings | — | ❌ Not Supported | Returns number of rings in polygon | Hole counting, polygon complexity |
ST_IsClosed | — | ❌ Not Supported | Tests if geometry is closed | Ring validation, path closure check |
ST_IsEmpty | — | ❌ Not Supported | Tests if geometry is empty | Null geometry check, data validation |
ST_IsValid | — | ❌ Not Supported | Tests if geometry is valid | Data quality, topology validation, error detection |
ST_Dimension | — | ❌ Not Supported | Returns geometry dimension (0,1,2) | Type classification, dimensional analysis |
ST_CoordDim | — | ❌ Not Supported | Returns coordinate dimension (2,3,4) | 2D/3D/4D detection, data structure analysis |
ST_Boundary | — | ❌ Not Supported | Returns geometry boundary | Edge extraction, outline generation |
ST_Envelope | — | ❌ Not Supported | Returns bounding box | Extent calculation, viewport, indexing |
ST_BoundingDiagonal | — | ❌ Not Supported | Returns bounding box diagonal | Extent diagonal, size estimation |
ST_X / ST_Y | point.1, point.2 | ✅ Supported | Extracts X/Y coordinate from point | Coordinate extraction, data transformation |
ST_Z / ST_M | — | ❌ Not Supported | Extracts Z/M coordinate | Elevation extraction, measure value access |
2.5 Format Conversion
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_AsText (WKT) | — | ❌ Not Supported | Converts geometry to WKT string | Data export, GIS interoperability, debugging |
ST_GeomFromText | readWKTPolygon() | ⚠️ Polygon Only | Creates geometry from WKT | Data import, boundary loading, GIS migration |
ST_AsGeoJSON | — | ❌ Not Supported | Converts to GeoJSON format | Web maps, API responses, Leaflet/Mapbox integration |
ST_GeomFromGeoJSON | — | ❌ Not Supported | Creates geometry from GeoJSON | Web API import, map editor outputs, user polygons |
ST_AsBinary (WKB) | — | ❌ Not Supported | Converts to Well-Known Binary | Binary storage, efficient transfer, database exchange |
ST_GeomFromWKB | — | ❌ Not Supported | Creates geometry from WKB | Binary import, database migration |
ST_AsKML | — | ❌ Not Supported | Converts to KML format | Google Earth export, KML applications |
ST_AsGML | — | ❌ Not Supported | Converts to GML format | OGC web services, enterprise GIS |
ST_AsSVG | — | ❌ Not Supported | Converts to SVG path | Web graphics, vector visualization |
ST_SetSRID | (WGS84 only) | ⚠️ WGS84 only | Sets coordinate reference system | Projection assignment, CRS management |
ST_Transform | (WGS84 only) | ⚠️ WGS84 only | Transforms between coordinate systems | Projection conversion, local CRS, survey data |
2.6 Geometry Processing
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_Centroid | — | ❌ Not Supported | Calculates geometric center | Label placement, representative point, center icon |
ST_Buffer | — | ❌ Not Supported | Creates buffer around geometry | Safety zones, influence areas, setbacks, proximity buffers |
ST_Union | — | ❌ Not Supported | Combines geometries into one | Merging parcels, combining areas, dissolving boundaries |
ST_Intersection | — | ❌ Not Supported | Returns shared area of geometries | Overlap extraction, common area, clipping |
ST_Difference | — | ❌ Not Supported | Returns part of A not in B | Subtraction, exclusion zones, carve-out areas |
ST_SymDifference | — | ❌ Not Supported | Returns non-overlapping parts | XOR operation, exclusive areas, difference zones |
ST_Simplify | — | ❌ Not Supported | Reduces geometry complexity | Map generalization, data reduction, zoom optimization |
ST_SimplifyPreserveTopology | — | ❌ Not Supported | Simplifies while preserving topology | Safe simplification, valid output guaranteed |
ST_ConvexHull | — | ❌ Not Supported | Returns convex hull of geometry | Bounding shape, point cloud envelope, extent polygon |
ST_ConcaveHull | — | ❌ Not Supported | Returns concave hull of geometry | Tighter boundary, natural shape fitting |
ST_OffsetCurve | — | ❌ Not Supported | Creates parallel offset line | Road lanes, setback lines, buffer outlines |
ST_VoronoiPolygons | — | ❌ Not Supported | Creates Voronoi diagram | Nearest neighbor regions, coverage areas, tessellation |
ST_DelaunayTriangles | — | ❌ Not Supported | Creates Delaunay triangulation | TIN creation, mesh generation, terrain modeling |
ST_Subdivide | — | ❌ Not Supported | Splits geometry into smaller parts | Tile creation, parallel processing, chunking |
ST_Split | — | ❌ Not Supported | Splits geometry by another | Parcel division, line splitting, area partitioning |
ST_Snap | — | ❌ Not Supported | Snaps geometry to another | Topology correction, alignment, gap closing |
ST_Reverse | — | ❌ Not Supported | Reverses point order | Direction correction, winding order fix |
ST_ForcePolygonCW | — | ❌ Not Supported | Forces clockwise winding | Polygon orientation, standard compliance |
ST_MakeValid | — | ❌ Not Supported | Repairs invalid geometry | Data cleaning, topology fixing, error correction |
2.7 Clustering
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_ClusterDBSCAN | — | ❌ Not Supported | Density-based clustering (DBSCAN) | Point clustering, hotspot detection, pattern discovery |
ST_ClusterKMeans | — | ❌ Not Supported | K-means clustering | Territory division, zone creation, grouping |
ST_ClusterWithin | — | ❌ Not Supported | Distance-based clustering | Proximity grouping, neighborhood clustering |
2.8 Linear Referencing
PostGIS Function | ClickHouse Equivalent | Status | Description | Use Cases |
ST_LineInterpolatePoint | — | ❌ Not Supported | Returns point at fraction along line | Mile markers, progress points, route positions |
ST_LineLocatePoint | — | ❌ Not Supported | Returns fraction of closest point | Position on route, progress calculation, snapping |
ST_LineSubstring | — | ❌ Not Supported | Returns portion of line | Route segments, partial paths, subsections |
ST_AddMeasure | — | ❌ Not Supported | Adds measure values along line | Linear referencing setup, distance markers |
ST_LocateAlong | — | ❌ Not Supported | Returns points at measure value | Position lookup, measure-based queries |
2.9 H3 Indexing (ClickHouse 전용 — PostGIS에는 별도 H3-pg 익스텐션 설치 필요)
ClickHouse Function | Description | Use Cases |
geoToH3(lon, lat, resolution) | Converts coordinates to H3 index | Spatial aggregation, heatmaps, surge pricing, analytics |
h3ToGeo(h3Index) | Converts H3 to center coordinates | Cell visualization, reverse geocoding, centroids |
h3kRing(h3Index, k) | Returns cells within k rings | Nearby search, proximity analysis, catchment areas |
h3ToGeoBoundary(h3Index) | Returns H3 cell boundary | Cell visualization, hexagon rendering, boundaries |
h3GetResolution(h3Index) | Returns H3 resolution level | Data validation, multi-resolution analysis |
h3ToParent(h3Index, res) | Returns parent cell at resolution | Hierarchical aggregation, zoom levels |
h3ToChildren(h3Index, res) | Returns child cells at resolution | Drill-down, subdivision, detail levels |
h3IndexesAreNeighbors() | Tests if cells are neighbors | Adjacency check, connectivity analysis |
h3Line(start, end) | Returns cells along line | Path coverage, route cells, linear features |
h3Distance(h3Index1, h3Index2) | Returns grid distance between cells | Cell distance, grid-based routing |
h3HexAreaM2(resolution) | Returns cell area at resolution | Area calculations, density normalization |
2.10 S2 Indexing (ClickHouse 전용)
ClickHouse Function | Description | Use Cases |
geoToS2(lon, lat) | Converts coordinates to S2 index | Global indexing, Google Maps integration, range queries |
s2ToGeo(s2Index) | Converts S2 to coordinates | S2 cell lookup, coordinate retrieval |
s2RectContains(rect, s2Index) | Tests if cell in rectangle | Bounding box queries, viewport filtering |
s2GetNeighbors(s2Index) | Returns neighboring S2 cells | Adjacency queries, neighbor lookup |
s2CellsIntersect() | Tests if S2 cells intersect | Overlap detection, intersection queries |
s2CapContains() | Tests if point in spherical cap | Radius queries on sphere, circular regions |
2.11 ClickHouse Spherical Geo (구면 기반 폴리곤)
ClickHouse Function | Description | Use Cases |
polygonAreaSpherical(polygon) | Calculates spherical polygon area | Geographic area on sphere, accurate area |
polygonPerimeterSpherical(polygon) | Calculates spherical perimeter | Geographic perimeter, accurate boundary length |
polygonsIntersectionSpherical() | Spherical polygon intersection | Geographic intersection on sphere |
polygonsUnionSpherical() | Spherical polygon union | Geographic union on sphere |
3. 매트릭스 재해석 — "6%의 함정"
표를 단순 카운트로 보면 PostGIS replace는 가망 없어 보인다.
카테고리 | 함수 수 | ✅ Direct | ⚠️ 조건부 | ❌ 미지원 |
Distance & Measurement | 13 | 2 | 1 | 10 |
Spatial Relationships | 12 | 1 | 3 | 8 |
Geometry Constructors | 6 | 1 | 0 | 5 |
Geometry Accessors | 14 | 1 | 0 | 13 |
Format Conversion | 11 | 0 | 3 | 8 |
Geometry Processing | 19 | 0 | 0 | 19 |
Clustering | 3 | 0 | 0 | 3 |
Linear Referencing | 5 | 0 | 0 | 5 |
합계 | 83 | 5 | 7 | 71 |
+H3 / S2 / Spherical (CH 전용) | 21 | — | — | — |
직접 replace 가능: 6%. 미지원: 86%. 이 수치만 보면 결론은 "PostGIS 못 대체한다"다.
그런데 이건 함정이다. 이유는 두 가지.
3.1 함수 분포 ≠ 호출 분포
프로덕션 분석 워크로드의 SQL을 모아 보면 호출 빈도가 극도로 편향된다:
ClickHouse가 직접 replace하는 5개 함수가 분석 워크로드 호출 빈도의 80%를 차지한다. 미지원 71개 대부분은 GIS 앱(지적도, 도시계획, surveying, cartographic generalization)에서 무거운 비중이지, 분석 대시보드에서는 가끔 손가락에 꼽힐 정도로 등장.
3.2 H3 패턴은 ❌ 함수 일부를 재설계해서 replace한다
미지원 71개 중에서도 H3/S2 패턴으로 의미적으로 동등하게 풀 수 있는 함수가 상당수다.
PostGIS 패턴 | ClickHouse 재설계 | 노트 |
ST_Buffer(point, R) + ST_Contains | WHERE pickup_h3 IN h3kRing(center, k) | k = R / edgeLen, 정확도 trade-off |
ST_ClusterDBSCAN(points, ε, minPts) | hex 카운트 + WHERE cnt > minPts | 격자 기반 DBSCAN — 빠르고 노이즈 강건 |
ST_Centroid(polygon) | h3ToGeo(h3PolygonToCells(poly, res)) 평균 | hex 중심 평균 — 근사값 |
ST_Area(polygon) | length(h3PolygonToCells) * h3HexAreaM2(res) | hex 합산 — res 높을수록 정확 |
ST_VoronoiPolygons(points) | hex 별로 nearest point 할당 | 격자 Voronoi — 실용적 근사 |
ST_Intersects(line, polygon) | h3Line(a,b) ∩ h3PolygonToCells(poly) | hex 셋 교집합 |
Continuous aggregates (Timescale) | AggregatingMergeTree + MV | 패턴 자체가 더 강력 |
핵심 패턴 전환: vector geometry 기반에서 hex grid 기반으로 사고를 바꾸면, 미지원 함수의 큰 부분이 재설계로 replace 가능해진다. 게다가 hex 격자는 equal-area (H3) — 위도에 따라 면적이 변하지 않아서 PostGIS의 long-lat 평면 계산보다 정확도가 더 높은 경우도 많다.
3.3 결론 — Replace 가능성의 그라디언트
Tier 1 + Tier 2 — 즉 모빌리티·IoT·광고테크·풋트래픽 같은 분석 워크로드의 대다수는 ClickHouse로 완전히 옮길 수 있다. Tier 3는 남기는 게 정답이지만, 그 규모는 보통 원본 PostGIS의 작은 부분집합이다.
4. 마이그레이션 경로 — pg_clickhouse가 replace를 가능하게 한다
PostGIS replace의 진짜 어려움은 기능 매핑이 아니라 기존 SQL 자산을 한꺼번에 못 바꾼다는 것. 대시보드, ORM, 크론, 분석 노트북 등 PostGIS SQL이 박힌 곳이 수백~수천 곳에 흩어져 있고, 한 번에 다 재작성할 수 없다.
여기서 pg_clickhouse 가 마이그레이션 도구로서 결정적인 역할을 한다.
Phase 3가 핵심이다. pg_clickhouse가 ClickHouse 테이블을 PG의 foreign table처럼 노출하면서 PostGIS SQL의 일부 함수(ST_Distance, ST_DWithin, point에 대한 ST_Contains, ST_X/ST_Y 등)는 번역돼서 ClickHouse에서 실행된다.
즉:
- 데이터: ClickHouse에 있다 (스케일 문제 해결)
- SQL: PostgreSQL 표준 그대로 (대시보드/ORM 코드 변경 0)
- 실행: ClickHouse의 컬럼 엔진 + geo 함수가 처리 (분석 성능)
그 다음 Phase 4 — 우선순위 높은 무거운 쿼리부터 H3 MATERIALIZED 컬럼 + AggregatingMergeTree MV로 재설계하면서 진정한 ClickHouse 네이티브 패턴으로 옮긴다. 한 번에 다 바꿀 필요 없이 점진적으로.
이게 replace의 의미다 — fallback이 아니라 마이그레이션의 안전선. pg_clickhouse가 없으면 PostGIS replace는 "어느 날 모든 SQL을 갈아엎는" 위험한 빅뱅이 된다. pg_clickhouse가 있으면 "데이터부터 옮기고, 쿼리는 살아 있는 동안 점진적으로 네이티브화"하는 안전한 전환이 된다.
5. PostGIS를 ClickHouse로 옮긴 use case — Before / After
이 섹션은 실제 PostGIS에서 ClickHouse로 옮긴 후의 모습을 5가지 워크로드로 보여준다. 각각 PostGIS에서 어떻게 했는지와 ClickHouse로 옮긴 후 어떻게 되는지를 나란히.
Use Case 1 — 모빌리티 수요 히트맵 (5M+ 트립 GROUP BY)
Before (PostGIS):
-- 그리드 셀별 픽업 카운트
SELECT ST_GeoHash(pickup_geom, 7) AS cell,
count(*)
FROM trips
WHERE pickup_ts >= now() - interval '7 days'
GROUP BY cell
ORDER BY count(*) DESC LIMIT 100;문제: ST_GeoHash가 함수 호출이라 인덱스 활용 못 함 → 5M 행 풀스캔 + 셀 인코딩 5M회 → 수십 초~분.
After (ClickHouse, native):
CREATE TABLE trips (
-- ...
pickup_h3_r8 UInt64 MATERIALIZED geoToH3(pickup_lon, pickup_lat, 8)
) ENGINE = MergeTree
ORDER BY (pickup_h3_r8, pickup_ts, trip_id);
SELECT h3ToString(pickup_h3_r8) AS cell, count()
FROM trips
WHERE pickup_ts >= now() - INTERVAL 7 DAY
GROUP BY pickup_h3_r8
ORDER BY count() DESC LIMIT 100;핵심 변화:
MATERIALIZED컬럼 — INSERT 시점에 hex 변환, 매 쿼리마다 재계산 안 함ORDER BY pickup_h3_r8 첫째— 같은 hex의 트립이 디스크 인접 → sequential scan- 컬럼 압축 — UInt64 hex 값이 LZ4로 매우 잘 압축
500만 행 → top 100 hex가 수십 ms. PostGIS 대비 10–100배 latency 개선이 일반적.
Migration via pg_clickhouse (Phase 3):
-- 기존 SQL이 그대로 살아 있다 — foreign table에 점만 박혀 있고, ST_GeoHash 대신 hex 컬럼을 쓰는 정도만 변경
SELECT cell, count(*) FROM imported_trips
WHERE pickup_ts >= now() - interval '7 days'
GROUP BY cell ORDER BY count DESC LIMIT 100;ClickHouse가 hex 집계 + count를 푸시다운으로 처리한다.
Use Case 2 — Geofencing at scale (수억 점 × N 폴리곤)
Before (PostGIS):
-- 100개 서비스 존 × 1억 GPS 핑 = 100억 ST_Contains
SELECT z.zone_id, count(*) AS pings
FROM gps_pings p
JOIN service_zones z ON ST_Contains(z.geom, p.geom)
WHERE p.ping_ts >= now() - interval '1 hour'
GROUP BY z.zone_id;문제: GIST 인덱스가 bbox prefilter는 해주지만 정밀 ST_Contains는 매 행 winding 검사 → 핑 1억 × 평균 폴리곤 검사 수십회 → 수 시간.
After (ClickHouse, hybrid hex prefilter + pointInPolygon):
핵심 변화:
- hex 집합 prefilter로 99%+ 행을 인덱스 단계에서 탈락
- 경계 hex의 점만
pointInPolygon정밀 검사 - ClickHouse가 hex IN 필터를 ORDER BY 활용해 sequential scan으로 처리
수 시간 → 수십 초 수준의 개선이 흔하다.
좌표 순서 함정:geoToH3(lon, lat, res)와pointInPolygon((lon, lat), poly)는 경도 먼저,h3PolygonToCells([(lat, lon)…], res)는 위도 먼저. 같은 ClickHouse 안에서 정반대 컨벤션이라 반드시 함수 시그니처 확인.h3PolygonToCells는 unnamed tuple만 받으니 컬럼명이 있으면arrayMap(p -> (p.1, p.2), polygon)로 strip.
Use Case 3 — Nearest neighbor 근접 검색
Before (PostGIS):
-- 사용자 위치 주변 1km 내 가까운 매장 10개
SELECT store_id,
ST_Distance(geom, ST_MakePoint(:lon, :lat)::geography) AS dist
FROM stores
WHERE ST_DWithin(geom, ST_MakePoint(:lon, :lat)::geography, 1000)
ORDER BY geom <-> ST_MakePoint(:lon, :lat)
LIMIT 10;PG에서 KNN GIST는 잘 동작하지만, 매장 수가 수백만 + 사용자 동시 검색이 많으면 GIST 인덱스 메모리·동시성 압박.
After (ClickHouse):
WITH :lon AS lon, :lat AS lat
SELECT store_id,
geoDistance(lon, lat, store_lon, store_lat) AS dist
FROM stores
WHERE store_h3_r8 IN h3kRing(geoToH3(lon, lat, 8), 3) -- 1차 후보 hex prefilter
ORDER BY dist ASC LIMIT 10;핵심 변화:
h3kRing(center, k=3)= 37개 hex로 후보 제한 → 수백만 매장 중 인근 수십~수백 개만 거리 계산geoDistance로 정확한 미터 거리 산출 (WGS84 ellipsoid)
수만 동시 검색에서도 ClickHouse는 수십 ms 일관성. read replica 늘릴 필요 없음.
Use Case 4 — 실시간 surge pricing (continuous aggregates 대체)
Before (PostGIS / Timescale):
-- Timescale continuous aggregate
CREATE MATERIALIZED VIEW demand_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', pickup_ts) AS bucket,
ST_GeoHash(pickup_geom, 7) AS cell,
count(*) AS pickups, sum(fare) AS revenue
FROM trips
GROUP BY bucket, cell;문제: PostGIS와 결합한 continuous aggregate는 refresh 주기·invalidation·메모리 관리 모두 운영자가 챙겨야 함. cell 카디널리티가 높으면 refresh가 무거워진다.
After (ClickHouse, AggregatingMergeTree + MV):
핵심 변화:
- INSERT 시점에 자동 집계 갱신 (refresh 정책 없음)
State()/Merge()합산 보장 — 부분 집계도 정확- 대시보드 쿼리는 ~72k 행만 스캔 (원본 5M 대비)
운영 자동화 측면에서도 훨씬 단순하다. Timescale의 continuous aggregate refresh 정책을 운영해본 사람이면 이 부분 차이를 즉시 체감한다.
Use Case 5 — IoT/광고테크 지리 집계 (위치 임프레션)
Before (PostGIS):
-- 분당 위치 임프레션 × hex 집계
SELECT ST_GeoHash(geom, 8) AS cell,
date_trunc('minute', impression_ts) AS minute,
count(*) AS impressions,
count(DISTINCT device_id) AS uniq_devices
FROM impressions
WHERE impression_ts >= now() - interval '1 hour'
GROUP BY cell, minute;count(DISTINCT) + ST_GeoHash + 1억 행 → 분 단위 결과가 분 단위로 안 나옴.
After (ClickHouse):
SELECT h3ToString(geoToH3(lon, lat, 9)) AS cell,
toStartOfMinute(impression_ts) AS minute,
count() AS impressions,
uniqExact(device_id) AS uniq_devices, -- 정확
-- 또는 uniqHLL12(device_id) — 더 빠른 근사
sumIf(1, conversion = 1) AS conversions
FROM impressions
WHERE impression_ts >= now() - INTERVAL 1 HOUR
GROUP BY cell, minute;핵심 변화:
uniqExact/uniqHLL12같은 ClickHouse 네이티브 unique 추정기 — count(DISTINCT) 대비 수십 배 빠름- H3 인덱스가 GeoHash보다 equal-area이므로 위도 편향 없음
- LowCardinality device_id 압축 효과까지
리테일 풋트래픽도 같은 패턴 — 매장 주변 hex k-ring 내 디바이스 카운트로 매장 방문 의도 측정.
6. Replace가 어려운 워크로드 — 어떻게 다룰 것인가
Tier 3 영역(geometry processing, 정밀 vector 작업)이 워크로드에 조금 있다면 그것 때문에 전체 마이그레이션을 포기할 필요는 없다. 세 가지 대응 옵션:
옵션 | 적용 시점 |
a. H3 패턴으로 우회 | ST_Buffer → h3kRing, ST_ClusterDBSCAN → hex 밀도, ST_Centroid → hex 중심 평균. 근사가 허용되는 분석 워크로드에 가장 흔함 |
b. 작은 PG instance 유지 + pg_clickhouse 양방향 | 정밀 geometry 처리는 PG에 남기고, 데이터는 ClickHouse가 들고, PG는 pg_clickhouse로 ClickHouse를 외부 테이블로 본다. 결과를 다시 ClickHouse로 적재할 수도 있음 |
c. 외부 도구 (오프라인) | 지적도/도시계획 같은 풀스택 GIS는 ETL 단계에서 GeoPandas·Shapely 등으로 사전 가공 후 ClickHouse에 적재 |
가장 극단적인 케이스 — 정밀 vector 가공이 워크로드의 50% 이상 — 이라면 PostGIS replace 자체를 재고해야 한다. 하지만 분석 OLAP 맥락에서 그런 워크로드는 흔치 않다.
7. 운영 함정 — Replace 과정에서 알아둘 5가지
함정 | 영향 | 해결 |
좌표 순서 비일관 | geoToH3(lon, lat) vs h3PointDistM(lat, lon) — 함수마다 다르다 | 매 함수 호출 시 시그니처 재확인. 컬럼명을 pickup_lon/pickup_lat처럼 명시적으로 |
Named tuple 거부 | h3PolygonToCells는 unnamed Tuple(Float64, Float64)만 받음 | arrayMap(p -> (p.1, p.2), polygon)으로 column 이름 strip |
H3 계층 근사성 | 같은 raw 좌표의 r10 hex의 parent가 같은 좌표의 r8 hex와 다를 수 있음 | 가장 세밀한 해상도만 저장, 거친 해상도는 h3ToParent로 유도 |
WGS84 고정 | 비-WGS84 CRS는 미지원 ( ST_Transform 없음) | 적재 단계에서 WGS84로 통일. 비-WGS 필요 시 ETL에서 처리 |
MATERIALIZED 컬럼 backfill | 기존 데이터에 새 H3 컬럼 추가하면 빈 컬럼이 남음 | ALTER TABLE ... MATERIALIZE COLUMN으로 백필 (대용량은 시간 소요) |
8. 마무리 — Replace는 가능하다, 단 그라디언트로
세 줄 요약:
- PostGIS 함수의 6%만이 ClickHouse에 직접 대응한다. 하지만 분석 워크로드 호출의 80%+가 그 6%에서 일어난다. 매트릭스의 비대칭이 replace 가능성의 핵심.
- H3 패턴으로 재설계 replace 가능한 영역(radius geofencing, DBSCAN 클러스터링, 버퍼/Voronoi 근사, 실시간 surge)이 또 한 층 있다. 사고를 벡터 geometry에서 hex grid로 옮기면 가능한 영역이 크게 늘어난다.
- pg_clickhouse가 마이그레이션의 안전선이다. 기존 PostGIS SQL을 그대로 유지하면서 데이터를 ClickHouse로 옮기고, 우선순위 높은 쿼리부터 네이티브 ClickHouse로 점진 재작성한다. 빅뱅 없는 replace가 가능한 이유.
PostGIS replace 결정을 그라디언트로 보면, 답이 명확해진다. 모빌리티·IoT·광고테크·풋트래픽 분석은 대부분 전면 replace 가능하다. 지적도·도시계획·GIS 앱은 PostGIS를 남긴다. 그 사이의 회색지대는 pg_clickhouse 하이브리드가 답이다. ClickHouse Geo는 PostGIS 대체 불가도 아니고 단순 보완재도 아니다 — 분석 워크로드에 한해, 점진적·안전하게 PostGIS를 대체할 수 있는 실용적 경로다.
참고 자료
- ClickHouse Geo 함수 레퍼런스
- ClickHouse H3 함수 레퍼런스
- H3 공식 문서 — Uber
- S2 Geometry library
- PostGIS reference
- pg_clickhouse — 마이그레이션 경로로서의 익스텐션 (이전 글)
- Introducing pg_clickhouse — David Wheeler
- ClickPipes — Postgres CDC 복제
- 본 글의 fit/misfit 매트릭스는 PostGIS↔ClickHouse 비교 시트(2026-06) 기준, 실측은 ClickHouse 26.5.1.882 / NYC 5M trip 합성 데이터셋에서 검증