GIS 2 — PostGIS: Không gian trong PostgreSQL

13 thg 7, 2026 4 lượt xem
#postgresql
#sql
#spatial
#postgis
#gis

GIS 2 — PostGIS: Không gian trong PostgreSQL

Ở bài tổng quan GIS chúng ta đã thấy dữ liệu không gian (điểm, đường, vùng) khác dữ liệu bảng thông thường ở chỗ nào và vì sao ngân hàng cần nó. Câu hỏi tiếp theo rất thực tế: lưu và truy vấn dữ liệu không gian ở đâu? Với một team dữ liệu đã có PostgreSQL làm xương sống, câu trả lời gần như luôn là: PostGIS.

PostGIS không phải một cơ sở dữ liệu riêng. Nó là một extension — đúng cơ chế "cắm thêm" đã mô tả ở PostgreSQL Extension. Chỉ một câu CREATE EXTENSION postgis; là Postgres của bạn hiểu thêm hàng trăm kiểu dữ liệu, hàm và toán tử không gian, tuân theo chuẩn OGC (Open Geospatial Consortium) và SQL/MM Spatial. Nghĩa là bạn không phải học một hệ mới: vẫn là bảng, cột, SELECT, JOIN, chỉ số — chỉ thêm một loại cột kiểu hình học và một họ hàm tiền tố ST_ (Spatial Type).

Lưu ý sandbox: Sandbox của Knowledge Base là PostgreSQL read-only không cài PostGIS. Do đó mọi block SQL PostGIS trong bài này là minh hoạ, không đánh dấu "▶ Chạy được". Chỉ có một ví dụ gom nhóm non-spatial ở cuối là chạy được thật.

PostGIS đứng ở đâu trong Postgres

PostGIS cắm vào lõi Postgres đúng như mọi extension khác: thêm kiểu, thêm hàm, và quan trọng nhất là thêm một phương thức chỉ mục không gian (GIST) vào bộ máy truy vấn.

Ba thư viện nền làm nên sức mạnh PostGIS: GEOS (phép toán hình học: giao, chứa, buffer), PROJ (đổi hệ toạ độ giữa các CRS), và GDAL (đọc/ghi định dạng không gian, dùng cho nhập liệu). Chi tiết kiến trúc lõi Postgres (heap, WAL, MVCC) xem PostgreSQL Architecture.

Cài đặt và bật extension

-- (minh hoạ) Bật PostGIS trong database hiện tại
CREATE EXTENSION IF NOT EXISTS postgis;

-- (tuỳ chọn) các module bổ sung
CREATE EXTENSION IF NOT EXISTS postgis_topology;   -- topo hình học
CREATE EXTENSION IF NOT EXISTS postgis_raster;      -- dữ liệu raster

-- Kiểm tra phiên bản
SELECT PostGIS_Full_Version();

Trên managed service (RDS, Cloud SQL, Azure Postgres) PostGIS thường đã được cấp sẵn — chỉ cần CREATE EXTENSION là dùng được, không cần cài .so thủ công. Trên server tự quản, bạn phải cài gói OS (apt install postgresql-16-postgis-3) trước.

Hai kiểu dữ liệu: geometry vs geography

Đây là quyết định thiết kế quan trọng nhất và cũng dễ sai nhất khi mới dùng PostGIS.

Tiêu chígeometrygeography
Mô hìnhMặt phẳng Descartes (Cartesian)Mặt cầu / mặt ellipsoid Trái Đất
Đơn vị ST_DistanceTheo đơn vị của SRID (độ, hoặc mét)Luôn là mét
Tính lat/lon (4326)Sai — coi độ như toạ độ phẳngĐúng — tính theo cung tròn lớn (great-circle)
Tốc độNhanh (đại số phẳng đơn giản)Chậm hơn (lượng giác cầu)
Số hàm hỗ trợRất nhiềuÍt hơn (tập con phổ biến)

