为什么要使用SQL Server 2008地理数据类型?


105

我正在重新设计一个客户数据库,我想与标准地址字段(街道,城市等)一起存储的新信息之一就是地址的地理位置。我要记住的唯一用例是,当找不到地址时,允许用户在Google地图上绘制坐标,这通常是在新开发的区域或位于偏远/农村位置时发生。

我的第一个倾向是将纬度和经度存储为十进制值,但是后来我想起了SQL Server 2008 R2具有geography数据类型。我绝对没有使用过的经验geography,并且从我的初步研究来看,对于我的情况而言,这似乎有些过分。

例如,要使用存储为的纬度和经度decimal(7,4),我可以这样做:

insert into Geotest(Latitude, Longitude) values (47.6475, -122.1393)
select Latitude, Longitude from Geotest

但是使用geography,我可以这样做:

insert into Geotest(Geolocation) values (geography::Point(47.6475, -122.1393, 4326))
select Geolocation.Lat, Geolocation.Long from Geotest

尽管没有那么复杂,但是如果不需要的话,为什么还要增加复杂性呢?

在放弃使用的想法之前geography,我应该考虑什么?使用空间索引搜索位置是否比索引纬度和经度字段更快?使用geography我没有意识到的优点吗?或者,另一方面,我应该知道哪些警告会阻止我使用geography


更新资料

@Erik Philips带来了使用进行邻近搜索的功能geography,这非常酷。

另一方面,一项快速测试显示,使用时,简单select地获取经度和纬度会明显变慢geography(详细信息如下)。,而对另一个SO问题的公认答案的评论geography让我很不高兴:

@SaphuA不客气。作为一个附带说明,非常小心使用可为null的GEOGRAPHY数据类型列上的空间索引。存在一些严重的性能问题,因此即使您必须重新构建架构,也应使GEOGRAPHY列不可为空。–托马斯6月18日11:18

总而言之,在权衡进行邻近搜索的可能性与性能和复杂性之间的权衡之后,我决定放弃geography在这种情况下的使用。


我运行的测试的详细信息:

我创建了两个表,一个使用geography,另一条使用decimal(9,6)的纬度和经度:

CREATE TABLE [dbo].[GeographyTest]
(
    [RowId] [int] IDENTITY(1,1) NOT NULL,
    [Location] [geography] NOT NULL,
    CONSTRAINT [PK_GeographyTest] PRIMARY KEY CLUSTERED ( [RowId] ASC )
) 

CREATE TABLE [dbo].[LatLongTest]
(
    [RowId] [int] IDENTITY(1,1) NOT NULL,
    [Latitude] [decimal](9, 6) NULL,
    [Longitude] [decimal](9, 6) NULL,
    CONSTRAINT [PK_LatLongTest] PRIMARY KEY CLUSTERED ([RowId] ASC)
) 

并在每个表格中使用相同的纬度和经度值插入一行:

insert into GeographyTest(Location) values (geography::Point(47.6475, -122.1393, 4326))
insert into LatLongTest(Latitude, Longitude) values (47.6475, -122.1393)

最后,运行以下代码表明,在我的机器上,使用时,选择纬度和经度的速度大约慢5倍geography

declare @lat float, @long float,
        @d datetime2, @repCount int, @trialCount int, 
        @geographyDuration int, @latlongDuration int,
        @trials int = 3, @reps int = 100000

create table #results 
(
    GeographyDuration int,
    LatLongDuration int
)

set @trialCount = 0

while @trialCount < @trials
begin

    set @repCount = 0
    set @d = sysdatetime()

    while @repCount < @reps
    begin
        select @lat = Location.Lat,  @long = Location.Long from GeographyTest where RowId = 1
        set @repCount = @repCount + 1
    end

    set @geographyDuration = datediff(ms, @d, sysdatetime())

    set @repCount = 0
    set @d = sysdatetime()

    while @repCount < @reps
    begin
        select @lat = Latitude,  @long = Longitude from LatLongTest where RowId = 1
        set @repCount = @repCount + 1
    end

    set @latlongDuration = datediff(ms, @d, sysdatetime())

    insert into #results values(@geographyDuration, @latlongDuration)

    set @trialCount = @trialCount + 1

end

select * 
from #results

select avg(GeographyDuration) as AvgGeographyDuration, avg(LatLongDuration) as AvgLatLongDuration
from #results

drop table #results

结果:

GeographyDuration LatLongDuration
----------------- ---------------
5146              1020
5143              1016
5169              1030

