如何优化此存储过程?

时间:2013-03-26 01:22:00

标签: mysql optimization stored-procedures

我需要一些帮助来优化此过程:

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

1 个答案:

答案 0 :(得分:2)

据我所知,你的逻辑没有直接的错误会导致这种情况变慢,所以问题最终导致你不能在这个查询中使用任何索引。

MySQL需要进行全表扫描,并将WHERE子句的功能应用于每一行,以确定它是否通过了条件。目前使用了1个索引:idx_lat_long

这是一个糟糕的索引,永远不会使用long部分,因为lat部分是浮点数。但至少你设法有效地过滤掉latitude范围之外的所有行。但它很可能......但这些仍然很多。

你实际上在经度上会得到稍微好一些的结果,因为人类实际上只生活在地球的30%中间。我们非常横向展开,但不是垂直展开。

无论如何,进一步最小化该字段的最佳方法是尝试过滤掉一般区域中的尽可能多的记录。现在它是地球上一个完整的垂直条带,试着把它变成一个边界框。

你可以天真地将地球切成10x10段。在最好的情况下,这将确保查询限制在地球的10%;)。

但是一旦你的边界框超过了单独的段,只有第一个坐标(lat或lng)可以在索引中使用,你最终会遇到同样的问题。

所以当我想到这个问题时,我开始以不同的方式思考这个问题。相反,我把地球划分为4个部分(比如说,东北,西北,东南,西南在地图上)。所以这给了我这样的坐标:

  • 0,0
  • 0,1
  • 1,0
  • 1,1

我没有将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年提出它的人。

所以这最终给我们留下了以下内容:

  1. 对于数据库中的每一行,计算morton数并存储它。
  2. 每当您进行查询时,首先确定最大边界框(作为morton数)并对其进行过滤。
  3. 这将大大减少您需要检查的记录数量。

    这是我编写的存储过程,它将为您进行计算:

    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;
    

    一些警告:

    1. 绝对最糟糕的情况仍将扫描整个表格的50%。这个机会非常低,而且我发现大多数现实世界的查询都有绝对显着的性能提升。
    2. 在这种情况下,边界框采用Eucllidean space,表示平坦的表面。实际上,你的边界框不是精确的正方形,当靠近极点时它们会发生剧烈的扭曲。只需将盒子放大一些(取决于你想要的精确程度),你就可以走得更远。大多数现实世界的数据通常也不接近极点;)。请记住,此过滤器只是一个“粗略过滤器”,可以排除大部分可能不需要的行。
    3. 这是基于所谓的Z-Order curve。为了获得更好的表现,如果你有冒险精神......你可以尝试去Hilbert Curve instead。这条曲线奇怪地旋转,这确保在最坏的情况下,你只扫描约25%的表..魔术!一般来说,这个也会过滤掉更多不需要的行。
    4. 所有这一切的来源:当我遇到同样的问题并试图创造性地找到解决方案时,我写了3篇关于这个主题的博文。与MySQL的GEO索引相比,我的性能要好得多。