Quy tắc thực chiến:

  • Dữ liệu kinh độ/vĩ độ (lat/lon toàn cầu) và bạn cần khoảng cách thực bằng mét → dùng geography. Đây là trường hợp của ngân hàng: điểm ATM/chi nhánh và khách hàng đều là lat/lon, ta muốn "cách nhau bao nhiêu mét".
  • Dữ liệu đã được chiếu về hệ mét (ví dụ UTM zone 48N của Việt Nam, EPSG:32648) hoặc bạn cần nhiều phép hình học phức tạp, hiệu năng cao → dùng geometry.

geography bản chất là một geometry với ràng buộc SRID 4326 và một bộ hàm hiểu mặt cầu. Bạn có thể ép kiểu qua lại: geom::geography, geog::geometry.

SRID: hệ toạ độ và EPSG

SRID (Spatial Reference System Identifier) là mã cho biết một toạ độ được đo trong hệ quy chiếu nào. Không có SRID, cặp số (105.85, 21.02) là vô nghĩa — có thể là độ, có thể là mét.

  • EPSG:4326 (WGS 84): hệ lat/lon toàn cầu, đơn vị độ. Đây là hệ của GPS, Google Maps, GeoJSON. Là mặc định khi nhập dữ liệu địa lý.
  • EPSG:32648 (WGS 84 / UTM zone 48N): hệ chiếu phẳng phủ phần lớn Việt Nam, đơn vị mét. Dùng khi cần đo diện tích/độ dài chính xác bằng mét trên geometry.
  • EPSG:3857 (Web Mercator): hệ hiển thị bản đồ web; không dùng để đo khoảng cách (méo theo vĩ độ).

Điểm mấu chốt: khoảng cách và diện tích trên geometry chỉ đúng khi tọa độ ở hệ mét. Nếu dữ liệu là 4326 (độ) mà bạn ST_Area trực tiếp trên geometry, kết quả ra "độ vuông" vô nghĩa. Hai cách xử lý: (1) đổi sang geography, hoặc (2) ST_Transform sang UTM rồi mới đo.

-- (minh hoạ) Gán SRID cho một điểm và đổi hệ toạ độ
SELECT ST_SetSRID(ST_MakePoint(105.85, 21.02), 4326);   -- lat/lon
SELECT ST_Transform(
         ST_SetSRID(ST_MakePoint(105.85, 21.02), 4326),
         32648                                            -- đổi sang UTM 48N (mét)
       );

Tạo cột không gian và nhập điểm

Có hai cách phổ biến để dựng một điểm từ toạ độ:

-- (minh hoạ) Tạo bảng chi nhánh với cột geography (lat/lon, đo bằng mét)
CREATE TABLE branch (
    id     int PRIMARY KEY,
    ten    text,
    geom   geography(Point, 4326)
);

-- Cách 1: từ số học — ST_MakePoint(lon, lat)  (LƯU Ý: kinh độ TRƯỚC)
INSERT INTO branch (id, ten, geom) VALUES
  (1, 'CN Hoan Kiem', ST_SetSRID(ST_MakePoint(105.8544, 21.0285), 4326)),
  (2, 'CN Cau Giay',  ST_SetSRID(ST_MakePoint(105.7998, 21.0313), 4326));

-- Cách 2: từ WKT (Well-Known Text) — ST_GeomFromText
INSERT INTO branch (id, ten, geom) VALUES
  (3, 'CN Ba Dinh', ST_GeogFromText('SRID=4326;POINT(105.8342 21.0389)'));

Bẫy kinh điển: ST_MakePoint nhận (kinh độ, vĩ độ) = (X, Y) = (lon, lat), ngược với thói quen đọc "vĩ độ, kinh độ" của con người. WKT POINT(105.85 21.02) cũng là X Y (lon lat). Đảo hai giá trị là lỗi im lặng phổ biến nhất trong GIS — điểm sẽ nhảy ra biển hoặc sang bán cầu khác.

Các hàm cốt lõi cần thuộc

