我需要一些帮助来优化此过程:
DELIMITER $$
CREATE DEFINER=`ryan`@`%` PROCEDURE `GetCitiesInRadius`(
cityID numeric (15),
`range` numeric (15)
)
BEGIN
DECLARE lat1 decimal (5,2);
DECLARE long1 decimal (5,2);
DECLARE rangeFactor decimal (7,6);
SET rangeFactor = 0.014457;
SELECT `latitude`,`longitude` into lat1,long1
FROM world_cities as wc WHERE city_id = cityID;
SELECT
wc.city_id,
wc.accent_city as city,
s.state_name as state,
c.short_name as country,
GetDistance(lat1, long1, wc.`latitude`, wc.`longitude`) as dist
FROM world_cities as wc
left join states s on wc.state_id = s.state_id
left join countries c on wc.country_id = c.country_id
WHERE
wc.`latitude` BETWEEN lat1 -(`range` * rangeFactor) AND lat1 + (`range` * rangeFactor)
AND wc.`longitude` BETWEEN long1 - (`range` * rangeFactor) AND long1 + (`range` * rangeFactor)
AND GetDistance(lat1, long1, wc.`latitude`, wc.`longitude`) <= `range`
ORDER BY dist limit 6;
END
以下是我对查询主要部分的解释:
+----+-------------+-------+--------+---------------+--------------+---------+--------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+--------------+---------+--------------------------+------+----------------------------------------------+
| 1 | SIMPLE | B | range | idx_lat_long | idx_lat_long | 12 | NULL | 7619 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | s | eq_ref | PRIMARY | PRIMARY | 4 | civilipedia.B.state_id | 1 | |
| 1 | SIMPLE | c | eq_ref | PRIMARY | PRIMARY | 1 | civilipedia.B.country_id | 1 | Using where |
+----+-------------+-------+--------+---------------+--------------+---------+--------------------------+------+----------------------------------------------+
3 rows in set (0.00 sec)
以下是索引:
mysql> show indexes from world_cities;
+--------------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| world_cities | 0 | PRIMARY | 1 | city_id | A | 3173958 | NULL | NULL | | BTREE | |
| world_cities | 1 | country_id | 1 | country_id | A | 23510 | NULL | NULL | YES | BTREE | |
| world_cities | 1 | city | 1 | city | A | 3173958 | NULL | NULL | YES | BTREE | |
| world_cities | 1 | accent_city | 1 | accent_city | A | 3173958 | NULL | NULL | YES | BTREE | |
| world_cities | 1 | idx_pop | 1 | population | A | 28854 | NULL | NULL | YES | BTREE | |
| world_cities | 1 | idx_lat_long | 1 | latitude | A | 1057986 | NULL | NULL | YES | BTREE | |
| world_cities | 1 | idx_lat_long | 2 | longitude | A | 3173958 | NULL | NULL | YES | BTREE | |
| world_cities | 1 | accent_city_2 | 1 | accent_city | NULL | 1586979 | NULL | NULL | YES | FULLTEXT | |
+--------------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
8 rows in set (0.01 sec)
您在查询中看到的功能我认为不会导致速度变慢,但这是函数:
CREATE DEFINER=`ryan`@`%` FUNCTION `GetDistance`(lat1 numeric (9,6),
lon1 numeric (9,6),
lat2 numeric (9,6),
lon2 numeric (9,6) ) RETURNS decimal(10,5)
BEGIN
DECLARE x decimal (20,10);
DECLARE pi decimal (21,20);
SET pi = 3.14159265358979323846;
SET x = sin( lat1 * pi/180 ) * sin( lat2 * pi/180 ) + cos(
lat1 *pi/180 ) * cos( lat2 * pi/180 ) * cos( (lon2 * pi/180) -
(lon1 *pi/180)
);
SET x = atan( ( sqrt( 1- power( x, 2 ) ) ) / x );
RETURN ( 1.852 * 60.0 * ((x/pi)*180) ) / 1.609344;
END
答案 0 :(得分:2)
据我所知,你的逻辑没有直接的错误会导致这种情况变慢,所以问题最终导致你不能在这个查询中使用任何索引。
MySQL需要进行全表扫描,并将WHERE子句的功能应用于每一行,以确定它是否通过了条件。目前使用了1个索引:idx_lat_long
。
这是一个糟糕的索引,永远不会使用long
部分,因为lat
部分是浮点数。但至少你设法有效地过滤掉latitude
范围之外的所有行。但它很可能......但这些仍然很多。
你实际上在经度上会得到稍微好一些的结果,因为人类实际上只生活在地球的30%中间。我们非常横向展开,但不是垂直展开。
无论如何,进一步最小化该字段的最佳方法是尝试过滤掉一般区域中的尽可能多的记录。现在它是地球上一个完整的垂直条带,试着把它变成一个边界框。
你可以天真地将地球切成10x10段。在最好的情况下,这将确保查询限制在地球的10%;)。
但是一旦你的边界框超过了单独的段,只有第一个坐标(lat或lng)可以在索引中使用,你最终会遇到同样的问题。
所以当我想到这个问题时,我开始以不同的方式思考这个问题。相反,我把地球划分为4个部分(比如说,东北,西北,东南,西南在地图上)。所以这给了我这样的坐标:
我没有将x和y值放在2个单独的字段中,而是将其用作位字段并一次存储。
然后我再分开的4个盒子中的每一个,这给了我们2组坐标。外部和内部坐标。我仍然在同一个字段中编码,这意味着我们现在使用4位用于8x8坐标系。
我们能走多远?如果我们假设一个64位整数字段,则意味着32位可以用于2个坐标中的每一个。这为我们提供了一个4294967295 x 4294967295的网格系统,所有这些都编码到一个数据库字段中。
这个领域的美妙之处在于你可以为它编制索引。这有时被称为(我相信)一棵四叉树。如果您需要在数据库中选择一个大区域,您只需计算64位左上角坐标(在4294967295 x 4294967295网格系统中)和左下角,并保证该框中的任何内容也将是在这两个数字中。
你如何得到这些数字。让我们懒惰并假设我们的x和y坐标的范围从-180到180度。 (当然,y坐标是一半,但我们很懒。)
首先,我们将其定为正面:
// assuming x and y are our long and lat.
var x+=180;
var y+=180;
所以那些最大值现在是360,(4294967295/360大约是11930464)。
因此,要转换为我们的新网格系统,我们只需:
var x*=11930464;
var y*=11930464;
现在我们必须使用不同的数字,我们需要将它们变成1个数字。 x的第一位1,y的第1位,x的第2位,y的第2位等等。
// The 'morton number'
morton = 0
// The current bit we're interleaving
bit = 1
// The position of the bit we're interleaving
position = 0
while(bit <= latitude or bit <= longitude) {
if (bit & latitude) morton = morton | 1 << (2*position+1)
if (bit & longitude) morton = morton | 1 << (2*position)
position += 1
bit = 1 << position
}
我正在调用最终变量'morton',这是1966年提出它的人。
所以这最终给我们留下了以下内容:
这将大大减少您需要检查的记录数量。
这是我编写的存储过程,它将为您进行计算:
CREATE FUNCTION getGeoMorton(lat DOUBLE, lng DOUBLE) RETURNS BIGINT UNSIGNED DETERMINISTIC
BEGIN
-- 11930464 is round(maximum value of a 32bit integer / 360 degrees)
DECLARE bit, morton, pos BIGINT UNSIGNED DEFAULT 0;
SET @lat = CAST((lat + 90) * 11930464 AS UNSIGNED);
SET @lng = CAST((lng + 180) * 11930464 AS UNSIGNED);
SET bit = 1;
WHILE bit <= @lat || bit <= @lng DO
IF(bit & @lat) THEN SET morton = morton | ( 1 << (2 * pos + 1)); END IF;
IF(bit & @lng) THEN SET morton = morton | ( 1 << (2 * pos)); END IF;
SET pos = pos + 1;
SET bit = 1 << pos;
END WHILE;
RETURN morton;
END;
一些警告:
所有这一切的来源:当我遇到同样的问题并试图创造性地找到解决方案时,我写了3篇关于这个主题的博文。与MySQL的GEO索引相比,我的性能要好得多。