Por que usar o tipo de dados geográficos do SQL Server 2008?

105

Estou redesenhando um banco de dados de clientes e uma das novas informações que gostaria de armazenar junto com os campos de endereço padrão (rua, cidade etc.) é a localização geográfica do endereço. O único caso de uso que tenho em mente é permitir que os usuários mapeiem as coordenadas nos mapas do Google quando o endereço não puder ser encontrado de outra forma, o que geralmente acontece quando a área é desenvolvida recentemente ou está em uma localização remota / rural.

Minha primeira inclinação foi armazenar latitude e longitude como valores decimais, mas então me lembrei que o SQL Server 2008 R2 tem um geographytipo de dados. Não tenho absolutamente nenhuma experiência com o uso geographye, pela minha pesquisa inicial, parece um exagero para o meu cenário.

Por exemplo, para trabalhar com latitude e longitude armazenadas como decimal(7,4), posso fazer o seguinte:

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

mas com geography, eu faria isso:

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

Embora não seja que muito mais complicado, porque a complexidade add se eu não tem que?

Antes de abandonar a ideia de usar geography, há algo que devo considerar? Seria mais rápido pesquisar um local usando um índice espacial em vez de indexar os campos Latitude e Longitude? Há vantagens em usar geographyque eu não conheço? Ou, por outro lado, há ressalvas que devo saber que me desencorajariam a usargeography ?


Atualizar

@Erik Philips trouxe a capacidade de fazer pesquisas de proximidade com geography , o que é muito legal.

Por outro lado, um teste rápido está mostrando que uma forma simples selectde obter a latitude e a longitude é significativamente mais lenta ao usar geography(detalhes abaixo). , e um comentário sobre a resposta aceita a outra pergunta do SO geographyme deixou desconfiado:

@SaphuA De nada. Como nota lateral, tenha MUITO cuidado ao usar um índice espacial em uma coluna de tipo de dados GEOGRAPHY anulável. Há alguns problemas sérios de desempenho, portanto, torne essa coluna GEOGRAPHY não anulável, mesmo se você precisar remodelar seu esquema. - Tomas 18 de junho às 11h18

Ao todo, pesando a probabilidade de fazer pesquisas de proximidade versus a compensação em desempenho e complexidade, decidi renunciar ao uso de geographyneste caso.


Detalhes do teste que fiz:

Criei duas tabelas, uma usando geographye outra usando decimal(9,6)para latitude e longitude:

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)
) 

e inseriu uma única linha usando os mesmos valores de latitude e longitude em cada tabela:

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

Finalmente, rodar o código a seguir mostra que, na minha máquina, selecionar a latitude e longitude é aproximadamente 5 vezes mais lento durante o uso 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

Resultados:

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

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

O que foi mais surpreendente é que mesmo quando nenhuma linha foi selecionada, por exemplo, selecionar onde RowId = 2, que não existe, geographyainda era mais lento:

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

AvgGeographyDuration AvgLatLongDuration
-------------------- ------------------
1608                 947
Jeff Ogata
fonte
4
Estou pensando em fazer os dois, salvar o Lat e o Lon em suas próprias colunas e ter outra coluna para um objeto Geografia, então, se eu só precisar do Lat / Lon, eu os pego das colunas, e se preciso da pesquisa de proximidade, vou usar a Geografia. Isso é sábio? Existem desvantagens (além de ocupar mais espaço ...)?
Yuval A.
@YuvalA. isso certamente parece razoável e pode ser um bom compromisso. A única preocupação que tenho em cima da minha cabeça é se ter a coluna Geografia na tabela tem algum impacto nas consultas da tabela - não tenho experiência com isso, então você precisaria testar para verificar.
Jeff Ogata,
1
Por que você continuou atualizando sua pergunta com novas perguntas em vez de fazer novas perguntas?
Chade
@Chad não tenho certeza do que você quer dizer. Eu atualizei o corpo da pergunta uma vez, e não era para fazer mais perguntas.
Jeff Ogata
6
É importante notar, agora, para aqueles que encontraram essa pergunta, que o SQL Server 2012 inclui aumentos de desempenho significativos com a indexação espacial. Também digno de nota é o fato de que, enquanto você estiver armazenando informações de localização, você pode adicionar informações espaciais posteriormente usando um serviço de pesquisa para geocodificar seus endereços já armazenados.
Volvox

