Login or Sign Up to become a member!
LessThanDot Sit Logo

LessThanDot

Data Management

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Lessthandot twitter Lessthandot Linkedin Lessthandot friendfeed Lessthandot facebook Lessthandot rss

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Your profile

Authors

Search

XML Feeds

Google Ads

« SQL Friday, The Best SQL Server Links Of The Past Week Episode 11SQL Server Zipcode Latitude/Longitude proximity distance search »
comments
Rate Post:
submit to reddit Digg!FacebookDotnetkicks

George and I decided to show you how you can do simple radius searches based on Zip Codes, he did the SQL 2005 and before version here: SQL Server Zipcode Latitude/Longitude proximity distance search and I will do the SQL Server 2008 version.

The first thing we need to do is load our data. There are various sources for this data, ranging from free to expensive monthly subscriptions. One source of free data is: http://download.geonames.org/export/zip/ make sure to grab the US.ZIP file for this demo. Unrar/Unzip the file and you will have a US.txt file.

Once you have downloaded your data, the next step is to import it in to your database. You can use the following script to do it.

  1. CREATE TABLE ZipCodesTemp(
  2.     [Country] [VARCHAR](2) NULL,
  3.     [ZipCode] [VARCHAR](5) NULL,
  4.     [City] [VARCHAR](200) NULL,
  5.     [STATE] [VARCHAR](50) NULL,
  6.     [StateAbbreviation] [VARCHAR](2) NULL,
  7.     [County] [VARCHAR](50) NULL,
  8.     [Unused1] [VARCHAR](5) NULL,
  9.     [Unused2] [VARCHAR](1) NULL,
  10.     [Latitude] [DECIMAL](8,5) NULL,
  11.     [Longitude] [DECIMAL](8,5) NULL,
  12.     [Unused3] [VARCHAR](1) NULL
  13.     )
  14.  
  15.  
  16. DECLARE @bulk_cmd VARCHAR(1000)
  17. SET @bulk_cmd = 'BULK INSERT ZipCodesTemp
  18.   FROM ''C:\YourFolder\US.txt''
  19.   WITH (FIELDTERMINATOR=''\t'', ROWTERMINATOR = '''+CHAR(10)+''')'
  20.  
  21. EXEC(@bulk_cmd)

Now you will create this table

  1. CREATE TABLE ZipCodes(
  2.     [Country] [VARCHAR](2)  NULL,
  3.     [ZipCode] [VARCHAR](5) NOT NULL,
  4.     [City] [VARCHAR](200)  NULL,
  5.     [STATE] [VARCHAR](50)  NULL,
  6.     [StateAbbreviation] [VARCHAR](2)  NULL,
  7.     [County] [VARCHAR](50) NULL,
  8.     [Latitude] [DECIMAL](8,5) NOT NULL,
  9.     [Longitude] [DECIMAL](8,5) NOT NULL,
  10.     [GeogCol1] [GEOGRAPHY]  NULL,
  11.     [GeogColTemp] [VARCHAR](100) NULL
  12.     )

There is at least one duplicate row in this file so we will import only uniques

  1. INSERT  ZipCodes (Country,ZipCode,City,STATE,StateAbbreviation,County,Latitude,Longitude)
  2. SELECT DISTINCT Country,ZipCode,City,STATE,StateAbbreviation,County,Latitude,Longitude
  3. FROM  ZipCodesTemp

Our next step will be to update the GeogCol1 table with something that SQL server can understand.
Here is some sample code that displays the format of this datatype

  1. DECLARE @h geography;
  2. SET @h = geography::STGeomFromText('POINT(-77.36750 38.98390)', 4326);
  3. SELECT @h

output
----------------------------------------------
0xE6100000010C6744696FF07D4340EC51B81E855753C0

As you can see it is some binary data. This data is using the World Geodetic System 1984 (WGS 84)

To see if this is supported in your database you can run this query

  1. SELECT * FROM sys.spatial_reference_systems

And yes in my database it has a spatial_reference_id of 4326

  1. SELECT * FROM sys.spatial_reference_systems
  2. WHERE spatial_reference_id = 4326

Here is the meta data

GEOGCS["WGS 84", DATUM["World Geodetic System 1984",
ELLIPSOID["WGS 84", 6378137, 298.257223563]], PRIMEM["Greenwich", 0],
UNIT["Degree", 0.0174532925199433]]

Back to our code, in order to have this data in our table

SET @h = geography::STGeomFromText('POINT(-77.36750 38.98390)', 4326);

We need to do some things, first we update our temp column

  1. UPDATE zipcodes
  2. SET GeogColTemp= 'POINT(' + CONVERT(VARCHAR(100),longitude)
  3. +' ' +  CONVERT(VARCHAR(100),latitude) +')'

Now we can update out geography column

  1. UPDATE zipcodes
  2. SET GeogCol1 =  geography::STGeomFromText(GeogColTemp,4326)

We can drop the temp column now

  1. ALTER TABLE zipcodes DROP COLUMN GeogColTemp



Now we have to add a primary key, this is needed because otherwise we won't be able to create our spatial index and the following message would be displayed

Server: Msg 12008, Level 16, State 1, Line 1
Table 'zipcodes' does not have a clustered primary key as required by the spatial index. Make sure that the primary key column exists on the table before creating a spatial index.

  1. ALTER TABLE zipcodes ADD
  2.     CONSTRAINT [PK_ZipCode] PRIMARY KEY  CLUSTERED
  3.     (
  4.         Zipcode,
  5.         Longitude
  6.     ) WITH  FILLFACTOR = 100

Create the spatial index

  1. CREATE SPATIAL INDEX SIndx_SpatialTable_geography_col1
  2.    ON zipcodes(GeogCol1);

first I will show you an example to calculate the distance that you can execute

  1. DECLARE @g geography;
  2. DECLARE @h geography;
  3. SET @h = geography::STGeomFromText('POINT(-77.36750 38.98390)', 4326);
  4. SET @g = geography::STGeomFromText('POINT(-77.36160 38.85570)', 4326);
  5. SELECT @g.STDistance(@h)/1609.344;

as you can see the distance is 8.8490611480890067

Now I want to see all the zipcode which are within 20 miles of zipcode 10028 (yes I used to live there)

Here is a way that will take a long time since it is not sargable, this will take about 2 seconds

  1. SELECT h.*
  2. FROM zipcodes g
  3. JOIN zipcodes h ON g.zipcode <> h.zipcode
  4. AND g.zipcode = '10028'
  5. AND h.zipcode <> '10028'
  6. WHERE g.GeogCol1.STDistance(h.GeogCol1)/1609.344 <= 20

Now we all know functions on the left side of the operator are bad, here is how this is optimized, we switch the calculation to the right side of the = sign

  1. SELECT h.*
  2. FROM zipcodes g
  3. JOIN zipcodes h ON g.zipcode <> h.zipcode
  4. AND g.zipcode = '10028'
  5. AND h.zipcode <> '10028'
  6. WHERE g.GeogCol1.STDistance(h.GeogCol1)<=(20 * 1609.344)

that ran in between 15 and 60 milliseconds

To find everything between 10 and 20 miles you can use this

  1. SELECT h.*
  2. FROM zipcodes g
  3. JOIN zipcodes h ON g.zipcode <> h.zipcode
  4. AND g.zipcode = '10028'
  5. AND h.zipcode <> '10028'
  6. WHERE g.GeogCol1.STDistance(h.GeogCol1)<=(20 * 1609.344)
  7. AND g.GeogCol1.STDistance(h.GeogCol1)>= (10 * 1609.344)

As you can see doing stuff like this on SQL Server 2008 is fairly easy because of the geograpy data type




*** If you have a SQL related question try our Microsoft SQL Server Programming forum or our Microsoft SQL Server Admin forum

About the Author

User bio imageDenis has been working with SQL Server since version 6.5. Although he worked as an ASP/JSP/ColdFusion developer before the dot com bust, he has been working exclusively as a database developer/architect since 2002. In addition to English, Denis is also fluent in Croatian and Dutch, but he can curse in many other languages and dialects (just ask the SQL optimizer) He lives in Princeton, NJ with his wife and three kids.
Social SitingsTwitterFacebookLinkedInHomePageLTD RSS Feed
6686 views
submit to reddit Digg!FacebookDotnetkicks

Comments and Feedback

20 comments

Comment from: Michelle Ufford [Visitor] Email · http://sqlfool.com
*****
I have absolutely no use for this, but this is a really cool new feature and a really great post. Thanks, Denis! :)
02/11/09 @ 14:46
Comment from: SQLDenis [Member] Email
You are welcome.

Credits go to George since he came up with the idea of the blogpost I just happened to do the 2008 version
02/11/09 @ 14:48
Comment from: AlexCuse [Member] Email
*****
Thanks for doing this you guys, I'd really been wanting to see a good tutorial to get me up to speed on this. I'm about to move crummy web page to a SQL 2008 host, I'm really hoping I'll be able to use this feature for a new and improved version of the location thing I'm working on.
02/11/09 @ 21:12
Comment from: AlexCuse [Member] Email
I had a little beef with their data for one zip code. This is more like it

update ZipCodes
set GeogCol1 = geography::STGeomFromText('POINT(40.02963540591171 -75.56317090988159)', 4326)
where ZipCode = 19345

These spatial indexes are pretty impressive!
02/12/09 @ 23:10
Comment from: Chuck Conway [Visitor] · http://cconway.com
*****
My apologies if I missed it, but in some of your procedures you use '1609.344' in some of the calculations.

