Calculating Distance - performance enhancement - sql-server

declare #Latitude DECIMAL(17, 14)='-29.72216606140100',#Longitude DECIMAL(17, 14)='31.06697845459000'
SELECT DISTINCT
*,
pm_Latitude,
pm_Longitude,
CASE
WHEN p.latpoint = pm_Latitude
AND p.longpoint = pm_Longitude
THEN CAST(0 AS DECIMAL(8, 2))
ELSE CAST(p.distance_unit * DEGREES(ACOS(COS(RADIANS(p.latpoint)) * COS(RADIANS(pm_Latitude)) * COS(RADIANS(p.longpoint) - RADIANS(pm_Longitude)) + SIN(RADIANS(p.latpoint)) * SIN(RADIANS(pm_Latitude)))) AS DECIMAL(8, 2))
END AS 'pm_Distance'
FROM Pm_PropertyMapping WITH(NOLOCK) CROSS APPLY
(
SELECT #Latitude AS latpoint,
#Longitude AS longpoint,
100 AS radius,
111.045 AS distance_unit
) AS p
WHERE pm_PropertyName LIKE '%' + 'propertyname' + '%'
AND pm_Latitude BETWEEN p.latpoint - (p.radius / p.distance_unit) AND p.latpoint + (p.radius / p.distance_unit)
AND pm_Longitude BETWEEN p.longpoint - (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint)))) AND p.longpoint + (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
ORDER BY pm_Distance;
What this code does is it searches in a table for the closest property given a specific lat and long.
My question is. Is this the most efficient way of calculating distance in SQL?

Related

Convert HHH:MM:SS to seconds

From the source database, I am getting HH:MM:SS as 832:24:12
Currently I am using below statement which is working fine for most of the cases hh:mm:ss but it fails when hours are more than 99
ISNULL(LEFT(COLUMN,2) * 3600 + RIGHT(LEFT(COLUMN,5),2) * 60 + RIGHT(COLUMN, 2) ,0)
Just another option with a small tweak to your original
Example
Declare #V varchar(50) = '832:24:12'
Select (left(#V,charindex(':',#V)-1)*3600) + (left(right(#V,5),2)*60) + right(#v,2)
Returns
2996652
You can use a tricky solution using PARSENAME() function.
DECALRE #Hours INT = 0, #Minutes INT = 0 , #Seconds INT = 0
SELECT #Hours = PARSENAME(REPLACE('832:24:12'+':00', ':', '.'),4),
#Minutes = PARSENAME(REPLACE('832:24:12'+':00', ':', '.'),3),
#Seconds = PARSENAME(REPLACE('832:24:12'+':00', ':', '.'),2)
SELECT #Hours * 3600 + #Minutes * 60 + #Seconds as TotalSeconds
I am replacing ':' with '.' character after appending dummy sequence of characters ':00' for PARSENAME() function to work by splitting into delimitted data.
For table query
SELECT PARSENAME(REPLACE(ISNULL(ColumnName + ':00',0), ':', '.'),4) * 3600 +
PARSENAME(REPLACE(ISNULL(ColumnName + ':00',0), ':', '.'),3) * 60 +
PARSENAME(REPLACE(ISNULL(ColumnName + ':00',0), ':', '.'),2) As TotalSecs
FROM TableName
This of a guess, however...
CREATE TABLE #Test (TimeString varchar(10))
INSERT INTO #Test
VALUES ('832:24:12')
SELECT TimeString,
(LEFT(TimeString, H.CI - 1) * 3600) + (SUBSTRING(TimeString,H.CI +1, M.CI - H.CI -1) * 60) + (RIGHT(TimeString, LEN(TimeString) - M.CI))
FROM #Test T
CROSS APPLY (VALUES(CHARINDEX(':',TimeString))) H(CI)
CROSS APPLY (VALUES(CHARINDEX(':',TimeString, H.CI+1))) M(CI);
DROP TABLE #Test;
Hours can be the leftwards chars minus 6 positions to take into account the positions for minutes and seconds in the string (:##:##).
The minutes can accessed by taking the left 2, of the rightmost 5 chars.
The seconds are the right 2 chars.
Ex:
DECLARE #tempval varchar(100) = '832:24:12'
SELECT LEFT(#tempval, LEN(#tempval) - 6) * 3600
+LEFT(RIGHT(#tempval, 5), 2) * 60
+RIGHT(#tempval, 2)
Returns
2996652

Issue calculating distances in google maps only in Panama

I have encountered a problem that is driving me crazy.
A few years ago i developed a browser app that calculate the distance from one given point (latitude and longitude coords) to anothers given points.
Everything has worked fine until a few days ago when a client from Panama started working with us. The same SQL procedure that works for years is giving us wrong measurements.
This is the SQL formula:
(Acos(Sin((Ofd.Latitud * PI()) / 180) * Sin((#Longitud * PI()) / 180) + Cos((Ofd.Latitud * PI()) / 180) * Cos((#Longitud * PI()) / 180) * Cos((Ofd.Logitud * PI() / 180) - (#Latitud * PI()) / 180)) * 6371 * 1000) AS Distance
I tried to calculate the distance using the new method since SQL 2008
DECLARE #Latitude float = 8.9749377
DECLARE #Longitude float = -79.5060562
DECLARE #TLatitude float = 8.9868425
DECLARE #TLongitude float = -79.5012872
DECLARE #Source geography
DECLARE #Target geography
SET #Source = geography::STPointFromText('POINT(' + CAST(#Latitude as varchar(20)) + ' ' + CAST(#Longitude as varchar(20)) + ')',4326)
SET #Target = geography::STPointFromText('POINT(' + CAST(#TLatitude as varchar(20)) + ' ' + CAST(#TLongitude as varchar(20)) + ')',4326)
SELECT #source.STDistance(#Target)
The diference between the two methods is negligible, a few meters. The distance that returns the method is ~500m
So, the problem is that the real distance is almost 1500 meters, I've seen and measured the distance in google maps and 1.500 meters is the real distance. The funny side is that, this problem, only happens in Panama. With the clients in Spain we have no problem calculating the distance.
Have I found the Bermuda's triangle?
You have the Latitude and Longitude reversed. WKT POINT coordinates are ordered X,Y (Longitude, Latitude).
DECLARE #Latitude float = 8.9749377
DECLARE #Longitude float = -79.5060562
DECLARE #TLatitude float = 8.9868425
DECLARE #TLongitude float = -79.5012872
DECLARE #Source geography
DECLARE #Target geography
SET #Source = geography::STPointFromText('POINT(' + CAST(#Longitude as varchar(20)) + ' ' + CAST(#Latitude as varchar(20)) + ')',4326)
SET #Target = geography::STPointFromText('POINT(' + CAST(#TLongitude as varchar(20)) + ' ' + CAST(#TLatitude as varchar(20)) + ')',4326)
SELECT #source.STDistance(#Target)

SQL Server: Calculate the Radius of a Lat/Long?

Say I have the latitude and longitude of a city and I need to find out all the airport that are within 100 miles of this location. How would I accomplish this? My data resides in SQL Server. 1 table has all the city info with lat and long and the other has the airport info with lat and long.
First ... convert city's data point
DECLARE #point geography;
SELECT geography::STPointFromText('POINT(' + CAST(#lat AS VARCHAR(20)) + ' ' +
CAST(#lon AS VARCHAR(20)) + ')', 4326)
where #lat and #lon are the latitude and longitude of the city in question.
Then you can query the table ...
SELECT [column1],[column2],[etc]
FROM [table]
WHERE #point.STBuffer(160934.4).STIntersects(geography::STPointFromText(
'POINT(' + CAST([lat] AS VARCHAR(20)) + ' ' +
CAST([lon] AS VARCHAR(20)) + ')', 4326) );
where 160934.4 is the number of meters in 100 miles.
This will be slow, though. If you wanted to do even more spatial work, you could add a persisted computed column (because lat and lon points aren't really going to change) and then use a spatial index.
ALTER TABLE [table]
ADD geo_point AS geography::STPointFromText('POINT(' + CAST([lat] AS VARCHAR(20))
+ ' ' + CAST([lon] AS VARCHAR(20)) + ')', 4326) PERSISTED;
CREATE SPATIAL INDEX spix_table_geopt
ON table(geo_point)
WITH ( BOUNDING_BOX = ( 0, 0, 500, 200 ) ); --you'd have to know your data
I used/wrote this several years ago, and it was close enough for what I needed. Part of the formula takes into account the curvature of the earth if I remember correctly, but it has been a while. I used zip codes, but you could easily adapt for cities instead - same logic.
ALTER PROCEDURE [dbo].[sp_StoresByZipArea] (#zip nvarchar(5), #Radius float) AS
DECLARE #LatRange float
DECLARE #LongRange float
DECLARE #LowLatitude float
DECLARE #HighLatitude float
DECLARE #LowLongitude float
DECLARE #HighLongitude float
DECLARE #istartlat float
DECLARE #istartlong float
SELECT #iStartlat=Latitude, #iStartLong=Longitude from zipcodes where zipcode=#ZIP
SELECT #LatRange = #Radius / ((6076 / 5280) * 60)
SELECT #LongRange = #Radius / (((cos((#iStartLat * 3.141592653589 / 180)) * 6076.) / 5280.) * 60)
SELECT #LowLatitude = #istartlat - #LatRange
SELECT #HighLatitude = #istartlat + #LatRange
SELECT #LowLongitude = #istartlong - #LongRange
SELECT #HighLongitude = #istartlong + #LongRange
/** Now you can create a SQL statement which limits the recordset of cities in this manner: **/
SELECT * FROM ZipCodes
WHERE (Latitude <= #HighLatitude) AND (Latitude >= #LowLatitude) AND (Longitude >= #LowLongitude) AND (Longitude <= #HighLongitude)

Convert signed int to string ip address in SQL Server

I'm retrieving a signed int from a SQL Server database and need to convert it to a "normal" looking dotted string for display to users.
Googling, I found this code:
SELECT
dbo.IPADDRESS.IPADDRESS,
CAST(ROUND( (cast(dbo.IPADDRESS.IPADDRESS as bigint) / 16777216 ), 0, 1) AS varchar(4)) + '.' +
CAST((ROUND( (cast(dbo.IPADDRESS.IPADDRESS as bigint) / 65536 ), 0, 1) % 256) AS varchar(4)) + '.' +
CAST((ROUND( (cast(dbo.IPADDRESS.IPADDRESS as bigint) / 256 ), 0, 1) % 256) AS varchar(4)) + '.' +
CAST((cast(dbo.IPADDRESS.IPADDRESS as bigint) % 256 ) AS varchar(4)) as IPDottedNotation
FROM
dbo.IPADDRESS
which works some of the time, but produces wacky output other times. For example, converting this -1951276725 yields the result -116.-78.-30.-181.
Any suggestions? Thanks.
DECLARE #IPADDRESS TABLE (
IPADDRESS INT);
INSERT INTO #IPADDRESS
VALUES (-1139627840),
( 1);
SELECT
LTRIM(CAST(SUBSTRING(IP,4,1) AS TINYINT)) + '.' +
LTRIM(CAST(SUBSTRING(IP,3,1) AS TINYINT)) + '.' +
LTRIM(CAST(SUBSTRING(IP,2,1) AS TINYINT)) + '.' +
LTRIM(CAST(SUBSTRING(IP,1,1) AS TINYINT))
FROM #IPADDRESS
CROSS APPLY (SELECT CAST(IPADDRESS AS BINARY(4))) C(IP)
The code you have would work if IPADDRESS was a bigint (effectively storing the unsigned int representation in the database - i.e. all values > 0). Do you have the option of changing the datatype in the table?
To get what you have to work, you need to convert your signed int to the equivalent unsigned int before the conversion to bigint. I'm not sure what the most efficient way to do this in TSQL is, but it might be to cast it to binary:
SELECT dbo.IPADDRESS.IPADDRESS,
CAST(ROUND( (cast(cast(dbo.IPADDRESS.IPADDRESS as binary(4)) as bigint) / 16777216 ), 0, 1) AS varchar(4)) + '.' +
CAST((ROUND( (cast(cast(dbo.IPADDRESS.IPADDRESS as binary(4)) as bigint) / 65536 ), 0, 1) % 256) AS varchar(4)) + '.' +
CAST((ROUND( (cast(cast(dbo.IPADDRESS.IPADDRESS as binary(4)) as bigint) / 256 ), 0, 1) % 256) AS varchar(4)) + '.' +
CAST((cast(cast(dbo.IPADDRESS.IPADDRESS as binary(4)) as bigint) % 256 ) AS varchar(4)) as IPDottedNotation
Like #Ed Harper stated that the selected solution doesn't work for a signed int. Below is my solution which requires a little less casting and isn't inverted. Check out the following test scenario shown below where the converted string/varchar IP should be 192.168.18.188:
CREATE TABLE #data
(
ip NVARCHAR(45),
before NVARCHAR(45)
)
INSERT INTO #data
VALUES ('converted-ip','-1139627840')
update #data
set ip = cast((cast(before as int) & 255) as nvarchar) + '.' +
cast((cast(floor(convert(decimal, before)/256) as int) & 255) as nvarchar) + '.' +
cast((cast(floor(convert(decimal, before)/65536) as int) & 255) as nvarchar) + '.' +
cast((cast(floor(convert(decimal, before)/16777216) as int) & 255) as nvarchar)
select * from #data

Making this spatial query more efficient

I have 2 tables:
tZipCodeNoCity with ZipCode and PointGeography
and MBLPosition with Latitude and Longitude
In this query I'm finding closest ZipCode to my positions. It's "poor mans" geocoding :)
How do I write this query so I don't have to do this SELECT TOP 1 inline?
It's pretty slow with even 150 devices (like 20 seconds)
I had to include 150 mile radius into this subselect to get it faster
SELECT LastPositions.DeviceId, P.Description, P.Latitude, P.Longitude, P.Speed, P.DeviceTime,
(
SELECT TOP 1 ZC.ZipCode
FROM dbo.tZipCodeNoCity ZC
WHERE ZC.PointGeography.STDistance(geography::STPointFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326)) < 150 * 1609.344
ORDER BY ZC.PointGeography.STDistance(geography::STPointFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326))
)
FROM dbo.MBLPosition P
INNER JOIN
(
SELECT D.DeviceId, MAX(P.PositionKey) LastPositionKey
FROM dbo.MBLPosition P
INNER JOIN IDATTApplication.dbo.MBLDevice D ON P.DeviceKey = D.DeviceKey
GROUP BY D.DeviceId
) LastPositions ON P.PositionKey = LastPositions.LastPositionKey
In a project I worked on about 12 years ago, I ran a query along these lines to reduce the list of possibilities before doing the actual distance calculation:
WHERE zip.lat < my.lat + 0.5 && zip.lat > my.lat - 0.5
&& zip.long < my.long + 0.5 && zip.long > my.long - 0.5
From that subset, I calculate the actual distance between the two points and sort on it. You'll have to adjust the "0.5" portion as appropriate to get a big enough box to be sure you're going to get a hit.
And I would imagine that there's a better way than STPointFromText to create your point object. Could you use STPointFromWKB? Could you convert to the geography type once?
See this page for an example of creating your point via SET.
DECLARE #p geography;
SET #p = geography::STGeomFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326);
SELECT TOP 1 ZC.ZipCode
FROM dbo.tZipCodeNoCity ZC
WHERE ZC.PointGeography.STDistance(#p)) < 150 * 1609.344
ORDER BY ZC.PointGeography.STDistance(#p))

Resources