HàmTrả vềCông dụng
ST_Distance(a, b)doubleKhoảng cách. Trên geographymét
ST_DWithin(a, b, d)boola và b có nằm trong khoảng d (mét với geography) — dùng được chỉ mục
ST_Contains(a, b)boola có chứa hoàn toàn b không
ST_Within(a, b)boola có nằm trong b không (nghịch của Contains)
ST_Intersects(a, b)boola và b có giao nhau không
ST_Buffer(g, d)geometryVùng đệm bán kính d quanh g
ST_Area(g)doubleDiện tích (đơn vị theo SRID; geography → m²)
ST_Length(g)doubleĐộ dài đường
ST_Transform(g, srid)geometryĐổi CRS (ví dụ 4326 → 32648 để đo mét)
ST_X(pt) / ST_Y(pt)doubleLấy kinh độ / vĩ độ của điểm
ST_AsGeoJSON(g)textXuất GeoJSON (cho map front-end)
ST_AsText(g)textXuất WKT (đọc bằng mắt)

ST_DWithin là ngôi sao cho bài toán "bán kính": nó vừa chính xác (không giống ST_Buffer xấp xỉ đa giác) vừa dùng được chỉ mục GIST nên nhanh. Với ST_Distance bạn không thể lọc bằng chỉ mục — mọi hàng đều phải tính khoảng cách rồi so sánh.

Bài toán ngân hàng: tìm chi nhánh trong bán kính 2km

Giả sử branch(id, ten, geom geography) như trên. Khách hàng ở toạ độ (105.81, 21.03), cần tìm mọi chi nhánh trong bán kính 2000 mét và sắp theo khoảng cách:

-- (minh hoạ) Chi nhánh trong 2km quanh khách, kèm khoảng cách (mét)
WITH cust AS (
  SELECT ST_SetSRID(ST_MakePoint(105.8100, 21.0300), 4326)::geography AS pt
)
SELECT b.id,
       b.ten,
       ROUND(ST_Distance(b.geom, cust.pt)::numeric, 0) AS metres
FROM   branch b, cust
WHERE  ST_DWithin(b.geom, cust.pt, 2000)   -- lọc bằng chỉ mục, 2000 m
ORDER  BY metres;

Kết quả (minh hoạ): các chi nhánh cách khách dưới 2km, gần nhất lên đầu. Vì geomgeography, con số metres là mét thật đo trên mặt cầu — không cần transform gì thêm.

Muốn tìm chi nhánh gần nhất bất kể xa gần (nearest neighbor), dùng toán tử khoảng cách <-> với ORDER BY ... LIMIT — PostGIS tối ưu việc này bằng KNN trên chỉ mục GIST:

-- (minh hoạ) 1 chi nhánh gần khách nhất (KNN qua chỉ mục)
SELECT b.id, b.ten
FROM   branch b
ORDER  BY b.geom <-> ST_SetSRID(ST_MakePoint(105.81, 21.03),4326)::geography
LIMIT  1;

Chỉ mục không gian GIST — vì sao bắt buộc

Không có chỉ mục, một truy vấn ST_DWithin phải quét toàn bảng (sequential scan) và tính khoảng cách cho từng hàng. Với vài chục chi nhánh thì không sao; với hàng triệu điểm (tất cả giao dịch có toạ độ, tất cả khách hàng) thì đây là thảm hoạ hiệu năng.

Giải pháp là chỉ mục GIST (Generalized Search Tree). GIST lưu bounding box (hộp bao chữ nhật) của mỗi hình và tổ chức chúng theo cây R-tree. Khi truy vấn:

-- (minh hoạ) Chỉ mục không gian — BẮT BUỘC cho bảng lớn
CREATE INDEX idx_branch_geom ON branch USING GIST (geom);

-- Sau khi tạo, nên cập nhật thống kê
ANALYZE branch;

Cơ chế hai pha: (1) lọc thô bằng toán tử && (bounding-box overlap) trên chỉ mục GIST — loại nhanh gần hết các hàng ở xa; (2) lọc tinh bằng hàm chính xác (ST_Distance/ST_DWithin recheck) chỉ trên nhúm ứng viên còn lại. ST_DWithin được viết để bên trong tự sinh điều kiện &&, nên nó dùng chỉ mục — đây là lý do luôn ưu tiên ST_DWithin thay vì ST_Distance(...) < d trong mệnh đề WHERE.

