MySQL Spatial Data: Tuyệt chiêu xử lý tọa độ GPS và tìm địa điểm gần nhất

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Cơn ác mộng khi làm app bản đồ bằng cách thủ công

Hồi mới đi làm, mình từng nhận một task nghe khá đơn giản: Tìm các cửa hàng tiện lợi trong bán kính 5km cho một app giao hàng. Lúc đó, mình hồn nhiên tạo hai cột latitudelongitude kiểu DECIMAL. Sau đó, mình nhồi nguyên công thức Haversine với mớ hàm Sin, Cos hỗn độn vào câu lệnh SQL để tính khoảng cách.

Kết quả thật thảm khốc. Với tập dữ liệu vài trăm bản ghi, mọi thứ vẫn ổn. Nhưng khi database chạm mốc 1 triệu điểm và dung lượng vượt 50GB, hệ thống bắt đầu “thở oxy”. Mỗi khi người dùng bấm tìm quanh đây, CPU server vọt lên 90%, query mất tới 5-7 giây mới trả về kết quả. Đó là bài học đắt giá về việc dùng sai công cụ cho dữ liệu địa lý.

Hai cách lưu tọa độ phổ biến: Chọn hiệu năng hay chọn dễ?

Khi cần lưu vị trí trên bản đồ, bạn thường sẽ đứng trước hai lựa chọn:

Cách 1: Lưu cột Latitude và Longitude riêng biệt (Dạng số)

Đây là cách làm bản năng nhất bằng việc dùng DECIMAL(10, 8).

  • Ưu điểm: Dễ đọc, dễ insert dữ liệu mà không cần quan tâm đến định dạng phức tạp.
  • Nhược điểm: MySQL buộc phải quét toàn bộ bảng (Full Table Scan) để tính toán cho từng dòng. Với bảng 1 triệu dòng, nó sẽ thực hiện 1 triệu phép tính Sin/Cos mỗi khi bạn tìm kiếm. Hệ thống sẽ nghẽn cổ chai ngay lập tức.

Cách 2: Sử dụng Spatial Data Type (Kiểu dữ liệu không gian)

MySQL cung cấp các kiểu dữ liệu chuyên dụng như POINT, LINESTRING, POLYGON.

  • Ưu điểm: Hỗ trợ SPATIAL INDEX. Đây là bộ tăng tốc giúp tìm kiếm không gian cực nhanh. Bạn có sẵn hàm ST_Distance_Sphere để tính khoảng cách trên mặt cầu với sai số cực thấp.
  • Nhược điểm: Bạn cần hiểu về SRID (hệ tọa độ) và cú pháp SQL đặc thù hơn một chút.

Tại sao Spatial Data là lựa chọn sống còn?

Nếu bạn chỉ lưu tọa độ để hiển thị địa chỉ trên web, DECIMAL là đủ. Nhưng nếu bạn làm tính năng “Tìm shipper gần nhất” hay “Gợi ý quán ăn quanh đây”, Spatial Data là bắt buộc. Thực tế tại dự án mình từng làm, việc chuyển sang Spatial Index đã kéo latency từ 2000ms xuống còn chưa đầy 10ms. Một sự khác biệt khủng khiếp về mặt trải nghiệm người dùng.

Hướng dẫn triển khai chuẩn chỉnh trên MySQL 8.0

Dưới đây là cách thiết lập bảng lưu trữ địa điểm tối ưu nhất hiện nay.

1. Khởi tạo bảng với kiểu dữ liệu POINT

Mấu chốt nằm ở cột location. Chúng ta dùng POINT kèm định danh SRID 4326 (hệ tọa độ WGS 84 chuẩn GPS toàn cầu).

CREATE TABLE stores (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    location POINT NOT NULL SRID 4326,
    SPATIAL INDEX(location)
) ENGINE=InnoDB;

Lưu ý: Spatial Index chỉ hoạt động nếu cột được đánh dấu NOT NULL.

2. Chèn dữ liệu tọa độ

Sử dụng hàm ST_GeomFromText để đưa dữ liệu vào. Trong MySQL 8.0 với SRID 4326, thứ tự mặc định là POINT(Vĩ_độ Kinh_độ).

-- Thêm Landmark 81 (Lat: 10.7946, Long: 106.7218)
INSERT INTO stores (name, location)
VALUES ('Landmark 81 Store', ST_GeomFromText('POINT(10.7946 106.7218)', 4326));

-- Thêm Chợ Bến Thành (Lat: 10.7719, Long: 106.6983)
INSERT INTO stores (name, location)
VALUES ('Ben Thanh Market Store', ST_GeomFromText('POINT(10.7719 106.6983)', 4326));

3. Tìm địa điểm trong bán kính X km

Đây là câu lệnh mình dùng nhiều nhất để lọc các điểm trong vùng 2km. MySQL sẽ tự động dùng Index để loại bỏ các điểm ở quá xa trước khi tính toán chi tiết.

-- Vị trí người dùng tại Bitexco (10.7715, 106.7042)
SET @user_loc = ST_GeomFromText('POINT(10.7715 106.7042)', 4326);

SELECT name, 
       ST_Distance_Sphere(location, @user_loc) AS distance
FROM stores
WHERE ST_Distance_Sphere(location, @user_loc) <= 2000
ORDER BY distance ASC;

Kiểm tra vị trí có nằm trong vùng giao hàng (Polygon) không

Nếu bạn có một vùng giao hàng phức tạp, hãy dùng ST_Contains. Ví dụ, kiểm tra xem shipper có đang ở trong khu vực Quận 1 đã được định nghĩa trước hay không.

-- Định nghĩa vùng giao hàng bằng Polygon
SET @delivery_zone = ST_GeomFromText('POLYGON((10.7 106.6, 10.8 106.6, 10.8 106.8, 10.7 106.8, 10.7 106.6))', 4326);

SELECT name FROM stores 
WHERE ST_Contains(@delivery_zone, location);

Kinh nghiệm “xương máu” để tối ưu hiệu năng

Làm việc với dữ liệu không gian trên hệ thống lớn rất dễ dính bẫy. Dưới đây là 3 lưu ý giúp bạn sống sót:

  1. Đừng quên SRID: Nếu thiếu SRID 4326, MySQL có thể tính khoảng cách theo đơn vị “độ” thay vì mét. Kết quả trả về sẽ sai lệch hoàn toàn so với thực tế.
  2. Thứ tự Lat/Long là cái bẫy: Leaflet dùng (Lat, Long), nhưng GeoJSON chuẩn lại dùng (Long, Lat). Hãy thống nhất quy chuẩn này ngay từ đầu để tránh việc cửa hàng tại TP.HCM lại hiển thị ở tận… Châu Phi.
  3. Dùng Bounding Box cho tập dữ liệu cực lớn: Với bảng hàng chục triệu dòng, hãy dùng ST_MakeEnvelope để tạo một hình chữ nhật bao quanh vị trí. Cách này giúp lọc dữ liệu thô nhanh hơn nữa trước khi dùng ST_Distance_Sphere.

Nắm vững Spatial Data giúp bạn xử lý hàng nghìn request mỗi giây mà database vẫn chạy mượt mà. Hy vọng những chia sẻ này giúp bạn tự tin hơn khi xây dựng các tính năng liên quan đến bản đồ.

Share: