【问题标题】:Eliminate subquery for average numeric value消除平均数值的子查询
【发布时间】:2011-02-16 06:10:24
【问题描述】:

任务

查询选择所有以“Vancouver”开头且距离以“Vancouver”开头的所有位置的中心 5 分钟范围内的点。例如,Vancouver South Fraser、Vancouver Fairview 和 Vancouver Ballantree Place W 的纬度和经度在其平均经纬度的 5 分钟内。纬度和经度存储为 (4915, 12311) 整数对(表示 49.15'N 和 123.11'W)。

SQL 代码

下面的 SQL 可恶做到了这一点:

SELECT
  NAME
FROM
 STATION
WHERE
      DISTRICT_ID = '110'
  AND NAME LIKE 'Vancouver%'
  AND LATITUDE BETWEEN
    (SELECT round((min(LATITUDE) + max(LATITUDE)) / 2)-5 FROM STATION WHERE DISTRICT_ID = '110' AND NAME LIKE 'Vancouver%')
    and
    (SELECT round((min(LATITUDE) + max(LATITUDE)) / 2)+5 FROM STATION WHERE DISTRICT_ID = '110' AND NAME LIKE 'Vancouver%')
  AND LONGITUDE BETWEEN
    (SELECT round((min(LONGITUDE) + max(LONGITUDE)) / 2)-5 FROM STATION WHERE DISTRICT_ID = '110' AND NAME LIKE 'Vancouver%')
    and
    (SELECT round((min(LONGITUDE) + max(LONGITUDE)) / 2)+5 FROM STATION WHERE DISTRICT_ID = '110' AND NAME LIKE 'Vancouver%')
ORDER BY
  LATITUDE

问题

如何在不使用视图的情况下简化此查询以消除冗余?

限制

数据库是 MySQL,但 ANSI SQL 总是很好。

谢谢!

【问题讨论】:

  • 你能用简单的英语描述一下你想用这个查询来完成什么吗?
  • @nineside:更新了 SQL 的目的。
  • 不是更准确的描述:选择所有以“Vancouver”开头且距离所有以“Vancouver”开头的位置中心 5 分钟半径范围内的点吗?
  • @eagle:你是对的。
  • 您没有在半径范围内得到结果,因为您需要查看距离,即 sqrt(sqr(x2-x1)+sqr(y2-y1))。你得到的是矩形内的结果。

标签: sql mysql ansi-sql


【解决方案1】:
select 
  name
from 
  (select 
    round((min(LATITUDE) + max(LATITUDE)) / 2) as LATITUDE,
    round((min(LONGITUDE) + max(LONGITUDE)) / 2) as LONGITUDE
   from STATION 
   where DISTRICT_ID = '110' 
     AND NAME LIKE 'Vancouver%') AS center
  inner join STATION s
where
  s.DISTRICT_ID = '110' 
  and s.NAME like 'Vancouver%'
  and s.LATITUDE between center.LATITUDE - 5 and center.LATITUDE + 5
  and s.LONGITUDE between center.LONGITUDE - 5 and center.LONGITUDE + 5

