【问题标题】:Query to get records based on Radius in SQLite?在 SQLite 中查询基于半径的记录?
【发布时间】:2011-03-08 19:07:21
【问题描述】:

我有这个在 MySQL 中运行良好的查询

SELECT ((ACOS(SIN(12.345 * PI() / 180) * SIN(lat * PI() / 180) +
         COS(12.345 * PI() / 180) * COS(lat * PI() / 180) * COS((67.89 - lon) * 
         PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344) AS distance, poi.* 
FROM poi
WHERE lang='eng' 
HAVING distance<='30'

距离单位为公里,输入为lat=12.345和lon=67.89

SQLite 是 3,我无法使用它运行自定义函数,因为它在 Android 上。我也没有 acos() 等...因为这不是标准 SQLite 的一部分。

上面的查询在 SQLite 中会怎样?

【问题讨论】:

  • 您有两个选择,在数据库之外计算或将坐标投影到椭球体上,这样可以获得米数并允许您使用常规距离公式作为近似值。
  • 我对这个解决方案持开放态度,请您在答案中详细说明。
  • 对于像我这样的初学者,整个公式称为Spherical Law of Cosines,在这里描述:movable-type.co.uk/scripts/latlong.html
  • 请分享此答案的代码

标签: sql android sqlite


【解决方案1】:

这是一个 Java 实现,用于在 Android 设备上构建基于位置的查询。 这个想法来自 KennyTM(请参阅接受的回复)并暗示在您的表中添加 4 列来存储纬度和经度的正弦值和余弦值。

这是在插入时为“Shop”表准备数据的代码:

public static void injectLocationValues(ContentValues values, double latitude, double longitude) {
    values.put(LocationColumns.LATITUDE, latitude);
    values.put(LocationColumns.LONGITUDE, longitude);
    values.put(LocationColumns.COSLAT, Math.cos(MathUtil.deg2rad(latitude)));
    values.put(LocationColumns.SINLAT, Math.sin(MathUtil.deg2rad(latitude)));
    values.put(LocationColumns.COSLNG, Math.cos(MathUtil.deg2rad(longitude)));
    values.put(LocationColumns.SINLNG, Math.sin(MathUtil.deg2rad(longitude)));
}

public static double deg2rad(double deg) {
    return (deg * Math.PI / 180.0);
}

然后您可以使用以下函数构建您的投影:

/**
 * Build query based on distance using spherical law of cosinus
 * 
 * d = acos(sin(lat1).sin(lat2)+cos(lat1).cos(lat2).cos(long2−long1)).R
 * where R=6371 and latitudes and longitudes expressed in radians
 * 
 * In Sqlite we do not have access to acos() sin() and lat() functions.
 * Knowing that cos(A-B) = cos(A).cos(B) + sin(A).sin(B)
 * We can determine a distance stub as:
 * d = sin(lat1).sin(lat2)+cos(lat1).cos(lat2).(cos(long2).cos(long1)+sin(long2).sin(long1))
 * 
 * First comparison point being fixed, sin(lat1) cos(lat1) sin(long1) and cos(long1)
 * can be replaced by constants.
 * 
 * Location aware table must therefore have the following columns to build the equation:
 * sinlat => sin(radians(lat))
 * coslat => cos(radians(lat))
 * coslng => cos(radians(lng))
 * sinlng => sin(radians(lng))
 *  
 * Function will return a real between -1 and 1 which can be used to order the query.
 * Distance in km is after expressed from R.acos(result) 
 *  
 * @param latitude, latitude of search
 * @param longitude, longitude of search
 * @return selection query to compute the distance
 */
public static String buildDistanceQuery(double latitude, double longitude) {
    final double coslat = Math.cos(MathUtil.deg2rad(latitude));
    final double sinlat = Math.sin(MathUtil.deg2rad(latitude));
    final double coslng = Math.cos(MathUtil.deg2rad(longitude));
    final double sinlng = Math.sin(MathUtil.deg2rad(longitude));
    //@formatter:off
    return "(" + coslat + "*" + LocationColumns.COSLAT
            + "*(" + LocationColumns.COSLNG + "*" + coslng
            + "+" + LocationColumns.SINLNG + "*" + sinlng
            + ")+" + sinlat + "*" + LocationColumns.SINLAT 
            + ")";
    //@formatter:on
}

它将注入一个响应列,其中包含您需要应用以下公式转换为公里的距离:

public static double convertPartialDistanceToKm(double result) {
    return Math.acos(result) * 6371;
}

如果您想使用部分距离对查询进行排序,则需要订购 DESC 而不是 ASC。

【讨论】:

  • 请注意,这种方法从磁盘读取所有位置并为每个位置进行计算(即使您限制了半径!)。它也与索引不兼容(总是执行全表扫描)。使用上述方法,使用this question first 中描述的方法和使用子查询对结果进行过滤/排序,可以大大加快查询速度。并且您可以在第一个查询中使用与此不同的索引!
【解决方案2】:

在为 ios 开发 sqlite3 时遇到了同样的问题,在玩了一点公式之后,这是一种不使用 sql 端函数(伪代码)的方法:

  1. 为您存储在数据库中的每个项目预先计算这些值(并存储它们):

    cos_lat = cos(lat * PI / 180)
    sin_lat = sin(lat * PI / 180)
    cos_lng = cos(lng * PI / 180)
    sin_lng = sin(lng * PI / 180)
    
  2. 在搜索时预先计算这些值(对于给定的位置 cur_lat,cur_lng)

    CUR_cos_lat = cos(cur_lat * PI / 180)
    CUR_sin_lat = sin(cur_lat * PI / 180)
    CUR_cos_lng = cos(cur_lng * PI / 180)
    CUR_sin_lng = sin(cur_lng * PI / 180)
    cos_allowed_distance = cos(2.0 / 6371) # This is 2km
    
  3. 您的 SQL 查询将如下所示(将 CUR_* 替换为您刚刚计算的值)

    SELECT * FROM position WHERE CUR_sin_lat * sin_lat + CUR_cos_lat * cos_lat * (cos_lng* CUR_cos_lng + sin_lng * CUR_sin_lng) > cos_allowed_distance;

【讨论】:

  • 我喜欢这种方法,但等式正确吗?我见过的大多数方程都有 acos()。你的没有。我错过了什么吗?
  • 我猜,他们使用 left part compare to cos(2) 而不是 acos(left part) compare to 2,这应该是等效的。
【解决方案3】:

您可以创建 4 个新列,分别是 lat 和 lon 的 sin 和 cos。由于cos(a+b) = cos a cos b - sin a sin b,以及其他像SIN(12.345 * PI() / 180)这样的sin和cos的出现可以在运行查询之前在程序中计算出来,所以大的“距离”表达式简化为P * SIN_LAT + Q * COS_LAT + ...形式的东西,可以由SQLite3处理。

顺便说一句,另请参阅Sqlite on Android: How to create a sqlite dist db function - to be used in the app for distance calculation using lat, long。

【讨论】:

  • 实际上,我对您链接到的内容提供商答案中概述的方法持怀疑态度。 org.sqlite 不在 Android 中,目前尚不清楚是否存在与 Android 的 SQLite 环境协同工作的 org.sqlite 实现。
  • 您添加一些列的方法听起来可行。
  • 那么你如何处理ACOS()?使用sqlite3_create_function() 创建自定义 SQL 函数?你是如何在 Android 中做到这一点的?
  • 您需要在插入时维护一组硬编码的值。您可以将在 Java 中计算的 acos() 值与新列中的值一起插入。
猜你喜欢
  • 1970-01-01
  • 2020-08-02
  • 1970-01-01
  • 1970-01-01
  • 2010-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-15
相关资源
最近更新 更多