早在《高性能MySQL》上看到了,一直想记录下来,我有严重拖延症。。。最近公司在这个类似业务方面需要优化,所以赶紧再仔细看一下,记录下来,加深一下印象。
此文章参考来源《高性能MySQL》6.8.2节,有兴趣可以去看一下。
解决的问题
很多业务有查找某个点附近的人,附近的商户等等。
准备
创建保存用户位置的表
CREATE TABLE locations (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
lat FLOAT NOT NULL CONTENT '纬度',
lon FLOAT NOT NULL CONTENT '经度'
);
最初方案
我们假设地球是圆的,然后使用两点所在最大元圆公式来计算两点之间的距离。
球面距离公式是计算球面上两点间距离的公式。设所求点A ,纬度β1 ,经度α1 ;点B ,纬度β2 ,经度α2。
则距离S=R·arccos[cosβ1cosβ2cos(α1-α2)+sinβ1sinβ2],其中R为球体半径。
以上公式R是地球的半径,如果去掉R则是两点之间的弧度。如果直接按照这种方法计算两点之间的距离,则计算公式如下:
---RADIANS(X):返回X从度转换成弧度的值
SELECT * FROM locations
WHERE 3979 * ACOS(
COS(RADIANS(lat))*COS(RADIANS(38.03))*COS(RADIANS(lon)-RADIANS(-78.48))
+ SIN(RADIANS(lat))*SIN(38.03))<=100;
缺点:这类查询不仅无法使用索引,而且还会非常消耗CPU时间,给数据库带来很大的压力。
思考:也许我们不需要这么精确的计算,其实本身地址已经很不精确,再加上没有正在直接的距离。地球确切的说也不是圆的。
改进方案
如果我们不用圆周,而是用正方形,如图(随便看看,下图是圆周,把那个地方改成正方形):
根据正方形公式来计算,公式为
l(弧度)=α(圆心角弧度数)*r(半径)
,地球半径为6371km=3959英里,所以弧度为100/3959=0.0253(100英里)的中心到边长的距离:
-----DEGREES(X):返回X从弧度转换为度值----
SWLECT * FROM locations
WHERE lat BETWEEN 38.03-DEGREES(0.0253) AND 38.03 +DEGREES(0.0253)
AND
lon BETEEEN -78.48-DEGREES(0.0253) AND -78.48 +DEGREES(0.0253)
然而这样就不能使用索引了(这里不考虑建两个索引的情况),因为两个查询都是都是范围的。但是我们可以使用IN()优化,我们先新增两个列,用来存储坐标的近似值FLOOR(),然后在查询中使用IN()讲所有点的整数值都放到列表中。
索引优化
下面是我们需要新增的列和索引:
ALTER TABLE locations ADD lat_floor INT NOT NULL DEFAULT 0,
ADD lon_floor INT NOT NULL DEFAULT 0,
ADD KEY(lat_floor,lon_floor);
------更新这两列的值
UPDATE locations SET lat_floor= FLOOR(lat), lon_floor = FLOOR(lon);
现在可以根据近似值来计算了,以下的计算可以在程序中进行,限定了经纬度的范围
-----CEILING(X):返回的最小整数值不小于X;
-----FLOOR(X):返回的最大整数但不大于X的值;
SELECT FLOOR(38.03-DEGREES(0.0253)) AS lat_lb,
CEILING(38.03+DEGREES(0.0253)) AS lat_ub,
FLOOR(-78.48-DEGREES(0.0253)) AS lon_lb,
CEILING(-78.48+DEGREES(0.0253)) AS lon_ub;
------结果为36 40 -80 -77
加上索引的sql
SWLECT * FROM locations
WHERE lat BETWEEN 38.03-DEGREES(0.0253) AND 38.03 +DEGREES(0.0253)
AND
lon BETEEEN -78.48-DEGREES(0.0253) AND -78.48 +DEGREES(0.0253)
AND lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77);
这样就可以使用索引了,并且速度也很快了。如果希望精度再高些,则可以使用第一个sql再加上IN查询就可以了。