我不相信 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 <
谓词,并大大减少缓存表的大小。