任何在这里对所有行进行操作的操作都将因有那么多记录而变慢。
你需要做的是利用索引。要使用索引,它必须是一个简单查询,而不是
函数的结果(目前就是这样)。
通过进行半径搜索,您正在围绕一个点画圆。在制作圆之前,通过使用一些三角函数,我们可以得出以下结果。
![Circle two squares](https://istack.dev59.com/B2i4W.gif)
S1代表内部最大的正方形,S2代表外部最小的正方形。
现在我们可以计算出这两个正方形的尺寸,S2外面的任何东西都会被索引命中,而S1里面的任何东西都会被索引命中,只留下需要使用慢速方法查找的小区域。
如果您需要从该点开始的距离,请忽略S1部分(因为圆内的所有内容都需要haversine函数),请注意,虽然圆内的所有内容都需要它,但不是每个点都在距离范围内,因此仍然需要两个WHERE子句。
因此,让我们使用单位圆来计算这些点 ![Unit Circle](https://istack.dev59.com/fMdam.webp)
function getS1S2($latitude, $longitude, $kilometer)
{
$radiusOfEarthKM = 6371;
$latitudeRadians = deg2rad($latitude);
$longitudeRadians = deg2rad($longitude);
$distance = $kilometer / $radiusOfEarthKM;
$deltaLongitude = asin(sin($distance) / cos($latitudeRadians));
$bounds = new \stdClass();
$bounds->minLat = rad2deg($latitudeRadians - $distance);
$bounds->maxLat = rad2deg($latitudeRadians + $distance);
$bounds->minLong = rad2deg($longitudeRadians - $deltaLongitude);
$bounds->maxLong = rad2deg($longitudeRadians + $deltaLongitude);
$bounds->innerMinLat = rad2deg($latitudeRadians + $distance * cos(5 * M_PI_4));
$bounds->innerMaxLat = rad2deg($latitudeRadians + $distance * sin(M_PI_4));
$bounds->innerMinLong = rad2deg($longitudeRadians + $deltaLongitude * sin(5 * M_PI_4));
$bounds->innerMaxLong = rad2deg($longitudeRadians + $deltaLongitude * cos(M_PI_4));
return $bounds;
}
现在您的查询变成了
SELECT
*
FROM
`places`
HAVING p.nlatitude BETWEEN {$bounds->minLat}
AND {$bounds->maxLat}
AND p.nlongitude BETWEEN {$bounds->minLong}
AND {$bounds->maxLong}
AND (
(
p.nlatitude BETWEEN {$bounds->innerMinLat}
AND {$bounds->innerMaxLat}
AND p.nlongitude BETWEEN {$bounds->innerMinLong}
AND {$bounds->innerMaxLong}
)
OR (
6371 * ACOS(
COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
RADIANS(`longitude`) - RADIANS({ $lon })
) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
)
)
)) <= {$radius}
ORDER BY distance ASC
重要提示
上述内容是为了易读性而添加的文本,请确保这些值被正确地转义/最好是参数化
然后就可以利用索引,使连接更快地完成
添加连接后变为:
SELECT
*
FROM
`places` p
INNER JOIN my_friends f ON f.id = p.id
WHERE p.latitude BETWEEN {$bounds->minLat}
AND {$bounds->maxLat}
AND p.longitude BETWEEN {$bounds->minLong}
AND {$bounds->maxLong}
AND (
(
p.latitude BETWEEN {$bounds->innerMinLat}
AND {$bounds->innerMaxLat}
AND p.longitude BETWEEN {$bounds->innerMinLong}
AND {$bounds->innerMaxLong}
)
OR (
6371 * ACOS(
COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
RADIANS(`longitude`) - RADIANS({ $lon })
) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
)
)
) <= {$radius}
AND f.personal_id = {$personal_id}
ORDER BY distance ASC
重要提示
以上内容为了易读性而提供,但请确保这些值被正确转义/最好是参数化。
假设您有正确的索引,此查询应保持快速并允许您进行联接。
查看上面的代码,我不确定personal_id
来自哪里,因此将其保留为原样。
如果您需要从查询中获取距离,则可以删除S1平方项。
(
p.latitude BETWEEN {$bounds->innerMinLat}
AND {$bounds->innerMaxLat}
AND p.longitude BETWEEN {$bounds->innerMinLong}
AND {$bounds->innerMaxLong}
)
并移动那个 OR
的第二部分
6371 * ACOS(
COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
RADIANS(`longitude`) - RADIANS({ $lon })
) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
)
回到仍然使用S2的选择器。
我还会确保在查询中删除“魔术数字”,6371是地球半径(单位:千米)。
mysqli
时,您应该使用参数化查询和bind_param
将用户数据添加到查询中。不要使用字符串插值或串联来实现此操作,因为这会导致严重的SQL注入漏洞。永远不要直接将$_POST
或$_GET
数据放入查询中,如果有人试图利用您的错误,这可能会非常危险。 - tadmanmysqli
),因此在这种情况下直接指出错误是过头了。但当然,你是正确的,关于字符串拼接和插值现在应该被避免,因为mysqli
是标准。 - Walter Tross