AvgGeographyDuration AvgLatLongDuration
-------------------- ------------------
5152                 1022

更令人惊讶的是,即使没有选择任何行,例如选择RowId = 2不存在的where ,geography速度仍然很慢:

GeographyDuration LatLongDuration
----------------- ---------------
1607              948
1610              946
1607              947

AvgGeographyDuration AvgLatLongDuration
-------------------- ------------------
1608                 947

4
我正在考虑同时做,将纬度和经度保存在自己的列中,并为地理对象另辟一列,因此,如果我只需要纬度/经度,则可以从列中获取它们,如果需要邻近搜索,将使用地理。这明智吗?有没有其他缺点(除了占用更多空间...)?
Yuval A. 2012年

@YuvalA。听起来确实合理,并且可能是一个很好的折衷方案。我最关心的唯一问题是,表中的“地理位置”列是否会对表查询产生任何影响-我对此没有经验,因此您需要进行测试以进行验证。
杰夫·绪方

1
您为什么不断用新问题而不是提出新问题来更新您的问题?
乍得

@乍得不知道你的意思。我更新了问题的正文一次,这不是要问更多的问题。
杰夫·绪方

6
对于那些发现此问题的人来说,现在值得注意的是,SQL Server 2012的空间索引性能显着提高。还要注意的事实是,只要您存储位置信息,便可以稍后使用查找服务对已经存储的地址进行地理编码,从而添加空间信息。
Volvox

Answers:


66

如果您打算进行任何空间计算,则EF 5.0允许使用LINQ表达式,例如:

private Facility GetNearestFacilityToJobsite(DbGeography jobsite)
{   
    var q1 = from f in context.Facilities            
             let distance = f.Geocode.Distance(jobsite)
             where distance < 500 * 1609.344     
             orderby distance 
             select f;   
    return q1.FirstOrDefault();
}

然后有一个很好的理由使用地理。

实体框架中空间的解释

更新了创建高性能空间数据库的信息

正如我在Noel Abrahams上指出的那样:

关于空格的注释,每个坐标都存储为长64位(8字节)的双精度浮点数,而8字节的二进制值大致等于15位十进制精度,因此比较十进制(9) ,6)仅5个字节,这并不是完全公平的比较。对于每个LatLong(总共18个字节),小数必须至少为Decimal(15,12)(9个字节),才能进行实际比较。

因此,比较存储类型:

CREATE TABLE dbo.Geo
(    
geo geography
)
GO

CREATE TABLE dbo.LatLng
(    
    lat decimal(15, 12),   
    lng decimal(15, 12)
)
GO

INSERT dbo.Geo
SELECT geography::Point(12.3456789012345, 12.3456789012345, 4326) 
UNION ALL
SELECT geography::Point(87.6543210987654, 87.6543210987654, 4326) 

GO 10000

INSERT dbo.LatLng
SELECT  12.3456789012345, 12.3456789012345 
UNION
SELECT 87.6543210987654, 87.6543210987654

GO 10000

EXEC sp_spaceused 'dbo.Geo'

EXEC sp_spaceused 'dbo.LatLng'

结果:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   560 KB

地理数据类型占用了30%以上的空间。

此外,geography数据类型不仅限于存储Point,还可以存储 LineString,CircularString,CompoundCurve,Polygon,CurvePolygon,GeometryCollection,MultiPoint,MultiLineString和MultiPolygon等。任何试图存储甚至超出一个点(例如LINESTRING(1 1,2 2)实例)的最简单的地理类型(如纬度/经度)都将为每个点带来额外的行,并为每个点的顺序排序另一列用于分组线。SQL Server还具有用于Geography数据类型的方法,其中包括计算Area,Boundary,Length,Distances等

将纬度和经度存储为十进制在Sql Server中似乎是不明智的。

更新2

如果您打算进行距离,面积等任何计算,则很难在地球表面正确地进行计算。存储在SQL Server中的每种地理类型也都存储有一个空间参考ID。这些ID可以属于不同的领域(地球为4326)。这意味着SQL Server中的计算实际上将在地球表面上正确地进行计算(而不是可能穿过地球表面的乌鸦蝇)。

在此处输入图片说明


1
要添加到此信息中,使用Geography确实可以扩展sql搜索的能力,因为sql数据类型允许您创建几乎任何大小和形状的多个区域。
Erik Philips

1
再次感谢。我确实询问了考虑使用的原因,geography并且您提供了一些不错的方法。最终,我决定decimal在这种情况下只使用字段(请参阅我的最新文章),但很高兴知道,geography如果我需要做的比简单地映射坐标更有趣,可以使用它。
杰夫·绪方

