You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 20:33:16