GIS 2 — PostGIS: Không gian trong PostgreSQL
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í | geometry | geography |
|---|---|---|
| Mô hình | Mặt phẳng Descartes (Cartesian) | Mặt cầu / mặt ellipsoid Trái Đất |
Đơn vị ST_Distance | Theo đơ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àm | Trả về | Công dụng |
|---|---|---|
ST_Distance(a, b) | double | Khoảng cách. Trên geography là mét |
ST_DWithin(a, b, d) | bool | a và b có nằm trong khoảng d (mét với geography) — dùng được chỉ mục |
ST_Contains(a, b) | bool | a có chứa hoàn toàn b không |
ST_Within(a, b) | bool | a có nằm trong b không (nghịch của Contains) |
ST_Intersects(a, b) | bool | a và b có giao nhau không |
ST_Buffer(g, d) | geometry | Vùng đệm bán kính d quanh g |
ST_Area(g) | double | Diệ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) | double | Lấy kinh độ / vĩ độ của điểm |
ST_AsGeoJSON(g) | text | Xuất GeoJSON (cho map front-end) |
ST_AsText(g) | text | Xuấ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ì geom là geography, 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 GISTogr2ogr(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ế:
- Bảng
atm(id, ten, status, geom geography(Point,4326))vàbranch(...)tương tự. Nạp toạ độ từ file GeoJSON của phòng Hành chính bằngogr2ogr. - 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. - Tìm ATM gần khách (real-time):
Nhờ GIST +
-- (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;ST_DWithin, truy vấn này trả về dưới 5ms dù có hàng nghìn ATM. - 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ằngST_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). geographycho lat/lon và khoảng cách thực bằng mét (chậm hơn);geometrycho 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
geometryphảiST_Transformsang hệ mét (vd EPSG:32648). - Dựng điểm:
ST_MakePoint(lon, lat)hoặcST_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ánhST_Distance(...) < dtrongWHERE. - 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
shp2pgsqlhoặcogr2ogr(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
citylà 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.
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í.
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.
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.