6

要考虑的另一件事是每种方法占用的存储空间。地理类型存储为VARBINARY(MAX)。尝试运行此脚本:

CREATE TABLE dbo.Geo
(
    geo geography

)

GO

CREATE TABLE dbo.LatLon
(
    lat decimal(9, 6)
,   lon decimal(9, 6)

)

GO

INSERT dbo.Geo
SELECT geography::Point(36.204824, 138.252924, 4326) UNION ALL
SELECT geography::Point(51.5220066, -0.0717512, 4326) 

GO 10000

INSERT dbo.LatLon
SELECT  36.204824, 138.252924 UNION
SELECT 51.5220066, -0.0717512

GO 10000

EXEC sp_spaceused 'dbo.Geo'
EXEC sp_spaceused 'dbo.LatLon'

结果:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   400 KB

地理数据类型几乎占据了两倍的空间。


2
关于空格的注释,每个坐标都存储为64位(8字节)长的双精度浮点数,而8字节的二进制值大致等于15位十进制精度,因此比较十进制(9) ,6)只有5个字节,这并不是完全公平的比较。对于每个LatLong(总共18个字节),小数必须至少为Decimal(15,12)(9个字节),才能进行实际比较。
Erik Philips

9
@ErikPhilips的要点是,为什么当您只需要十进制(9,6)时为什么要使用十进制(15,12)?上面的比较是一种实际的比较,而不是学术上的比较。
Noel Abrahams 2013年

-1
    CREATE FUNCTION [dbo].[fn_GreatCircleDistance]
(@Latitude1 As Decimal(38, 19), @Longitude1 As Decimal(38, 19), 
            @Latitude2 As Decimal(38, 19), @Longitude2 As Decimal(38, 19), 
            @ValuesAsDecimalDegrees As bit = 1, 
            @ResultAsMiles As bit = 0)
RETURNS decimal(38,19)
AS
BEGIN
    -- Declare the return variable here
    DECLARE @ResultVar  decimal(38,19)

    -- Add the T-SQL statements to compute the return value here
/*
Credit for conversion algorithm to Chip Pearson
Web Page: www.cpearson.com/excel/latlong.aspx
Email: chip@cpearson.com
Phone: (816) 214-6957 USA Central Time (-6:00 UTC)
Between 9:00 AM and 7:00 PM

Ported to Transact SQL by Paul Burrows BCIS
*/
DECLARE  @C_RADIUS_EARTH_KM As Decimal(38, 19)
SET @C_RADIUS_EARTH_KM = 6370.97327862
DECLARE  @C_RADIUS_EARTH_MI As Decimal(38, 19)
SET @C_RADIUS_EARTH_MI = 3958.73926185
DECLARE  @C_PI As Decimal(38, 19)
SET @C_PI =  pi()

DECLARE @Lat1 As Decimal(38, 19)
DECLARE @Lat2 As Decimal(38, 19)
DECLARE @Long1 As Decimal(38, 19)
DECLARE @Long2 As Decimal(38, 19)
DECLARE @X As bigint
DECLARE @Delta As Decimal(38, 19)

If @ValuesAsDecimalDegrees = 1 
Begin
    set @X = 1
END
Else
Begin
    set @X = 24
End 

-- convert to decimal degrees
set @Lat1 = @Latitude1 * @X
set @Long1 = @Longitude1 * @X
set @Lat2 = @Latitude2 * @X
set @Long2 = @Longitude2 * @X

-- convert to radians: radians = (degrees/180) * PI
set @Lat1 = (@Lat1 / 180) * @C_PI
set @Lat2 = (@Lat2 / 180) * @C_PI
set @Long1 = (@Long1 / 180) * @C_PI
set @Long2 = (@Long2 / 180) * @C_PI

-- get the central spherical angle
set @Delta = ((2 * ASin(Sqrt((power(Sin((@Lat1 - @Lat2) / 2) ,2)) + 
    Cos(@Lat1) * Cos(@Lat2) * (power(Sin((@Long1 - @Long2) / 2) ,2))))))

If @ResultAsMiles = 1 
Begin
    set @ResultVar = @Delta * @C_RADIUS_EARTH_MI
End
Else
Begin
    set @ResultVar = @Delta * @C_RADIUS_EARTH_KM
End

    -- Return the result of the function
    RETURN @ResultVar

END

2
总是欢迎有新的答案,但是请添加一些上下文。简要解释上述解决方案的方法会使答案对其他人更有用。
Leigh
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.