Respostas:

66

Se você planeja fazer qualquer computação espacial, EF 5.0 permite Expressões LINQ como:

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();
}

Então, há um bom motivo para usar a Geografia.

Explicação de espacial no Entity Framework .

Atualizado com a criação de bancos de dados espaciais de alto desempenho

Como observei na resposta de Noel Abrahams :

Uma nota sobre o espaço, cada coordenada é armazenada como um número de ponto flutuante de precisão dupla com 64 bits (8 bytes) de comprimento, e o valor binário de 8 bytes é aproximadamente equivalente a 15 dígitos de precisão decimal, portanto, comparando um decimal (9 , 6) que tem apenas 5 bytes, não é exatamente uma comparação justa. Decimal deve ser no mínimo Decimal (15,12) (9 bytes) para cada LatLong (total de 18 bytes) para uma comparação real.

Então, comparando os tipos de armazenamento:

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'

Resultado:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   560 KB

O tipo de dados geográficos ocupa 30% mais espaço.

Além disso, o tipo de dados geográficos não se limita apenas ao armazenamento de um Ponto, você também pode armazenar LineString, CircularString, CompoundCurve, Polygon, CurvePolygon, GeometryCollection, MultiPoint, MultiLineString e MultiPolygon e muito mais . Qualquer tentativa de armazenar até mesmo o mais simples dos tipos de geografia (como latitude / longitude) além de um ponto (por exemplo, LINESTRING (1 1, 2 2) instância) incorrerá em linhas adicionais para cada ponto, uma coluna para sequenciamento para a ordem de cada ponto e outra coluna para agrupamento de linhas. O SQL Server também possui métodos para os tipos de dados geográficos que incluem o cálculo de área, limite, comprimento, distâncias e muito mais .

Parece imprudente armazenar Latitude e Longitude como Decimal no Sql Server.

Atualização 2

Se você planeja fazer cálculos como distância, área, etc, calculá-los corretamente sobre a superfície da Terra é difícil. Cada tipo de geografia armazenado no SQL Server também é armazenado com um ID de referência espacial . Esses ids podem ser de esferas diferentes (a terra é 4326). Isso significa que os cálculos no SQL Server irão, na verdade, calcular corretamente sobre a superfície da terra (em vez de como o corvo voa, que poderia ser através da superfície da terra).

insira a descrição da imagem aqui

Erik Philips
fonte
1
Para adicionar a essas informações, o uso de Geografia realmente expande a capacidade de pesquisas sql a partir de uma latitude / longitude entre outras latitudes / longitudes (normalmente apenas retângulos) porque o tipo de dados Geografia permite criar várias regiões de quase qualquer tamanho e forma.
Erik Philips,
1
obrigado novamente. Eu perguntei por motivos para considerar o uso geographye você forneceu alguns bons. No final das contas, decidi usar apenas decimalcampos neste caso (veja minha atualização prolixa), mas é bom saber que posso usar geographyse precisar fazer algo mais sofisticado do que simplesmente mapear coordenadas.
Jeff Ogata
6

Outra coisa a se considerar é o espaço de armazenamento ocupado por cada método. O tipo de geografia é armazenado como um VARBINARY(MAX). Tente executar este script:

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'

Resultado:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   400 KB

O tipo de dados geográficos ocupa quase o dobro do espaço.

Noel Abrahams
fonte
2
Uma nota sobre o espaço, cada coordenada é armazenada como um número de ponto flutuante de precisão dupla com 64 bits (8 bytes) de comprimento, e o valor binário de 8 bytes é aproximadamente equivalente a 15 dígitos de precisão decimal , portanto, comparando um decimal (9 , 6) que tem apenas 5 bytes , não é exatamente uma comparação justa. Decimal deve ser no mínimo Decimal (15,12) (9 bytes) para cada LatLong (total de 18 bytes) para uma comparação real.
Erik Philips,
9
@ErikPhilips o ponto é por que usar um decimal (15, 12) quando tudo o que você precisa é um decimal (9, 6)? A comparação acima é prática - não um exercício acadêmico.
Noel Abrahams,
-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: [email protected]
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
Paul Burrows
fonte
2
Novas respostas são sempre bem-vindas, mas por favor, adicione algum contexto. Explicar resumidamente como o acima resolve o problema torna a resposta mais útil para outras pessoas.
Leigh,