The SQL Server is pretty fast finding neighbors of a geography in a spatial index. Either it is finding nearest neighbors or all neighbors in specific area, the query result is instant.
I am not able to get the result so fast, if I need to find neighbors for multiple points.
I guess the business case is widely recognized. Let's say we have one table with coordinates of locations (it can be stores, restaurants, offices, etc.) and one table with coordinates of the users and their travel distance.
In the context of the user, if I am in the center of the city and want to travel 5 miles - getting all locations in my range is fast. But in the context of a business, if I want to send email to all users who are in range of my stores, the query is pretty slow.
Here is some sample date with several queries trying to find all users - locations pairs.
DROP TABLE IF EXISTS #Users;
DROP TABLE IF EXISTS #Locations;
CREATE TABLE #Users
(
[UserID] BIGINT pRIMARY KEY
,[Latitude] DECIMAL(9, 6)
,[Longitude] DECIMAL(9, 6)
-- this is our user point and user's travel distances in meters
,[CenterGeography] GEOGRAPHY
,[TravelDistance] INT
-- this is the [CenterGeography] * [TravelDistance]
,[AreaGeography] GEOGRAPHY
);
CREATE SPATIAL INDEX [IX_#Users_CenterGeography] ON #Users
(
[CenterGeography]
);
CREATE SPATIAL INDEX [IX_#Users_AreaGeography] ON #Users
(
[AreaGeography]
);
CREATE TABLE #Locations
(
[LocationID] BIGINT pRIMARY KEY
,[CenterGeography] GEOGRAPHY
);
CREATE SPATIAL INDEX [IX_#Locations_AreaGeography] ON #Locations
(
[CenterGeography]
);
INSERT INTO #Users ([UserID], [Latitude], [Longitude], [TravelDistance])
SELECT CenterLocations.[CenterID] * 10000 + U.[value]
,CenterLocations.[Lat] + ABS(CHECKSUM(NEWID())) % 1000 * 0.0001
,CenterLocations.[Long] + ABS(CHECKSUM(NEWID())) % 1000 * 0.0001
,1 + ABS(CHECKSUM(NEWID())) % 5 * 1609
FROM GENERATE_SERIES(1, 1000) U
CROSS APPLY
(
VALUES (1, 13.404954, 52.520008)
,(2, 13.737262, 51.050407)
,(3, 6.783333, 51.233334)
,(4, 9.183333, 48.783333)
,(5, 6.953101, 50.935173)
) CenterLocations ([CenterID], [Lat], [Long]);
UPDATE #Users
SET [CenterGeography] = GEOGRAPHY::Point([Latitude], [Longitude], 4326)
,[AreaGeography] = GEOGRAPHY::Point([Latitude], [Longitude], 4326).STBuffer([TravelDistance]);
INSERT INTO #Locations ([LocationID], [CenterGeography])
SELECT CenterLocations.[CenterID] * 10000 + L.[value]
,GEOGRAPHY::Point(CenterLocations.[Lat] + ABS(CHECKSUM(NEWID())) % 1000 * 0.0001, CenterLocations.[Long] + ABS(CHECKSUM(NEWID())) % 1000 * 0.0001, 4326)
FROM GENERATE_SERIES(1, 100) L
CROSS APPLY
(
VALUES (1, 13.404954, 52.520008)
,(2, 13.737262, 51.050407)
,(3, 6.783333, 51.233334)
,(4, 9.183333, 48.783333)
,(5, 6.953101, 50.935173)
) CenterLocations ([CenterID], [Lat], [Long]);
RETURN;
-- varint 00 - get locations for one user (the spatial index is used without the hint)
SELECT U.[UserID]
,L.[LocationID]
FROM #Users U
INNER JOIN #Locations L WITH (INDEX = [IX_#Locations_AreaGeography])
ON U.[CenterGeography].STDistance(L.[CenterGeography] ) <= U.[TravelDistance]
WHERE U.[UserID] = 10001;
-- variant 01 - intersect -- spatial index not used -- 83 seconds
DROP TABLE IF EXISTS #TEST;
SELECT U.[UserID]
,L.[LocationID]
INTO #TEST
FROM #Users U
INNER JOIN #Locations L
ON U.[AreaGeography].STIntersects(L.[CenterGeography]) = 1;
-- variant 02 - STDistance -- spatila index is used -- 62 seconds
DROP TABLE IF EXISTS #TEST;
SELECT U.[UserID]
,L.[LocationID]
INTO #TEST
FROM #Users U
INNER JOIN #Locations L WITH (INDEX = [IX_#Locations_AreaGeography])
ON U.[CenterGeography].STDistance(L.[CenterGeography] ) <= U.[TravelDistance];
-- variant 03 - STDistance -- only works if the <= argument is static but it is executed for 13 seconds
DROP TABLE IF EXISTS #TEST;
SELECT U.[UserID]
,L.[LocationID]
INTO #TEST
FROM #Users U WITH (INDEX = [IX_#Users_CenterGeography])
INNER JOIN #Locations L
ON U.[CenterGeography].STDistance(L.[CenterGeography] ) <= 6437 --U.[TravelDistance]
OPTION (RECOMPILE);
The example uses GENERATE_SERIES, so it requires SQL Server 2022. I can create any type of index or materialized any kind of geography object, so one can change the tables definitions if you want. For now, it seems the fastest technique is variant 03
but it requires hard coded travel distance to work, which basically means I am not returning the correct data.
I want to know is there a way to optimize such queries using spatial data in SQL Server or I need to find alternative to get better performance (for example to pre-calalculate with triggers the locations in range per each user).
ST_Dwithin
but I don't know if sql-server has that operator - if you really want speed then it might be worth switching to postgres with postgis extension.