1

我有这个查询

SELECT zip, 
( 3959 * acos( cos( radians(34.12520) ) * cos( radians( zip_info.latitude ) ) * cos(radians( zip_info.longitude ) - radians(-118.29200) ) + sin( radians(34.12520) ) * sin( radians( zip_info.latitude ) ) ) ) AS distance, 
user_info.*, office_locations.* 

FROM zip_info 

RIGHT JOIN office_locations ON office_locations.zipcode = zip_info.zip 

RIGHT JOIN user_info ON office_locations.doctor_id = user_info.id 

WHERE user_info.status='yes' 

HAVING distance < 50 ORDER BY distance ASC

它输出

距离 | 医生 ID | 等等

7 --------------- 5 -------- 等

8 --------------- 4 -------- 等等

34 --------------- 4 -------- 等等

49 --------------- 5 -------- 等等

当我选择 30 或更小的距离时,它也会显示前两个结果,这很好。

问题:我不想为每个医生 ID 显示多个结果,所以我做了一个 GROUP BY user_info.doctor_id,当距离小于 50 时不显示任何结果。出于某种原因,它希望将所有结果分组,否则它不会工作。有小费吗?你还有什么需要帮助我的吗?

所以我想要的是

距离 | 医生 ID | 等等

7 --------------- 5 -------- 等

8 --------------- 4 -------- 等等

即使它想给我所有 4 行的结果,我只想对它们进行分组,以便只有每个唯一 user_info.doctor_id 距离最小的那些才会出现。请记住,距离是一个虚拟的不存在的表。


根据 llion 的查询,结果如下:

 (concat(user_info.id))     zip     distance    id
          1                 NULL    6.6643992   1 

它只给出一个结果,为了让它工作,我不得不再次将 AND 更改为 HAVING 距离。

4

2 回答 2

1

我不相信 GROUP BY 会给你你想要的结果。不幸的是,MySQL 不支持分析函数(这就是我们在 Oracle 或 SQL Server 中解决这个问题的方法。)

通过使用用户定义的变量,可以模拟一些基本的分析函数。

在这种情况下,我们要模拟:

ROW_NUMBER() OVER(PARTITION BY doctor_id ORDER BY distance ASC) AS seq

因此,从原始查询开始,我更改了 ORDER BY 以便它首先排序doctor_id,然后在计算的distance. (在我们知道这些距离之前,我们不知道哪一个是“最近的”。)

有了这个排序的结果,我们基本上为每个医生 ID 的行“编号”,最接近的为 1,第二接近的为 2,依此类推。当我们得到一个新的医生 ID 时,我们从最接近的 1 重新开始。

为此,我们使用了用户定义的变量。我们使用一个来分配行号(变量名是@i,返回的列有别名seq)。我们用来“记住”前一行的医生ID的另一个变量,因此我们可以检测到医生ID中的“中断”,因此我们可以知道何时再次重新开始编号为1的行。

这是查询:


SELECT z.*
, @i := CASE WHEN z.doctor_id = @prev_doctor_id THEN @i + 1 ELSE 1 END AS seq
, @prev_doctor_id := z.doctor_id AS prev_doctor_id
FROM
(

  /* original query, ordered by doctor_id and then by distance */
  SELECT zip, 
  ( 3959 * acos( cos( radians(34.12520) ) * cos( radians( zip_info.latitude ) ) * cos(radians( zip_info.longitude ) - radians(-118.29200) ) + sin( radians(34.12520) ) * sin( radians( zip_info.latitude ) ) ) ) AS distance, 
  user_info.*, office_locations.* 
  FROM zip_info 
  RIGHT JOIN office_locations ON office_locations.zipcode = zip_info.zip 
  RIGHT JOIN user_info ON office_locations.doctor_id = user_info.id 
  WHERE user_info.status='yes' 
  ORDER BY user_info.doctor_id ASC, distance ASC

) z JOIN (SELECT @i := 0, @prev_doctor_id := NULL) i
HAVING seq = 1 ORDER BY z.distance

我假设原始查询正在返回您需要的结果集,它只是有太多行,并且您想要消除每个医生 ID 的“最接近”(距离最小值的行)之外的所有内容。

我已将您的原始查询包装在另一个查询中;我对原始查询所做的唯一更改是按医生 ID 排序结果,然后按距离排序,并删除该HAVING distance < 50子句。(如果您只想返回小于 50 的距离,那么继续并保留该子句。目前尚不清楚这是否是您的意图,或者是否是为了将行限制为每个医生 ID 的一个而指定的。)

需要注意的几个问题:

替换查询返回两个附加列;这些在结果集中并不是真正需要的,除非作为生成结果集的手段。(可以将整个 SELECT 再次包装在另一个 SELECT 中以省略这些列,但这确实比它的价值更混乱。我只会检索这些列,并且知道我可以忽略它们。)

