2 giờ sáng và bài toán “Tìm tài xế gần nhất”
Điện thoại rung bần bật. Hệ thống của khách hàng bên mảng giao đồ ăn báo lỗi timeout liên tục ở API tìm kiếm tài xế. Kiểm tra log, mình thấy một query SQL đang gồng mình tính khoảng cách bằng công thức Haversine trên hơn 2 triệu dòng dữ liệu. CPU server chạm ngưỡng 100% chỉ với vài chục request đồng thời.
Nhiều bạn khi mới làm app bản đồ thường coi tọa độ (Lat, Long) như hai cột Float rồi dùng toán cấp 3 để tính toán. Cách này chạy ổn với vài trăm bản ghi. Tuy nhiên, khi dữ liệu lên tới hàng trăm nghìn, đó là một thảm họa về hiệu năng. Giải pháp duy nhất lúc đó là migrate gấp sang PostGIS.
PostGIS không đơn thuần là một extension. Nó biến PostgreSQL thành một database không gian thực thụ. Nó hiểu rõ bản chất của điểm (Point), đường (Line) hay đa giác (Polygon) trên mặt cầu. Thay vì tính toán thủ công, bạn để database xử lý với những thuật toán đã được tối ưu hóa ở tầng thấp.
Geometry vs Geography: Đừng để chọn sai ngay từ đầu
Trước khi bắt tay vào code, bạn cần phân biệt rõ hai kiểu dữ liệu này để tránh sai số khi tính toán diện tích hoặc khoảng cách.
- Geometry: Dùng cho mặt phẳng (Cartesian). Nó coi trái đất phẳng như một tờ giấy. Kiểu này tính toán cực nhanh, phù hợp cho sơ đồ tòa nhà hoặc khu vực nhỏ.
- Geography: Dùng cho mặt cầu. Nó tính đến độ cong của trái đất. Đây là lựa chọn bắt buộc cho các app như Grab hay ShopeeFood để tính khoảng cách GPS chính xác.
Đừng quên SRID (Spatial Reference System Identifier). Với dữ liệu GPS toàn cầu, hãy luôn dùng SRID 4326 (chuẩn WGS 84). Sai mã SRID, mọi phép tính của bạn sẽ vô nghĩa.
Thực hành: Cài đặt và cấu hình PostGIS
1. Cài đặt Extension
Nếu dùng Ubuntu, bạn cài đặt trực tiếp qua apt. Quá trình này chỉ mất khoảng 30 giây:
sudo apt-get update
sudo apt-get install postgis postgresql-15-postgis-3
Sau khi cài gói ở tầng OS, bạn cần kích hoạt nó trong database. Lưu ý: Extension này cần được bật riêng cho từng database bạn sử dụng.
-- Kết nối vào database
CREATE EXTENSION postgis;
-- Kiểm tra phiên bản
SELECT postgis_full_version();
2. Thiết lập bảng lưu trữ
Thay vì tách rời lat và lng, hãy gom chúng vào một cột GEOGRAPHY duy nhất. Cách này giúp quản lý dữ liệu gọn gàng và tận dụng được các hàm hỗ trợ sẵn.
CREATE TABLE cafes (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
location GEOGRAPHY(POINT, 4326)
);
3. Thêm dữ liệu tọa độ
Khi chèn dữ liệu, thứ tự cực kỳ quan trọng: Longitude (Kinh độ) trước, Latitude (Vĩ độ) sau. Nếu nhầm thứ tự, quán cafe của bạn có thể “bay” từ Hà Nội sang tận Nam Cực.
INSERT INTO cafes (name, location)
VALUES ('Cộng Cà Phê', ST_SetSRID(ST_MakePoint(105.8523, 21.0285), 4326));
Để xử lý nhanh các file CSV danh sách địa điểm sang định dạng JSON trước khi import, mình thường dùng converter tại toolcraft.app. Công cụ này chạy client-side nên dữ liệu khách hàng luôn được bảo mật tuyệt đối.
Truy vấn bán kính (Radius Search) trong nháy mắt
Đây là lúc PostGIS tỏa sáng. Giả sử cần tìm các quán cafe trong bán kính 2km quanh vị trí người dùng:
SELECT name,
ST_Distance(location, ST_SetSRID(ST_MakePoint(105.8500, 21.0300), 4326)::geography) as distance
FROM cafes
WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(105.8500, 21.0300), 4326)::geography, 2000)
ORDER BY distance;
Hàm ST_DWithin không chỉ để lọc dữ liệu. Nó được thiết kế để hoạt động cực mượt với Index, giúp bỏ qua những vùng không gian không liên quan ngay lập tức.
Tối ưu hiệu năng với GiST Index
Nếu query trên 1 triệu dòng mà không có Index, server sẽ lại “khóc thét”. Với dữ liệu địa lý, B-Tree thông thường sẽ vô dụng. Bạn phải dùng GiST (Generalized Search Tree).
CREATE INDEX idx_cafes_location ON cafes USING GIST(location);
Index này chia không gian thành các ô vuông nhỏ (R-Tree). Trong thực tế, mình từng giúp một hệ thống giảm thời gian query từ 8 giây xuống còn 45ms chỉ nhờ dòng lệnh này.
Kinh nghiệm xương máu từ thực tế
Sau nhiều năm vận hành các hệ thống bản đồ lớn, mình rút ra 3 lưu ý quan trọng:
- Cân nhắc hiệu năng: Geography chính xác hơn nhưng tốn tài nguyên hơn Geometry. Nếu app chỉ chạy trong một quận hoặc một thành phố nhỏ, Geometry là lựa chọn tối ưu hơn.
- Đơn vị đo lường:
ST_DWithintrên Geography dùng đơn vị mét. Trong khi đó, trên Geometry nó lại dùng đơn vị của SRID (thường là độ – degree). Rất nhiều dev đã dính bug “tìm mãi không thấy” vì nhầm lẫn này. - Dọn dẹp database: Các bảng chứa tọa độ shipper cập nhật liên tục sẽ sinh ra rất nhiều dead tuples. Hãy đảm bảo
Autovacuumđược cấu hình đủ mạnh để không làm chậm hệ thống.
Lời kết
PostGIS là tiêu chuẩn vàng nếu bạn muốn làm app liên quan đến vị trí. Đừng cố gắng tự viết lại các công thức toán học trên code ứng dụng. Hãy đẩy gánh nặng đó xuống database. Việc làm quen với SRID hay GiST có thể hơi khó lúc đầu, nhưng sự ổn định và tốc độ mà nó mang lại là hoàn toàn xứng đáng.