Dùng EXPLAIN để xác nhận truy vấn có Index Scan using idx_branch_geom thay vì Seq Scan.

Nạp dữ liệu không gian từ ngoài vào

Toạ độ điểm gõ tay chỉ hợp cho demo. Dữ liệu thật (ranh giới tỉnh/phường, mạng đường) thường nằm trong file Shapefile hoặc GeoJSON. Hai công cụ chuẩn:

  • shp2pgsql — chuyển Shapefile → câu SQL nạp vào PostGIS:
    shp2pgsql -s 4326 -I ranh_gioi_phuong.shp public.ward | psql -d ncb_gis
    # -s 4326: gán SRID; -I: tự tạo luôn chỉ mục GIST
    
  • ogr2ogr (thuộc GDAL) — bộ chuyển đổi vạn năng, đọc/ghi hàng chục định dạng:
    ogr2ogr -f PostgreSQL PG:"dbname=ncb_gis" atm.geojson -nln atm -lco GEOMETRY_NAME=geom
    

Chi tiết các định dạng (Shapefile, GeoJSON, GeoPackage, WKT/WKB) và cách chọn, xem bài Định dạng dữ liệu không gian. Truy vấn không gian nâng cao (join theo vùng, spatial aggregation) xem Truy vấn không gian.

Ví dụ non-spatial chạy được (đối chiếu)

Để thấy rõ khác biệt, đây là một câu gom nhóm theo city trên bảng customers của sandbox — hoàn toàn non-spatial, không cần PostGIS:

-- ▶ Chạy được
SELECT city, COUNT(*) AS so_khach
FROM customers
GROUP BY city
ORDER BY so_khach DESC;

city chỉ là chuỗi tên thành phố — muốn hỏi "khách nào trong bán kính 2km quanh điểm này" thì bắt buộc phải có cột toạ độ và PostGIS như trên. Đó chính là ranh giới giữa dữ liệu địa lý dạng thuộc tính (text) và dữ liệu không gian thật.

Use case thực tế

Bài toán: NCB có ~200 chi nhánh/phòng giao dịch và ~1.500 ATM trên toàn quốc. Bộ phận CX muốn, mỗi khi khách mở app, hiển thị ATM gần nhất còn hoạt động trong bán kính 3km; đồng thời phòng Vận hành muốn báo cáo "phường nào có khách đông nhưng không có ATM nào trong 1km" để đề xuất lắp máy mới.

Thiết kế:

  1. Bảng atm(id, ten, status, geom geography(Point,4326))branch(...) tương tự. Nạp toạ độ từ file GeoJSON của phòng Hành chính bằng ogr2ogr.
  2. Tạo chỉ mục: CREATE INDEX idx_atm_geom ON atm USING GIST(geom); — bắt buộc vì truy vấn chạy real-time theo mỗi lượt mở app.
  3. Tìm ATM gần khách (real-time):
    -- (minh hoạ) ATM đang hoạt động, trong 3km, gần nhất trước
    SELECT id, ten, ROUND(ST_Distance(geom, :cust)::numeric,0) AS m
    FROM   atm
    WHERE  status = 'ACTIVE'
      AND  ST_DWithin(geom, :cust, 3000)
    ORDER  BY geom <-> :cust
    LIMIT  5;
    
    Nhờ GIST + ST_DWithin, truy vấn này trả về dưới 5ms dù có hàng nghìn ATM.
  4. Báo cáo vùng trắng ATM: lấy ranh giới phường (bảng ward, polygon), đếm khách trong mỗi phường (ST_Contains(ward.geom, cust.geom)), rồi loại các phường đã có ATM trong 1km bằng ST_DWithin — ra danh sách phường "đông khách, thiếu ATM" để trình đề xuất đầu tư.

