Skip to content

Commit c5fc677

Browse files
committed
fix(postgres): let distanceLessThan use the spatial index
ST_Distance(a, b) < d cannot be served by a GIST index, so radius queries filtered every row while walking the primary key. Without meters, distanceLessThan now leads with ST_DWithin on the geometry column. With meters on a point column and a point value, it leads with a degree box (&& ST_Expand) that holds every point within range on the WGS84 spheroid. Lines and polygons, and boxes that would reach a pole or the antimeridian, keep the exact geography comparison only. The exact ST_Distance check stays in every case, so the boundary remains exclusive.
1 parent 1c99c21 commit c5fc677

1 file changed

Lines changed: 55 additions & 2 deletions

File tree

‎src/Database/Adapter/Postgres.php‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,14 +1486,67 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
14861486
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
14871487
}
14881488

1489+
$within = $query->getMethod() === Query::TYPE_DISTANCE_LESS_THAN;
1490+
14891491
if ($meters) {
14901492
$attr = "({$alias}.{$attribute}::geography)";
14911493
$geom = "ST_SetSRID(" . $this->getSpatialGeomFromText(":{$placeholder}_0", null) . ", " . Database::DEFAULT_SRID . ")::geography";
1492-
return "ST_Distance({$attr}, {$geom}) {$operator} :{$placeholder}_1";
1494+
$distance = "ST_Distance({$attr}, {$geom}) {$operator} :{$placeholder}_1";
1495+
1496+
// The GIST index is on geometry, so only a degree box around the point can narrow a geography distance
1497+
$degrees = $within && $query->getAttributeType() === Database::VAR_POINT
1498+
? $this->getDegreesWithinMeters($distanceParams[0], (float) $distanceParams[1])
1499+
: null;
1500+
1501+
if ($degrees === null) {
1502+
return $distance;
1503+
}
1504+
1505+
$binds[":{$placeholder}_2"] = $degrees[0];
1506+
$binds[":{$placeholder}_3"] = $degrees[1];
1507+
1508+
return "{$alias}.{$attribute} && ST_Expand(" . $this->getSpatialGeomFromText(":{$placeholder}_0") . ", :{$placeholder}_2, :{$placeholder}_3) AND {$distance}";
14931509
}
14941510

14951511
// Without meters, use the original SRID (e.g., 4326)
1496-
return "ST_Distance({$alias}.{$attribute}, " . $this->getSpatialGeomFromText(":{$placeholder}_0") . ") {$operator} :{$placeholder}_1";
1512+
$distance = "ST_Distance({$alias}.{$attribute}, " . $this->getSpatialGeomFromText(":{$placeholder}_0") . ") {$operator} :{$placeholder}_1";
1513+
1514+
// ST_DWithin can use the GIST index; ST_Distance keeps the boundary exclusive
1515+
if ($within) {
1516+
return "ST_DWithin({$alias}.{$attribute}, " . $this->getSpatialGeomFromText(":{$placeholder}_0") . ", :{$placeholder}_1) AND {$distance}";
1517+
}
1518+
1519+
return $distance;
1520+
}
1521+
1522+
/**
1523+
* Longitude and latitude degrees that hold every point within $meters of $point on the WGS84 spheroid.
1524+
*
1525+
* Null for lines and polygons, whose geodesic edges leave any degree box, and when the box would reach a pole or the antimeridian.
1526+
*
1527+
* @return array{0: float, 1: float}|null
1528+
*/
1529+
protected function getDegreesWithinMeters(mixed $point, float $meters): ?array
1530+
{
1531+
if (!\is_array($point) || \count($point) !== 2 || !\is_numeric($point[0] ?? null) || !\is_numeric($point[1] ?? null)) {
1532+
return null;
1533+
}
1534+
1535+
$longitude = (float) $point[0];
1536+
$latitude = (float) $point[1];
1537+
1538+
// A degree of latitude spans at least 110,574 m, and a degree of longitude at least 111,319 m × cos(latitude)
1539+
$latitudeDegrees = $meters / 110574;
1540+
if (\abs($latitude) + $latitudeDegrees >= 90) {
1541+
return null;
1542+
}
1543+
1544+
$longitudeDegrees = $meters / (111319 * \cos(\deg2rad(\abs($latitude) + $latitudeDegrees)));
1545+
if ($longitude - $longitudeDegrees <= -180 || $longitude + $longitudeDegrees >= 180) {
1546+
return null;
1547+
}
1548+
1549+
return [$longitudeDegrees, $latitudeDegrees];
14971550
}
14981551

14991552

0 commit comments

Comments
 (0)