一、环境准备
1.1 达梦数据库版本要求
-
达梦数据库 DM8 及以上版本
-
需安装空间数据组件(可选)
1.2 创建测试数据库
sql
-- 创建表空间
CREATE TABLESPACE lbs_data DATAFILE 'lbs_data.dbf' SIZE 1024;
-- 创建用户
CREATE USER lbs_user IDENTIFIED BY "Lbs_123456"
DEFAULT TABLESPACE lbs_data;
GRANT RESOURCE, VTI TO lbs_user;
二、空间数据类型基础
2.1 达梦支持的空间数据类型
sql
-- 1. 点(POINT)
CREATE TABLE location_points (
id INT PRIMARY KEY,
name VARCHAR(100),
-- 经度,纬度格式
coordinate ST_POINT
);
-- 2. 线(LINESTRING)
CREATE TABLE road_segments (
id INT PRIMARY KEY,
road_name VARCHAR(100),
path ST_LINESTRING
);
-- 3. 多边形(POLYGON)
CREATE TABLE service_areas (
id INT PRIMARY KEY,
area_name VARCHAR(100),
boundary ST_POLYGON
);
-- 4. 几何集合(GEOMETRYCOLLECTION)
CREATE TABLE mixed_features (
id INT PRIMARY KEY,
features ST_GEOMETRY
);
三、LBS数据建模
3.1 创建地理位置表
sql
-- 创建店铺位置表
CREATE TABLE store_locations (
store_id INT PRIMARY KEY,
store_name VARCHAR(200),
address VARCHAR(500),
-- 存储经纬度,SRID 4326表示WGS84坐标系
location ST_POINT SRID 4326,
business_hours VARCHAR(100),
phone VARCHAR(20),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
);
-- 创建空间索引
CREATE SPATIAL INDEX idx_store_location
ON store_locations(location)
STORAGE(ON MAIN);
-- 创建用户位置记录表
CREATE TABLE user_locations (
record_id BIGINT IDENTITY(1,1) PRIMARY KEY,
user_id INT,
device_id VARCHAR(50),
location ST_POINT SRID 4326,
accuracy FLOAT,
speed FLOAT,
record_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
address VARCHAR(500)
);
CREATE INDEX idx_user_time ON user_locations(user_id, record_time);
CREATE SPATIAL INDEX idx_user_location ON user_locations(location);
3.2 创建地理围栏表
sql
-- 地理围栏表
CREATE TABLE geo_fences (
fence_id INT PRIMARY KEY,
fence_name VARCHAR(200),
fence_type VARCHAR(20), -- 'CIRCLE', 'POLYGON', 'ROUTE'
-- 圆形围栏:圆心和半径
center ST_POINT SRID 4326,
radius_meters FLOAT,
-- 多边形围栏
polygon ST_POLYGON SRID 4326,
-- 路线围栏(缓冲区)
route ST_LINESTRING SRID 4326,
route_buffer_meters FLOAT,
valid_start DATE,
valid_end DATE,
properties VARCHAR(4000) -- JSON格式存储额外属性
);
-- 围栏触发记录表
CREATE TABLE fence_triggers (
trigger_id BIGINT IDENTITY(1,1) PRIMARY KEY,
fence_id INT,
user_id INT,
event_type VARCHAR(20), -- 'ENTER', 'EXIT', 'INSIDE'
location ST_POINT SRID 4326,
trigger_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
FOREIGN KEY (fence_id) REFERENCES geo_fences(fence_id)
);
四、数据操作示例
4.1 插入空间数据
sql
-- 插入点数据(WGS84坐标系)
INSERT INTO store_locations (store_id, store_name, address, location)
VALUES (
1,
'北京旗舰店',
'北京市海淀区中关村大街1号',
ST_POINT('POINT(116.316836 39.983825)')
);
-- 使用ST_GeomFromText插入
INSERT INTO store_locations (store_id, store_name, location)
VALUES (
2,
'上海中心店',
ST_GeomFromText('POINT(121.472641 31.231706)', 4326)
);
-- 插入多边形围栏(商业区范围)
INSERT INTO geo_fences (fence_id, fence_name, fence_type, polygon)
VALUES (
1,
'中关村科技园区',
'POLYGON',
ST_GeomFromText('POLYGON((116.306 39.977, 116.320 39.977, 116.320 39.990, 116.306 39.990, 116.306 39.977))', 4326)
);
4.2 批量导入地理位置数据
sql
-- 创建批量导入存储过程
CREATE OR REPLACE PROCEDURE batch_import_stores()
AS
BEGIN
-- 示例:批量插入店铺数据
INSERT INTO store_locations (store_id, store_name, location)
VALUES
(101, '门店A', ST_Point(116.407526, 39.904030)),
(102, '门店B', ST_Point(116.403963, 39.915215)),
(103, '门店C', ST_Point(116.350676, 39.958702)),
(104, '门店D', ST_Point(116.486124, 39.996786));
COMMIT;
END;
/
CALL batch_import_stores();
五、LBS查询示例
5.1 基础空间查询
sql
-- 1. 查找最近的店铺(按距离排序)
SELECT
store_id,
store_name,
address,
ST_Distance(
location,
ST_Point(116.407526, 39.904030) -- 用户当前位置
) * 111319.5 AS distance_meters -- 转换为米
FROM store_locations
ORDER BY distance_meters
LIMIT 10;
-- 2. 查找指定半径内的店铺(5公里范围内)
SELECT
store_id,
store_name,
ST_AsText(location) AS coordinates,
ST_Distance(location, ST_Point(116.407526, 39.904030)) * 111319.5 AS distance_m
FROM store_locations
WHERE ST_Distance(location, ST_Point(116.407526, 39.904030)) * 111319.5 <= 5000
ORDER BY distance_m;
-- 3. 空间范围查询(矩形区域)
SELECT *
FROM store_locations
WHERE ST_Within(location,
ST_GeomFromText('POLYGON((116.30 39.90, 116.45 39.90, 116.45 40.00, 116.30 40.00, 116.30 39.90))', 4326)
);
5.2 地理围栏查询
sql
-- 1. 检查用户是否进入围栏
CREATE OR REPLACE PROCEDURE check_fence_trigger(
p_user_id INT,
p_longitude FLOAT,
p_latitude FLOAT
)
AS
v_point ST_POINT;
v_fence_id INT;
v_distance FLOAT;
BEGIN
-- 创建用户位置点
v_point := ST_Point(p_longitude, p_latitude);
-- 检查圆形围栏
FOR fence IN (
SELECT fence_id, center, radius_meters
FROM geo_fences
WHERE fence_type = 'CIRCLE'
AND valid_start <= SYSDATE
AND valid_end >= SYSDATE
) LOOP
v_distance := ST_Distance(v_point, fence.center) * 111319.5;
IF v_distance <= fence.radius_meters THEN
-- 记录触发事件
INSERT INTO fence_triggers(fence_id, user_id, event_type, location)
VALUES (fence.fence_id, p_user_id, 'ENTER', v_point);
COMMIT;
END IF;
END LOOP;
-- 检查多边形围栏
FOR fence IN (
SELECT fence_id, polygon
FROM geo_fences
WHERE fence_type = 'POLYGON'
AND valid_start <= SYSDATE
AND valid_end >= SYSDATE
) LOOP
IF ST_Within(v_point, fence.polygon) THEN
INSERT INTO fence_triggers(fence_id, user_id, event_type, location)
VALUES (fence.fence_id, p_user_id, 'INSIDE', v_point);
COMMIT;
END IF;
END LOOP;
END;
/
-- 2. 查询围栏内所有用户
SELECT DISTINCT u.user_id, u.record_time
FROM user_locations u
JOIN geo_fences g ON ST_Within(u.location, g.polygon)
WHERE g.fence_id = 1
AND u.record_time >= SYSDATE - INTERVAL '1' HOUR;
5.3 路径规划相关查询
sql
-- 1. 查找沿路径的店铺(路径缓冲区查询)
CREATE OR REPLACE FUNCTION find_stores_along_route(
route_coords CLOB, -- JSON格式的路径点
buffer_distance FLOAT -- 缓冲区距离(米)
)
RETURN TABLE(store_id INT, store_name VARCHAR(200), distance FLOAT)
AS
v_route ST_LINESTRING;
v_buffer ST_POLYGON;
BEGIN
-- 将JSON路径转换为线(这里简化处理,实际需要解析JSON)
v_route := ST_GeomFromText('LINESTRING(116.306 39.977, 116.310 39.978, 116.315 39.980)', 4326);
-- 创建缓冲区(将度转换为近似米)
v_buffer := ST_Buffer(v_route, buffer_distance / 111319.5);
-- 返回缓冲区内的店铺
RETURN QUERY
SELECT s.store_id, s.store_name,
ST_Distance(s.location, v_route) * 111319.5 AS distance
FROM store_locations s
WHERE ST_Within(s.location, v_buffer)
ORDER BY distance;
END;
/
-- 2. 最近设施查询(包含路线距离)
WITH user_location AS (
SELECT ST_Point(116.407526, 39.904030) AS loc
),
store_distances AS (
SELECT
s.*,
ST_Distance(s.location, u.loc) * 111319.5 AS straight_distance,
-- 这里可以调用路线规划API计算实际路线距离
0 AS route_distance -- 实际应用中替换为计算值
FROM store_locations s, user_location u
)
SELECT *
FROM store_distances
ORDER BY route_distance
LIMIT 5;
六、性能优化
6.1 空间索引优化
sql
-- 1. 查看空间索引信息
SELECT * FROM USER_SDO_GEOM_METADATA;
-- 2. 重建空间索引
ALTER SPATIAL INDEX idx_store_location REBUILD;
-- 3. 创建复合索引
CREATE INDEX idx_store_loc_name ON store_locations(store_name, location);
-- 4. 分区表提升性能
CREATE TABLE user_locations_partitioned (
record_id BIGINT,
user_id INT,
location ST_POINT,
record_time TIMESTAMP
)
PARTITION BY RANGE(record_time) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
PARTITION p202403 VALUES LESS THAN ('2024-04-01')
);
CREATE SPATIAL INDEX idx_part_loc ON user_locations_partitioned(location) LOCAL;
6.2 查询优化技巧
sql
-- 1. 使用空间索引提示
SELECT /*+ INDEX(store_locations idx_store_location) */
store_id, store_name
FROM store_locations
WHERE ST_DWithin(location, ST_Point(116.407526, 39.904030), 0.05); -- 约5公里
-- 2. 预计算距离并缓存
CREATE TABLE store_distance_cache AS
SELECT
s1.store_id AS store_id1,
s2.store_id AS store_id2,
ST_Distance(s1.location, s2.location) * 111319.5 AS distance_m
FROM store_locations s1
CROSS JOIN store_locations s2
WHERE s1.store_id < s2.store_id;
CREATE INDEX idx_cache ON store_distance_cache(store_id1, store_id2);
-- 3. 分页查询优化
CREATE OR REPLACE PROCEDURE find_nearby_stores_paged(
p_longitude FLOAT,
p_latitude FLOAT,
p_radius_km FLOAT,
p_page_num INT,
p_page_size INT
)
AS
v_offset INT;
v_limit INT;
BEGIN
v_offset := (p_page_num - 1) * p_page_size;
v_limit := p_page_size;
SELECT * FROM (
SELECT
store_id,
store_name,
ST_Distance(location, ST_Point(p_longitude, p_latitude)) * 111319.5 AS distance_m,
ROW_NUMBER() OVER (ORDER BY distance_m) AS rn
FROM store_locations
WHERE ST_Distance(location, ST_Point(p_longitude, p_latitude)) * 111319.5 <= p_radius_km * 1000
) WHERE rn > v_offset AND rn <= v_offset + v_limit;
END;
/
七、Java应用示例
Spring Boot集成
bash
# application.yml
spring:
datasource:
driver-class-name: dm.jdbc.driver.DmDriver
url: jdbc:dm://localhost:5236/LBS_DB
username: lbs_user
password: Lbs_123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
java
@Repository
public interface StoreRepository {
@Query(value = """
SELECT store_id as id, store_name as name,
ST_X(location) as lng, ST_Y(location) as lat,
ST_Distance(location, ST_Point(:lng, :lat)) * 111319.5 as distance
FROM store_locations
WHERE ST_DWithin(location, ST_Point(:lng, :lat), :radius / 111319.5)
ORDER BY distance
LIMIT :limit
""", nativeQuery = true)
List<StoreProjection> findNearbyStores(
@Param("lng") double longitude,
@Param("lat") double latitude,
@Param("radius") double radiusMeters,
@Param("limit") int limit
);
}
@Service
public class LbsService {
@Autowired
private StoreRepository storeRepository;
public List<StoreDTO> findStoresWithinRadius(
LocationPoint center,
double radiusKm,
int maxResults
) {
return storeRepository.findNearbyStores(
center.getLongitude(),
center.getLatitude(),
radiusKm * 1000,
maxResults
).stream()
.map(proj -> new StoreDTO(
proj.getId(),
proj.getName(),
proj.getLng(),
proj.getLat(),
proj.getDistance()
))
.collect(Collectors.toList());
}
}
八、最佳实践
8.1 数据验证
sql
-- 创建验证约束
ALTER TABLE store_locations
ADD CONSTRAINT chk_valid_location
CHECK (
ST_X(location) BETWEEN -180 AND 180
AND ST_Y(location) BETWEEN -90 AND 90
);
-- 创建验证函数
CREATE OR REPLACE FUNCTION validate_geo_fence(
p_fence_type VARCHAR,
p_center ST_POINT,
p_radius FLOAT,
p_polygon ST_POLYGON
)
RETURN BOOLEAN
AS
BEGIN
IF p_fence_type = 'CIRCLE' AND (p_center IS NULL OR p_radius <= 0) THEN
RETURN FALSE;
END IF;
IF p_fence_type = 'POLYGON' AND p_polygon IS NULL THEN
RETURN FALSE;
END IF;
RETURN TRUE;
END;
/
8.2 定时清理任务
sql
-- 创建位置数据清理作业
CREATE OR REPLACE PROCEDURE cleanup_old_locations(
p_retention_days INT DEFAULT 90
)
AS
BEGIN
-- 删除过期的用户位置记录
DELETE FROM user_locations
WHERE record_time < SYSDATE - p_retention_days;
-- 归档围栏触发记录
INSERT INTO fence_triggers_archive
SELECT * FROM fence_triggers
WHERE trigger_time < SYSDATE - 365;
DELETE FROM fence_triggers
WHERE trigger_time < SYSDATE - 365;
COMMIT;
-- 更新统计信息
DBMS_STATS.GATHER_TABLE_STATS('LBS_USER', 'USER_LOCATIONS');
END;
/
-- 创建定时任务
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'CLEANUP_LOCATIONS_JOB',
job_type => 'STORED_PROCEDURE',
job_action => 'LBS_USER.CLEANUP_OLD_LOCATIONS',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=DAILY;BYHOUR=2',
enabled => TRUE
);
九、监控与维护
空间数据统计
sql
-- 查看空间数据分布
SELECT
COUNT(*) as total_stores,
MIN(ST_X(location)) as min_lng,
MAX(ST_X(location)) as max_lng,
MIN(ST_Y(location)) as min_lat,
MAX(ST_Y(location)) as max_lat
FROM store_locations;
-- 空间索引使用统计
SELECT
index_name,
num_rows,
leaf_blocks,
distinct_keys
FROM user_indexes
WHERE index_type LIKE '%SPATIAL%';
-- 查询性能监控
SELECT
sql_text,
executions,
elapsed_time / 1000000 as elapsed_seconds,
buffer_gets
FROM v$sqlarea
WHERE sql_text LIKE '%ST_%'
ORDER BY elapsed_time DESC
LIMIT 10;
十、常见问题处理
10.1 坐标系转换
sql
-- WGS84转GCJ-02(中国坐标系)函数
CREATE OR REPLACE FUNCTION wgs84_to_gcj02(
wgs_lng FLOAT,
wgs_lat FLOAT
)
RETURN VARCHAR
AS
-- 实现坐标系转换算法(这里需要实现具体转换逻辑)
gcj_lng FLOAT;
gcj_lat FLOAT;
BEGIN
-- 这里添加坐标系转换算法
-- ...
RETURN CONCAT(gcj_lng, ',', gcj_lat);
END;
/
-- 创建转换视图
CREATE VIEW store_locations_gcj02 AS
SELECT
store_id,
store_name,
wgs84_to_gcj02(ST_X(location), ST_Y(location)) as gcj_coord
FROM store_locations;
10.2 性能问题排查
sql
-- 1. 检查空间索引有效性
SELECT
table_name,
index_name,
status
FROM user_indexes
WHERE index_type LIKE '%SPATIAL%';
-- 2. 分析空间查询执行计划
EXPLAIN PLAN FOR
SELECT * FROM store_locations
WHERE ST_DWithin(location, ST_Point(116.407526, 39.904030), 0.01);
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY());
-- 3. 监控空间函数性能
SELECT
function_name,
total_time,
calls
FROM v$function
WHERE function_name LIKE 'ST_%';
总结
本教程涵盖了达梦数据库在LBS应用中的核心功能实现,包括:
-
空间数据存储:使用达梦的空间数据类型存储地理位置信息
-
空间查询:实现距离计算、范围查询、空间关系判断
-
地理围栏:实现电子围栏的触发和监控
-
性能优化:空间索引、分区表、查询优化技巧
-
应用集成:Java和Spring Boot集成示例
-
运维监控:数据清理、性能监控、问题排查
达梦数据库提供了完整的空间数据处理能力,可以满足大多数LBS应用的需求。在实际应用中,还需要结合具体业务场景进行优化和扩展。