在SQLite中基于半径获取记录的查询?

29

我有这个查询,在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.345lon=67.89

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

如何在SQLite中执行上述查询?


1
你有两个选择,可以在数据库外计算或者将你的坐标投影到一个椭球体上,这样可以得到米数并使用常规距离公式作为近似值。 - Vinko Vrsalovic
我对这个解决方案持开放态度,您能否详细说明一下呢? - Pentium10
2
对于像我这样的初学者,整个公式被称为“球面余弦定理”,并在此处描述:http://www.movable-type.co.uk/scripts/latlong.html - Afriza N. Arief
请分享此代码。 - Parag Chauhan
1
查看此回答 https://dev59.com/0XA65IYBdhLWcg3wrQh4#12997900 - Bobs
3个回答

32

这是一个用Java实现的在Android设备上构建基于位置查询的例子。

这个想法来自于KennyTM(见被接受的回答),它需要在你的表中添加4列,以存储纬度和经度的正弦和余弦值。

以下是为“商店”表准备插入数据的代码:

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;
}

如果你想使用偏移距离(partial distance)排序查询结果,那么你需要按照降序(DESC)而不是升序(ASC)的方式排序。


1
请注意,这种方法从磁盘读取所有位置并对每个位置进行计算(即使您限制了半径!)。它也与索引不兼容(始终执行完整的表扫描)。通过使用在此问题中描述的方法首先限制边界,并使用上述方法过滤/排序结果的子查询,可以大大加速查询。而且,您可以在第一个查询中使用索引,而不是这个查询! - user1643723

17

在使用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)

  3. 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
    
  4. 您的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;


1
我喜欢这种方法,但方程式正确吗?我看到的大多数方程式都有acos()。你的没有。我有什么遗漏吗? - Sam
1
我猜他们使用“左侧与cos(2)相比较”来代替“acos(左侧)与2相比较”,这应该是等效的。 - Slawa

15
您可以创建4个新的列,分别是latlon的正弦和余弦值。由于cos(a+b) = cos a cos b - sin a sin b,以及其他出现在程序中的像SIN(12.345 * PI() / 180)这样的sin和cos值可以在运行查询之前在程序中计算,所以大的"distance"表达式可以简化为形如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中,并且不清楚是否存在一个org.sqlite实现可以与Android的SQLite环境协同工作。 - Pentium10
你的方法听起来可行,可以添加一些列。 - Pentium10
那么你如何处理 ACOS()?使用 sqlite3_create_function() 创建自定义 SQL 函数?在 Android 中该怎么做? - Afriza N. Arief
你需要在插入时维护一组硬编码的值。你需要在一个新列中插入这些值,同时计算Java中的acos()值。 - Pentium10

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接