What is '1609.344' ?
05/14/09 @ 16:52
Comment from: SQLDenis [Member] Email
*****
Chuck, it is done to convert to miles, the metric system is used in geospatial calculations
05/14/09 @ 17:20
Comment from: Chuck Conway [Visitor] · http://www.cconway.com
*****
Thanks!

I see it now. I didn't think about the conversion.
05/14/09 @ 21:51
Comment from: Tien [Visitor] Email · http://www.lekevin.com
*****
Thank you so much for a great tutorial about geograpy data type.
07/14/09 @ 16:34
Comment from: Jeremy Kane [Visitor] · http://www.kidseatcheaptonight.com/
*****
I was very glad to find this article! I have a site I'm working on, and by using the techniques described in this blog, I was able to make my proximity searching MUCH faster.

I wrote a blog entry (http://www.kidseatcheaptonight.com/blog/2009/08/site-updated-speed-increase.aspx) pointing some people to this blog, if any tech geeks happen to come across my site, hopefully they will pay your blog a visit too.

Thanks!
Jeremy
08/11/09 @ 00:13
Comment from: JoeSF [Visitor]
*****
I would highly recommend doing the geography right from the original numeric rather than converting to text:

geography::Point(latitude, longitude, 4326);

As in:

DECLARE @h geography;
SET @h = geography::Point(-77.36750, 38.98390, 4326);
SELECT @h

Then there is no interim conversion and problems are resolved before the statement is run.
09/08/09 @ 10:52
Comment from: joe mocerino [Visitor]
****-
this is great and works well. one question, how can i sort by distinct (closer to farthest)?
09/11/09 @ 15:10
Comment from: SQLDenis [Member] Email
Joe,

can you try if

ORDER BY g.GeogCol1.STDistance(h.GeogCol1)

works for you, not near a PC with sql server right now but that should work
09/11/09 @ 17:23
Comment from: Bass [Visitor]
*****
Hi ... Denis ...
to connvert to miles we used this : '1609.344'
and what the value if i want to convert to metre


thanx

10/30/09 @ 09:44
Comment from: SQLDenis [Member] Email
Bass for meters leave of the division

examples

--MILES
DECLARE @g geography;
DECLARE @h geography;
SET @h = geography::STGeomFromText('POINT(-77.36750 38.98390)', 4326);
SET @g = geography::STGeomFromText('POINT(-77.36160 38.85570)', 4326);
SELECT @g.STDistance(@h)/1609.344; --miles 8.84906114808901
GO

--METERS
DECLARE @g geography;
DECLARE @h geography;
SET @h = geography::STGeomFromText('POINT(-77.36750 38.98390)', 4326);
SET @g = geography::STGeomFromText('POINT(-77.36160 38.85570)', 4326);
SELECT @g.STDistance(@h); --meters...14241.1834643102
Go

--KILOMETERS
DECLARE @g geography;
DECLARE @h geography;
SET @h = geography::STGeomFromText('POINT(-77.36750 38.98390)', 4326);
SET @g = geography::STGeomFromText('POINT(-77.36160 38.85570)', 4326);
SELECT @g.STDistance(@h)/1000; --kilometers 14.2411834643102
GO
10/30/09 @ 10:53
Comment from: Bass [Visitor]
*****
Thanx a large ,,, Denis

Actually i v been create simple GIS applcations
using sql server 2008 for database and
visual studio 2008 to build the appliaction ...

but i dont know how to preview spatial data on visual studio 2008 as shown on sql server 2008 ...

if you have reference or tutorial for me...
i am very appreciated ....

i hope you not borring for my question

thanx before
and sory i am not good enough in english ...
10/31/09 @ 00:36
Comment from: Eve [Visitor]
I am taking a T-SQL class and need some help. I have to writw UDF that takes as input the latitude and lingitutde of each of two points on the earth's surface, and calculates the distance between the ow points. The @distance formula was given and need the accuracy of one tenth of a mile.

Hope some can help!
Eve
12/11/09 @ 23:51
Comment from: chrissie1 [Member] Email
Questions like these are better posted in the forum.
12/12/09 @ 02:09
Comment from: Ash [Visitor]
This stuff is amazing. I've just started rewriting a proximity search piece of one of our websites and happened upon this little gem. Thanks Denis and George!
01/29/10 @ 07:19
Comment from: Matt [Visitor] · http://www.scratty.com
This is GREAT!! Wow, been working with Zip Code radius searches and this makes it so much easier. Thanks!!!
02/08/10 @ 19:24
Comment from: Matt [Visitor]
One question, why doesn't the query return the zip code that was searched in the results? The distance would be 0 but is still less than the search radius.
02/08/10 @ 19:53

Leave a comment


Your email address will not be revealed on this site.

Your URL will be displayed.
(Line breaks become <br />)
(Name, email & website)
(Allow users to contact you through a message form (your email will not be revealed.)