【讨论】:

  • 这正是我试图将我的大脑包裹起来的东西。谢谢。
  • 嗯,好的,但不是最佳的 - 如果将查询重写为 ABS(latitude-(SELECT avg(latitude))
  • @eagle: -1,根据 Unreason,您会在投影的正方形而不是圆形中找到东西,但您也没有解决经度/纬度的整数表示问题,并且您对中心的计算是错误的 - 请参阅其他地方的 Unreason 答案
【解决方案2】:

首先请注意,您的“彼此相距 5 分钟内”的定义并未定义单一解决方案,并且您的 (MIN()+MAX())/2 不是平均值,而只是最小值和最大值的中间值。您可能正在子查询中寻找 AVG()。

其次,您不是在 5 秒内获得结果,而是在经度和纬度最多 10 秒(对角线上可能更接近 14)的条目。

在mysql中你可以使用会话变量,例如:

SET @avg_lat := (SELECT round(avg(LATITUDE)) FROM STATION WHERE DISTRICT_ID = '110' AND NAME LIKE 'Vancouver%');
SET @avg_long := (SELECT round(avg(LONGITUDE)) FROM STATION WHERE DISTRICT_ID = '110' AND NAME LIKE 'Vancouver%');

SELECT
  NAME
FROM
 STATION
WHERE
  DISTRICT_ID = '110'
  AND pow(LATITUDE-@avg_lat,2)+pow(LONGITUDE-@avg_long,2)<25
ORDER BY
  LATITUDE

即使没有必要(正如上面写的查询,两个变量只出现一次)。

编辑:糟糕,误读了这个问题。它是中心的半径 - 因此将 25 替换为 100(同时决定是否要使用更少或等于)。此外,如果中心是边界框的中心,那么您的 (min()+max())/2 是正确的公式,而不是我的建议。 '所有位置的中心'仍然有点模糊,所以我留下我的答案(很容易改变它)。

EDIT2:刚刚注意到我的查询中的单位不正确,如果纬度以厘分存储,那么比较也应该是厘分 (10*100)^2=1000000

最后,您坚持使用 (min()+max())/2 的决定将导致您可能有一行远离 max 和 min 的情况,这可能会使查询错过任何结果(并且可能发生,通常所有具有相似名称的位置都彼此相邻,但通常会有另一个位置以相同名称开头,这是一个远离位置组合的孤立位置)

至于 5 分钟区域,更准确地说是 10x10 分钟区域,这就是查询返回的内容。

EDIT3:如果您远离赤道,上面用于距离的公式不是很精确。 这是距离formula 的更好近似值 对于严肃的工作,您可能需要this

【讨论】:

  • 谢谢。我可能不得不回来重新检查要求。
  • 这是一个很好的答案,最终将是我所需要的。我决定使用不同的数据集来获取城市的纬度/经度,然后使用此代码查找给定半径内的所有站点。
  • 好的,最后的部分实际上是这样的:dev.mysql.com/doc/refman/5.1/en/spatial-extensions.html,如果您的应用程序确实花费大量时间处理空间数据,请查看它(注意,我没有在 mysql 上使用空间扩展,仅在 postgres 和 oracle 上,但我知道这些是相对较新的功能,所以请注意你的步骤;空间索引数据和定期索引数据之间的性能差异仍然可以轻松达到 5-6 个数量级)
  • 我意识到,正如你所暗示的,总体思路是有缺陷的。原来 MaxMind.com 发布了他们的城市和纬度/经度数据库。我已经集成了它;用户选择城市名称,然后 SQL 将所选城市的位置与车站位置进行匹配。这是一个干净的解决方案。再次感谢您的帮助!
【解决方案3】:

使用公用表表达式 ...

with cte as
 (  SELECT round((min(LATITUDE) + max(LATITUDE)) / 2)-5 min_lat
           , round((min(LATITUDE) + max(LATITUDE)) / 2)+5 max_lat
           , round((min(LONGITUDE) + max(LONGITUDE)) / 2)-5 min_long
           , round((min(LONGITUDE) + max(LONGITUDE)) / 2)+5 max_long
           , DISTRICT_ID
           ,  'Vancouver%' AS NAME 
    FROM STATION 
    WHERE DISTRICT_ID = '110' 
AND NAME LIKE 'Vancouver%'
group by DISTRICT_ID,  'Vancouver%')
SELECT
  NAME
FROM
 STATION , cte
WHERE
      station.DISTRICT_ID = cte.DISTRICT_ID
  AND station.NAME LIKE cte.NAME
  AND station.LATITUDE BETWEEN cte.min_lat AND cte.max_lat
   AND station.LONGITUDE BETWEEN cte.min_long AND cte.max_long
ORDER BY
  station.LATITUDE

注意:我现在无法访问数据库,因此无法对此进行测试。因此,我不能保证它是有效的。我会尽可能地测试它。原则 持有。

【讨论】:

  • 我从不记得公用表表达式。谢谢。
  • @APC - AFAIK,MySql 不支持 CTE。
  • @Thomas - 抱歉,我不是一个真正的 MySQL 小伙子,但 Dave 确实要求提供 ANSI SQL 解决方案 :) 无论如何,你是对的:MySQL 目前不支持 WITH 子句,尽管最后一个此错误报告中的消息表明它可能即将出现bugs.mysql.com/bug.php?id=16244
【解决方案4】:

我假设原始查询为您的目的提供了准确的结果。如果是这种情况,那么您可以通过将端点的计算放入子查询中来合并查询。

Select ...
From Station As S
    Cross Join  (
                Select Round( (Min(S1.Latitude)  + Max(S1.Latitude)) / 2 ) As Latitude
                    , Round( (Min(S1.Longitude)  + Max(S1.Longitude)) / 2 ) As Longitude
                From Station As S1
                Where S1.District_Id = '110'
                    And S1.Name Like 'Vancouver%'
                ) As S2
Where S.District_Id = '110'
    And S.Name Like 'Vancouver%'
    And  S.Latitude Between (S2.Latitude - 5) And (S2.Latitude + 5)
    And  S.Longitude Between (S2.Longitude - 5) And (S2.Longitude + 5)
Order By S.Latitude

【讨论】:

    【解决方案5】:

    无论老毕达哥拉斯发生了什么(好吧,我知道它并不真正适用于曲面 - 但应该是一个足够好的近似值)。如果您正在寻找坐标对集合的中心(实际上,质心 基于物理学家而不是几何学家应用的解释),那么您不应该使用 MIN 和 MAX,尽管您可能考虑基于 MIN 和 MAX 限制搜索)。唯一美中不足的是你存储了一个整数表示,它是一个坐标角的字符串表示。

    考虑:

    SELECT b.name
    FROM
    (SELECT AVG(CALC(a.lattitude)) AS c_lat, AVG(CALC(a.longitude)) AS c_long
      FROM station a 
      WHERE a.district_id='110'
      AND a.name like 'VANCOUVER%'
    ) AS ilv,
    station b
    WHERE b.district_id='110'
    AND b.name LIKE 'VANCOUVER%'
    AND POW(ilv.c_lat-CALC(b.lattitude),2)
         + POW(olv.c_long-CALC(b.longitude),2)<=25;
    

    其中 CALC 函数将存储的值转换为以分钟为单位的经度/纬度,即

    CALC(x)=(FLOOR(x/100)*60+MOD(x,100))
    

    C.

    【讨论】:

    • 我不会说您需要 CALC,这些值以厘分为单位,只需将它们除以 100。否则我猜示例会说 49 度 15 分钟而不是 49 点 15 分钟。
    • 是的 - 你是对的 - 我只是习惯于看到坐标以度/分/秒为单位
    猜你喜欢
    • 2013-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-02
    • 1970-01-01
    • 2020-07-05
    • 2021-05-26
    相关资源
    最近更新 更多