另一个问题是.*在内部查询中使用 有点危险,因为我们确实需要保证该查询返回的列名是唯一的。(即使现在列名不同,向其中一个表中添加列也可能在查询中引入“不明确”列异常。最好避免这种情况,这很容易通过.*将要返回的列,并为任何“重复”列名指定别名。(z.*在外部查询中使用 不是问题,只要我们控制由 . 返回的列z。)


附录:

我注意到 GROUP BY 不会为您提供所需的结果集。虽然可以通过使用 GROUP BY 的查询获得结果集,但返回正确结果集的语句将是乏味的。您可以指定MIN(distance) ... GROUP BY doctor_id,这将使您获得最小的距离,但不能保证 SELECT 列表中的其他非聚合表达式将来自具有最小距离的行,而不是其他行。(MySQL 在 GROUP BY 和聚合方面非常自由。为了让 MySQL 引擎更加谨慎(并与其他关系数据库引擎保持一致),SET sql_mode = ONLY_FULL_GROUP_BY

附录 2:

Darious 报告的性能问题“某些查询需要 7 秒”。

为了加快速度,您可能希望缓存函数的结果。基本上,建立一个查找表。例如

CREATE TABLE office_location_distance
( office_location_id INT UNSIGNED NOT NULL COMMENT 'PK, FK to office_location.id'
, zipcode_id         INT UNSIGNED NOT NULL COMMENT 'PK, FK to zipcode.id'
, gc_distance        DECIMAL(18,2)         COMMENT 'calculated gc distance, in miles'
, PRIMARY KEY (office_location_id, zipcode_id)
, KEY (zipcode_id, gc_distance, office_location_id)
, CONSTRAINT distance_lookup_office_FK
  FOREIGN KEY (office_location_id) REFERENCES office_location(id)
  ON UPDATE CASCADE ON DELETE CASCADE
, CONSTRAINT distance_lookup_zipcode_FK
  FOREIGN KEY (zipcode_id) REFERENCES zipcode(id)
  ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB

这只是一个想法。(我希望您正在搜索特定邮政编码的 office_location 距离,因此 (zipcode, gc_distance, office_location_id) 上的索引是您的查询需要的覆盖索引。(我会避免将计算出的距离存储为 FLOAT,因为差FLOAT 数据类型的查询性能)

INSERT INTO office_location_distance (office_location_id, zipcode_id, gc_distance)
SELECT d.office_location_id
     , d.zipcode_id
     , d.gc_distance
  FROM (
         SELECT l.id AS office_location_id
              , z.id AS zipcode_id
              , ROUND( <glorious_great_circle_calculation> ,2) AS gc_distance
           FROM office_location l
          CROSS
           JOIN zipcode z
          ORDER BY 1,3
       ) d
ON DUPLICATE KEY UPDATE gc_distance = VALUES(gc_distance)

通过缓存和索引函数结果,您的查询应该更快。

SELECT d.gc_distance, o.*
  FROM office_location o
  JOIN office_location_distance d ON d.office_location_id = o.id
 WHERE d.zipcode_id = 63101
   AND d.gc_distance <= 100.00
 ORDER BY d.zipcode_id, d.gc_distance

我对在缓存表的 INSERT/UPDATE 上添加 HAVING 谓词犹豫不决;(如果您的纬度/经度有误,并且计算了 100 英里以下的错误距离;在纬度/经度固定且距离达到 1000 英里之后的后续运行...如果该行从查询中排除,那么缓存表中的现有行将不会被更新。(您可以清除缓存表,但这并不是必需的,这只是对数据库和日志的大量额外工作。如果维护查询的结果集太大,可以分解为每个邮政编码或每个 office_location 迭代运行。)

另一方面,如果您对超过某个值的任何距离不感兴趣,则可以添加HAVING gc_distance <谓词,并大大减少缓存表的大小。

于 2012-06-19T22:19:10.457 回答
1

HAVING 子句基于聚合结果进行过滤。当您将 GROUP BY 添加到语句中时,构成该distance列的方程式会为单个医生 ID 的所有行添加值。因此,距离最终将是:

distance | doctor_id | etc
      56 |         5 | etc
      42 |         4 | etc

如您所见,医生 ID 5 > 50。如果医生 ID 4 没有返回结果,那么我假设您没有显示更多行。

您想要的是距离小于 50 的不同医生 ID。您想要最小、最大、平均距离吗?也许这就是你想要的(我没有测试过这个,我认为你需要围绕 zip 值进行分组):

SELECT distinct(concat(zip,user_info.doctor_id)), zip, min(( 3959 * acos( cos( radians(34.12520) ) * cos( radians( zip_info.latitude ) ) * cos(radians( zip_info.longitude ) - radians(-118.29200) ) + sin( radians(34.12520) ) * sin( radians( zip_info.latitude ) ) ) )) AS distance, 
user_info.doctor_id

FROM zip_info 

RIGHT JOIN office_locations ON office_locations.zipcode = zip_info.zip 

RIGHT JOIN user_info ON office_locations.doctor_id = user_info.id 

WHERE user_info.status='yes' 
AND distance < 50 ORDER BY distance ASC

这应该提供独特的 zip/doctor 分组,而无需添加其他内容。

于 2012-06-19T22:19:41.513 回答