Kết quả: báo cáo chỉ ra 12 phường nội thành đông khách mà ATM gần nhất > 1km; đề xuất lắp 8 máy, mỗi máy phủ thêm ~2.000 khách trong bán kính đi bộ. Toàn bộ chạy trên chính cụm Postgres hiện có, không cần thêm hệ GIS chuyên dụng.

Ghi nhớ

  • PostGIS là extension của Postgres: CREATE EXTENSION postgis; — không phải DB riêng, dùng chung SQL/JOIN/index quen thuộc (pg-06-extensions).
  • geography cho lat/lon và khoảng cách thực bằng mét (chậm hơn); geometry cho hệ phẳng/hệ mét, nhanh, nhiều hàm. Chọn sai kiểu = con số vô nghĩa.
  • Luôn khai SRID. EPSG:4326 = độ (lat/lon, GPS); muốn đo mét trên geometry phải ST_Transform sang hệ mét (vd EPSG:32648).
  • Dựng điểm: ST_MakePoint(lon, lat) hoặc ST_GeomFromText('POINT(lon lat)') — nhớ kinh độ đứng trước (X trước Y).
  • Bài toán bán kính: dùng ST_DWithin (chính xác + dùng chỉ mục), tránh ST_Distance(...) < d trong WHERE.
  • Chỉ mục GIST bắt buộc cho bảng lớn: CREATE INDEX ... USING GIST(geom); — lọc thô bằng bounding box rồi lọc tinh, bỏ qua 99% hàng ở xa.
  • Nạp dữ liệu ngoài bằng shp2pgsql hoặc ogr2ogr (xem gis-04).
  • Trong sandbox này không có PostGIS → mọi SQL không gian là minh hoạ; chỉ câu gom nhóm theo city là chạy được thật.

Bài viết liên quan

Các kỹ thuật phân tích không gian tạo giá trị kinh doanh cho ngân hàng: phân tích mật độ & hotspot, gom cụm ST_ClusterDBSCAN/KMeans, tự tương quan không gian (Moran's I), chọn địa điểm ATM/chi nhánh bằng service area, isochrone và overlay đa lớp. Nối tiếp bài truy vấn không gian, minh hoạ bằng SQL PostGIS và quy trình site selection.

13 thg 7, 2026 4

Nhập môn dữ liệu không gian (spatial/geospatial): dữ liệu gắn vị trí trên Trái Đất, các loại hình học điểm/đường/vùng, hệ toạ độ CRS/SRID (WGS84, UTM/VN2000), vector vs raster, quan hệ và phép đo không gian. Đặt nền cho cả series GIS và giá trị của nó với ngân hàng NCB: mạng lưới chi nhánh/ATM, phân tích khách hàng theo địa bàn, rủi ro và gian lận theo vị trí.

13 thg 7, 2026 4

Biến dữ liệu không gian thành bản đồ để ra quyết định. Bản đồ chuyên đề: choropleth (phải chuẩn hoá), symbol/bubble, heatmap, cluster, flow map — chọn theo dữ liệu. Công cụ: QGIS, Leaflet/Mapbox GL/deck.gl/Kepler.gl, bản đồ trong Power BI & Superset; tile OSM, level of detail. Luồng PostGIS → GeoJSON (ST_AsGeoJSON) → web map, nguyên tắc thiết kế và cạm bẫy. Bối cảnh NCB: dashboard dư nợ/huy động/khách theo tỉnh cho lãnh đạo.

13 thg 7, 2026 3

Các mẫu truy vấn không gian thực chiến với PostGIS, nối tiếp bài PostGIS: spatial join điểm-trong-vùng, đếm điểm theo vùng, nearest-neighbor bằng toán tử <-> và LATERAL, truy vấn bán kính ST_DWithin, tổng hợp không gian, phủ & khoảng trống. Kèm mẹo hiệu năng (GIST index, tránh ST_Distance), cạm bẫy đơn vị/SRID/geometry-vs-geography, và bối cảnh NCB gán giao dịch vào địa bàn.

13 thg 7, 2026 3