0.问题背景
项目中需要计算两个点的距离,已记录对应的经纬度
计算公式如下:
1
| SELECT ROUND((6371393.0*ACOS(SIN(点1纬度/180*PI())*SIN(点2纬度/180*PI())+COS(点1纬度/180*PI())*COS(点2纬度/180*PI())*COS((点1经度-点2经度)/180*PI())))::NUMERIC, 3);
|
1.问题复现
点1位置: 经度122.132181
、纬度40.272723
,点2位置与点1相同,正常结果距离应该是0,实际执行结果如下
1
| SELECT ROUND((6371393.0*ACOS(SIN(40.272723/180*PI())*SIN(40.272723/180*PI())+COS(40.272723/180*PI())*COS(40.272723/180*PI())*COS((122.132181-122.132181)/180*PI())))::NUMERIC, 3);
|
报错:
1
| ERROR: input is out of range
|
2.问题定位
反余弦ACOS
函数的参数范围必须在[-1,1]范围内,当参数超出范围时就会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| dev=# SELECT ACOS(1); acos
0 (1 row)
dev=# SELECT SIN(40.272723/180*PI())*SIN(40.272723/180*PI())+COS(40.272723/180*PI())*COS(40.272723/180*PI())*COS((122.132181-122.132181)/180*PI()); ?column?
1 (1 row)
dev=# SELECT SIN(40.272723/180*PI())*SIN(40.272723/180*PI())+COS(40.272723/180*PI())*COS(40.272723/180*PI())*COS((122.132181-122.132181)/180*PI()) > 1; ?column?
t (1 row)
|
3.原因描述
二进制无法精确表示浮点数计算后的值,当计算后的值理论上为1时,在计算机中存储的可能是1.0000000000000001
或者0.999999999999999
在PG
的低版本中(上面测试用的版本是10.20)无法直观的看出精度,只能通过比较的方式得到结论
在PG
较高的版本中(如12版本)可以直观的看出结果, 存储的值是1.0000000000000002
1 2 3 4 5
| dev=# SELECT SIN(40.272723/180*PI())*SIN(40.272723/180*PI())+COS(40.272723/180*PI())*COS(40.272723/180*PI())*COS((122.132181-122.132181)/180*PI()); ?column?
1.0000000000000002 (1 row)
|
4.解决方案
PG有一对函数可计算最大值和最小值: GREATEST
取最大值,LEAST
取最小值
1 2 3 4 5
| dev=# SELECT GREATEST(1,2,3,4), LEAST(1,2,3,4); greatest | least
4 | 1 (1 row)
|
GREATES
和 LEAST
函数搭配使用可以限制ACOS
函数的参数只能在[-1,1]之间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| dev=# SELECT GREATEST(LEAST(0.99, 1), -1); greatest
0.99 (1 row)
dev=# SELECT GREATEST(LEAST(1.01, 1), -1); greatest
1 (1 row)
dev=# SELECT ACOS(GREATEST(LEAST(SIN(40.272723/180*PI())*SIN(40.272723/180*PI())+COS(40.272723/180*PI())*COS(40.272723/180*PI())*COS((122.132181-122.132181)/180*PI()), 1), -1)); acos
0 (1 row)
|
5.更健壮的根据经纬度计算距离的SQL语句
1
| SELECT ROUND((6371393.0*ACOS(GREATEST(LEAST(SIN(点1纬度/180*PI())*SIN(点2纬度/180*PI())+COS(点1纬度/180*PI())*COS(点2纬度/180*PI())*COS((点1经度-点2经度)/180*PI()), 1), -1)))::NUMERIC, 3);
|