Swift Vapor中基于PostgreSQL实现用户按距离筛选与排序
Swift Vapor + PostgreSQL 实现按距离筛选并排序用户
需求说明
开发Swift Vapor项目,使用PostgreSQL数据库,需实现以下功能:
- 按距离筛选用户:仅返回距离当前用户不超过100公里的用户
- 按距离排序:最近的用户排在首位
- 要求使用
filter和sort查询方法,保证性能优化
已知用户模型包含可选的latitude(纬度)和longtitude(经度)字段,相关代码片段如下:
// 用户模型字段定义 @OptionalField(key: "latitude") var latitude: Float? @OptionalField(key: "longtitude") var longtitude: Float?
// 查询方法片段 func getAll(_ req: Request, arguments: UserFilter) async throws -> [User] { let user = try req.auth.require(User.self) var query = User.query(on: req.db) arguments.filter(&query) if let lat = user.latitude, let lon = user.longtitude { // 需要补充筛选和排序逻辑 } return try await query.all() }
实现方案
要保证性能,必须让距离计算和筛选在数据库层面完成,避免将所有用户加载到内存后再处理。PostgreSQL提供了专门的地理空间函数来高效计算球面距离,我们将使用ST_DWithin(筛选)和ST_DistanceSphere(排序)这两个函数。
1. (可选但推荐)添加地理索引优化性能
为了让距离查询更快,建议给用户表添加基于经纬度的地理索引。可以通过迁移文件实现:
struct AddUserGeographyIndex: AsyncMigration { func prepare(on database: Database) async throws { guard let db = database as? PostgreSQLDatabase else { throw Abort(.internalServerError, reason: "仅支持PostgreSQL") } try await db.raw(""" CREATE INDEX idx_user_geography ON users USING GIST (ST_MakePoint(longtitude, latitude)::geography); """).run() } func revert(on database: Database) async throws { guard let db = database as? PostgreSQLDatabase else { throw Abort(.internalServerError, reason: "仅支持PostgreSQL") } try await db.raw("DROP INDEX IF EXISTS idx_user_geography;").run() } }
注意:
ST_MakePoint的参数顺序是经度在前,纬度在后,这是PostGIS的标准要求。
2. 修改查询逻辑实现筛选和排序
在查询中,我们需要调用PostgreSQL的地理函数来完成筛选和排序:
func getAll(_ req: Request, arguments: UserFilter) async throws -> [User] { let user = try req.auth.require(User.self) var query = User.query(on: req.db) arguments.filter(&query) if let currentLat = user.latitude, let currentLon = user.longtitude { // 构造当前用户的地理点:ST_MakePoint(当前经度, 当前纬度)::geography let currentPoint = SQLFunction( "ST_MakePoint", args: SQLBind(currentLon), SQLBind(currentLat) ).cast(to: "geography") // 构造用户表的地理点:ST_MakePoint(longtitude, latitude)::geography let userPoint = SQLFunction( "ST_MakePoint", args: Column(User.self, \.$longtitude), Column(User.self, \.$latitude) ).cast(to: "geography") // 1. 筛选:距离不超过100公里(100000米) let distanceFilter = SQLFunction( "ST_DWithin", args: userPoint, currentPoint, SQLBind(100000) // 单位:米 ) query.filter(distanceFilter) // 2. 排序:按距离从小到大排序 let distanceSort = SQLFunction( "ST_DistanceSphere", args: userPoint, currentPoint ) query.sort(distanceSort, .ascending) } return try await query.all() }
关键说明
ST_DWithin:直接判断两个地理点的球面距离是否小于等于指定值(单位:米),配合地理索引可以大幅提升查询速度ST_DistanceSphere:计算两个地理点的球面距离(单位:米),用于排序- 所有计算都在数据库端完成,避免了内存中处理大量数据的性能损耗
- 注意经纬度的顺序:PostGIS的
ST_MakePoint要求经度在前,纬度在后,不要搞反
补充说明
如果你的项目没有引入PostGIS扩展,需要先在PostgreSQL中启用它:
CREATE EXTENSION IF NOT EXISTS postgis;
可以通过迁移文件执行这条SQL,或者直接在数据库管理工具中运行。
内容的提问来源于stack exchange,提问作者Artemiy Ziuzin
相关产品推荐
相关产品推荐

