SQL Server 2012 Spatial Index Nearest Neighbor query - sql-server-2012

I have created a table containing US zipcodes along with the latitude and longitude of these zip codes. From the latitude and longitude I created a geography column. I created a spatial index on that geography column. I want to return the closest 100 zip codes to a latitude and longitude so I created the following stored procedure
CREATE PROCEDURE [dbo].[Geo_Locate]
#Latitude DECIMAL,
#Longitude DECIMAL
AS
BEGIN
SET NOCOUNT ON;
DECLARE #GeogTemp NVARCHAR(200)
DECLARE #Geog GEOGRAPHY
-- Create a geography type variable from the passed in latitude and longitude. This will be used to find the closest point in the database
SET #GeogTemp = 'POINT(' + convert(NVARCHAR(100), #Longitude) + ' ' + convert(NVARCHAR(100), #Latitude) + ')'
SET #Geog = geography::STGeomFromText(#GeogTemp, 4326)
-- Run the main query
SELECT TOP 100
[Id],
[Country],
[ZipPostalCode],
[City],
[County],
[Latitude],
[Longitude],
[GeographyCol],
GeographyCol.STDistance(#Geog) AS Distance
FROM
[dbo].[ZipCode] WITH(INDEX(ZipCode_SpatialIndex))
WHERE
GeographyCol.STDistance(#Geog) < 100000 -- 100 KM
ORDER BY
GeographyCol.STDistance(#Geog) ASC
END
However when I pass the latitude = 35.48330 and longitude = -97.17340 to this stored procedure I get the following returned at item 55
869531 US 73045 Harrah Oklahoma OK 35.48330 -97.17340 0xE6100000010C12143FC6DCBD4140174850FC184B58C0 55894.2236191955
The last column is the distance. Basically the query is saying that this record is nearly 56KM from the entered point but the latitude and longitude are the same.
I've read MSDN and my query looks correct. Can anyone help me please?

This one was a doozy and I'm not sure I got it. I started with trying to replicate in purest terms what you were seeing: incorrect STDistance() between two geography points.
DECLARE #zipcode geography
DECLARE #searchpoint geography
SET #zipcode = geography::STPointFromText('POINT(-97.17340 35.48330)', 4326);
SET #searchpoint = geography::STPointFromText('POINT(-97.17340 35.48330)', 4326);
SELECT #zipcode.STDistance(#searchpoint)
The results is, correctly, 0 meters (SRID 4326 uses meters as the distance unit), so I kept scratching my head. The only thing I noticed that I can't replicate is this: you flipped between geography and geometry (your code from question):
SET #Geog = geography::STGeomFromText(#GeogTemp, 4326)
When I plopped that into my code I still got 0 for distance. So my only question, and perhaps you could post the T-SQL, is about your spatial index. I think the STDistance() calculation is going to use the index. So without seeing the index and knowing that you hopped between Geography and Geometry at least in this instance, I'd wonder if the odd results are in there since I can't replicate it with my above code.

Related

Find distance between two points with longitude and latitude

I got an answer to this question from this website however the answer I get is wrong.
DECLARE #orig_lat DECIMAL
DECLARE #orig_lng DECIMAL
SET #orig_lat=52.676 set #orig_lng=-1.6193
DECLARE #orig geography = geography::Point(#orig_lat, #orig_lng, 4326);
SELECT #orig.STDistance(geography::Point(Latitude, longitude, 4326)) AS distance
From ...
However I get the wrong answer
e.g. distance 234229 latitude 55.0853 and longitude -1.595
I have to admit I just copied the code and Don't understand it. The answer should be 166 miles which is 267 km.
Any ideas?
Short answer:
Make #orig_lat and #orig_lng of type decimal(19,6) (for example) or float.
Long answer:
The issue is because your decimals have no actual decimals (you are using the default precision, which is decimal(18,0)), so they end up being 53 and -2. You should define their precision (e.g. decimal(19,6)) or just use float, which is the type the function expects anyway. If you simply change this, it works fine:
DECLARE #orig_lat float
DECLARE #orig_lng float
SET #orig_lat=52.676 set #orig_lng=-1.6193
DECLARE #orig geography = geography::Point(#orig_lat, #orig_lng, 4326);
DECLARE #dest geography = geography::Point(55.0853, -1.595, 4326);
select #orig.STDistance(#dest)
This returns 268166.415685712.
I discovered this by simply printing the varchar equivalent of the geography:
DECLARE #orig_lat decimal
DECLARE #orig_lng decimal
SET #orig_lat=52.676 set #orig_lng=-1.6193
DECLARE #orig geography = geography::Point(#orig_lat, #orig_lng, 4326);
select cast(#orig as varchar)
That will print POINT (-2 53), which also gives you another piece of information besides the rounding: that varchar format uses longitude-latitude instead of latitude-longitude. So if you wanted to create those points the other way, you should use:
DECLARE #orig geography = 'POINT(-1.6193 52.676)'
DECLARE #dest geography = 'POINT(-1.595 55.0853)'
SELECT #orig.STDistance(#dest)
The answer is in meters, divide by 1000 and you have km.
I try to break it down to you:
-- I assume you already know that this part is only declaring variables
-- and setting them.
-- In this case they are the variables for your starting coordinates.
DECLARE #orig_lat DECIMAL
DECLARE #orig_lng DECIMAL
SET #orig_lat=52.676 set #orig_lng=-1.6193
-- MS SQL has built in geography functions
-- like a function to determine the distance between 2 points.
-- But it uses an own unit instead of longitude/latitude: point.
-- So you have to use the geography::Point-function to convert
-- a lat/lon coordinate into a point value:
DECLARE #orig geography = geography::Point(#orig_lat, #orig_lng, 4326);
-- The rest is basically the same again with the target coordinates,
-- namely this part: geography::Point(Latitude, longitude, 4326)
-- embedded in the STDistance function. This function calculates the distance
-- between 2 point values.
SELECT #orig.STDistance(geography::Point(Latitude, Longitude, 4326)) AS distance
From ...
-- where Latitude and Longitude would be your destination coordinates.
If you want you can write your own statement, the mathematical background of this is the Haversine formula basically measuring the distance between 2 points on a sphere.
A more simplified way is this:
DECLARE #source geography = 'POINT(52.676 -1.6193)'
DECLARE #target geography = 'POINT(55.0853 -1.595)'
SELECT #source.STDistance(#target)

SQL points around line between A and B

In SQL Server 2014, I have a database with Geometry points - City
Driving from City A to City B gives me a line (we take an airplane).
I need to find points in my database - which are in certain distance (10 miles) "off-track" of this line.
I know how to find the closest points around a single point, how to calculate the distance between them - but - how can I search along this line? Like POI in your Navi...
DECLARE #g geography
SELECT #g = Geo_LatLong_deg
FROM airports
WHERE iata_code = 'MyAirportCode' -- radius 100km
SELECT *
FROM airports
WHERE #g.STDistance(Geo_LatLong_deg) <= 100000
Use the STBuffer method. Assuming that you've got some way to determine your path as a geography instance, it's as simple as:
declare #distance float = 16.09344 --10 miles in km
select *
from airports
where #path.STBuffer(#distance).STIntersects(Geo_LatLong_deg) = 1
By way of explanation, the STBuffer() method creates a region that is the set of points within 10 miles of your path. Then, we select all points from your table that intersect with that region with STIntersects().
Thank you for your help. I mixed up Long/Lat sequence in string... now I get the results as expected.
here the code - if others want to see how to combine two or more points - together with the area around the line(s).
DECLARE #BuildString NVARCHAR(MAX)
SELECT #BuildString = COALESCE(#BuildString + ',', '') + CAST(longitude_deg AS NVARCHAR(50)) + ' ' + CAST(latitude_deg AS NVARCHAR(50))
FROM dbo.airports where iata_code='RLG' or iata_code='FRA'
ORDER BY ID
SET #BuildString = 'LINESTRING(' + #BuildString + ')';
DECLARE #LineFromPoints geography = geography::STLineFromText(#BuildString, 4326);
declare #distance float = 50000
select *
from airports
where #LineFromPoints.STBuffer(#distance).STIntersects(airports.GEO_LatLong_deg) = 1 and type<>'heliport'

Alternative way to get Max, Min for Lat and Long from Geography field?

I have geography field of irregular shapes. Geography field can vary from hundred to thousands of Lat/Long points that define that shape. In regards to size it could be from several US. Postal Codes to a size of entire US State. In order to have increased performance I have build Spacial index on that field. On frequent basis I have to find vehicles based on Lat/Long point that are within specific zone.
My original approach was this.
WITH LastP
AS ( SELECT vlp.ID
,GEOGRAPHY::STPointFromText('POINT(' + CAST(vlp.Long AS VARCHAR(20)) + ' '
+ CAST(vlp.Lat AS VARCHAR(20)) + ')', 4326) AS LastKnownPoint
FROM LastPosition AS vlp )
SELECT lp.ID
,zn.ZONE
FROM dbo.GeogZone AS zn WITH ( NOLOCK )
JOIN #zones AS z
ON zn.Zone = z.Zone
JOIN LastP AS lp
ON lp.LastKnownPoint.STWithin(zn.ZoneGeog) = 1
I was getting all records from my table LastPosition and than I converted Lat/Long into Geography point and later JOIN using STWithin function. This process works great but can be very slow. I have tried to adjust Spacial indexes but it did not make big changed.
To increase performance I want to introduce the following process.
From Geography type I will extract NorthLat, SouthLat, EastLong, WestLong
Now I can limit the number of results before I do compare in the following matter.
WITH LastP
AS ( SELECT vlp.ID
,GEOGRAPHY::STPointFromText('POINT(' + CAST(vlp.Long AS VARCHAR(20)) + ' '
+ CAST(vlp.Lat AS VARCHAR(20)) + ')', 4326) AS LastKnownPoint
FROM LastPosition AS vlp
WHERE (vlp.Long BETWEEN #WestLong and #EastLong) AND (vlp.Lat BETWEEN #SouthLat AND #NorthLat))
SELECT lp.ID
,zn.ZONE
FROM dbo.GeogZone AS zn
JOIN #zones AS z
ON zn.Zone = z.Zone
JOIN LastP AS lp
ON lp.LastKnownPoint.STWithin(zn.ZoneGeog) = 1
Here is the code for building the box.
DECLARE #geomenvelope GEOMETRY;
DECLARE #BoundingBox AS TABLE
(
SouthLat DECIMAL(10, 8)
,NorthLat DECIMAL(10, 8)
,EastLong DECIMAL(10, 8)
,WestLong DECIMAL(10, 8)
);
SELECT #geomenvelope = GEOMETRY::STGeomFromWKB(zn.ZoneGeog.STAsBinary(), zn.ZoneGeog.STSrid).STEnvelope()
FROM dbo.GeogZone AS zn
WHERE zn.Zone = 'CA-1'
INSERT INTO #BoundingBox (SouthLat,NorthLat,EastLong,WestLong)
SELECT #geomenvelope.STPointN(1).STY
,#geomenvelope.STPointN(3).STY
,#geomenvelope.STPointN(1).STX
,#geomenvelope.STPointN(3).STX
SELECT *
FROM #BoundingBox
My question: Is there an alternative (easier) way to get East, West, North, South Points from my Geography Field?
Sorry for the late reply, but hope I can add something.
Firstly, the conversion into LastKnownPoint, you should be able to declare it as follows:
GEOGRAPHY::Point(vlp.Lat, vlp.Long, 4326) AS LastKnownPoint
It works just the same, but is so must easier to read and doesn't require the casts.
To get better performance, you wouldn't have to do the conversion if you can store the Lat / Long as a Geography column in itself which if you're searching regularly is a lot of overhead. Doing this would also allow you to use the Zone directly as a filter and using a spatial index and I couldn't recommend it highly enough. Not to mention no longer needing to create the bounding box.
If you can't do all of that, at least the reduction in CAST'ing and Concatenation should gain you a fair few milliseconds here and there.

SQL Geometry find all points in a radius

I am fluent in SQL but new to using the SQL Geometry features. I have what is probably a very basic problem to solve, but I haven't found any good resources online that explain how to use geometry objects. (Technet is a lousy way to learn new things...)
I have a collection of 2d points on a Cartesian plane, and I am trying to find all points that are within a collection of radii.
I created and populated a table using syntax like:
Update [Things] set [Location] = geometry::Point(#X, #Y, 0)
(#X,#Y are just the x and y values, 0 is an arbitrary number shared by all objects that allows set filtering if I understand correctly)
Here is where I go off the rails...Do I try to construct some sort of polygon collection and query using that, or is there some simple way of checking for intersection of multiple radii without building a bunch of circular polygons?
Addendum: If nobody has the answer to the multiple radii question, what is the single radius solution?
UPDATE
Here are some examples I have worked up, using an imaginary star database where stars are stored on a x-y grid as points:
Selects all points in a box:
DECLARE #polygon geometry = geometry::STGeomFromText('POLYGON(('
+ CAST(#MinX AS VARCHAR(10)) + ' ' + CAST(#MinY AS VARCHAR(10)) + ','
+ CAST(#MaxX AS VARCHAR(10)) + ' ' + CAST(#MinY AS VARCHAR(10)) + ', '
+ CAST(#MaxX AS VARCHAR(10)) + ' ' + CAST(#MaxY AS VARCHAR(10)) + ','
+ CAST(#MinX AS VARCHAR(10)) + ' ' + CAST(#MaxY AS VARCHAR(10)) + ','
+ CAST(#MinX AS VARCHAR(10)) + ' ' + CAST(#MinY AS VARCHAR(10)) + '))', 0);
SELECT [Star].[Name] AS [StarName],
[Star].[StarTypeId] AS [StarTypeId],
FROM [Star]
WHERE #polygon.STContains([Star].[Location]) = 1
using this as a pattern, you can do all sorts of interesting things, such as
defining multiple polygons:
WHERE #polygon1.STContains([Star].[Location]) = 1
OR #polygon2.STContains([Star].[Location]) = 1
OR #polygon3.STContains([Star].[Location]) = 1
Or checking distance:
WHERE [Star].[Location].STDistance(#polygon1) < #SomeDistance
Sample insert statement
INSERT [Star]
(
[Name],
[StarTypeId],
[Location],
)
VALUES
(
#Name,
#StarTypeId,
GEOMETRY::Point(#LocationX, #LocationY, 0),
)
This is an incredibly late answer, but perhaps I can shed some light on a solution. The "set" number you refer to is a Spatial Reference Indentifier or SRID. For lat/long calculations you should consider setting this to 4326, which will ensure metres are used as a unit of measurement. You should also consider switching to SqlGeography rather than SqlGeometry, but we'll continue with SqlGeometry for now. To bulk set the SRID, you can update your table as follows:
UPDATE [YourTable] SET [SpatialColumn] = GEOMETRY.STPointFromText([SpatialColumn].STAsText(), 4326);
For a single radius, you need to create a radii as a spatial object. For example:
DECLARE #radiusInMeters FLOAT = 1000; -- Set to a number in meters
DECLARE #radius GEOMETRY = GEOMETRY::Point(#x, #y, 4326).STBuffer(#radiusInMeters);
STBuffer() takes the spatial point and creates a circle (now a Polygon type) from it. You can then query your data set as follows:
SELECT * FROM [YourTable] WHERE [SpatialColumn].STIntersects(#radius);
The above will now use any Spatial Index you have created on the [SpatialColumn] in its query plan.
There is also a simpler option which will work (and still use a spatial index). The STDistance method allows you to do the following:
DECLARE #radius GEOMETRY = GEOMETRY::Point(#x, #y, 4326);
DECLARE #distance FLOAT = 1000; -- A distance in metres
SELECT * FROM [YourTable] WHERE [SpatialColumn].STDistance(#radius) <= #distance;
Lastly, working with a collection of radii. You have a few options. The first is to run the above for each radii in turn, but I would consider the following to do it as one:
DECLARE #radiiCollection TABLE
(
[RadiusInMetres] FLOAT,
[Radius] GEOMETRY
)
INSERT INTO #radiiCollection ([RadiusInMetres], [Radius]) VALUES (1000, GEOMETRY::Point(#xValue, #yValue, 4326).STBuffer(1000));
-- Repeat for other radii
SELECT
X.[Id],
MIN(R.[RadiusInMetres]) AS [WithinRadiusDistance]
FROM
[YourTable] X
JOIN
#radiiCollection RC ON RC.[Radius].STIntersects(X.[SpatialColumn])
GROUP BY
X.[IdColumn],
R.[RadiusInMetres]
DROP TABLE #radiiCollection;
The final above has not been tested, but I'm 99% sure it's just about there with a small amount of tweaking being a possibility. The ideal of taking the min radius distance in the select is that if the multiple radii stem from a single location, if a point is within the first radius, it will naturally be within all of the others. You'll therefore duplicate the record, but by grouping and then selecting the min, you get only one (and the closest).
Hope it helps, albeit 4 weeks after you asked the question. Sorry I didn't see it sooner, if only there was only one spatial tag for questions!!!!
Sure, this is possible. The individual where clause should be something like:
DIM #Center AS Location
-- Initialize the location here, you probably know better how to do that than I.
Dim #Radius AS Decimal(10, 2)
SELECT * from pointTable WHERE sqrt(square(#Center.STX-Location.STX)+square(#Center.STX-Location.STX)) > #Radius
You can then pile a bunch of radii and xy points into a table variable that looks like like:
Dim #MyCircleTable AS Table(Geometry Circle)
INSERT INTO #MyCircleTable (.........)
Note: I have not put this through a compiler, but this is the bare bones of a working solution.
Other option looks to be here:
http://technet.microsoft.com/en-us/library/bb933904.aspx
And there's a demo of seemingly working syntax here:
http://social.msdn.microsoft.com/Forums/sqlserver/en-US/6e1d7af4-ecc2-4d82-b069-f2517c3276c2/slow-spatial-predicates-stcontains-stintersects-stwithin-?forum=sqlspatial
The second post implies the syntax:
SELECT Distinct pointTable.* from pointTable pt, circletable crcs
WHERE crcs.geom.STContains(b.Location) = 1

Update statement- Geography column - sql server

Is it different to update geography column in sql server than a regular field( varchar....). Can you please provide a sample statement to do this. thanks.
I am not sure if this is the answer you are looking for - but as I would say, the main difference is that when updating a "regular field", you typically provide directly the new value - for example:
UPDATE mytable SET name = 'John' WHERE id = 1
When updating a geography column, you probably cannot provide the value directly (since it is a very long hexadecimal number, which encodes the geoghraphy information) but you will want to compute it from some other values (which can, but do not have to be columns of the same table), e.g.:
UPDATE mytable SET gps=geography::STPointFromText('POINT(' + lng + ' ' + lat + ')', 4326)
where lng and lat are varchar values specifying the GPS coordinates in a "human-readable" format (like lat = '48.955790', lng = '20.524500') - in this case they are also columns of mytable.
If you have Latitude and Longitude as decimals, you can update a geography column as shown below:
DECLARE #latitude DECIMAL(15,6)
,#longitude DECIMAL(15,6);
SET #latitude = 29.938580;
SET #longitude = -81.337384;
UPDATE Properties
SET Geog = GEOGRAPHY::Point(#latitude, #longitude, 4326)
WHERE PropertyID = 858;