Dlaczego warto używać typu danych geograficznych programu SQL Server 2008?

105

Przeprojektowuję bazę danych klientów, a jedną z nowych informacji, które chciałbym przechowywać wraz ze standardowymi polami adresowymi (Ulica, Miasto, itp.) Jest lokalizacja geograficzna adresu. Jedynym przypadkiem użycia, o którym myślę, jest umożliwienie użytkownikom mapowania współrzędnych na mapach Google, gdy nie można znaleźć adresu w inny sposób, co często ma miejsce, gdy obszar jest nowo zagospodarowany lub znajduje się w odległej / wiejskiej lokalizacji.

Moją pierwszą skłonnością było przechowywanie szerokości i długości geograficznej jako wartości dziesiętnych, ale potem przypomniałem sobie, że SQL Server 2008 R2 ma geographytyp danych. Nie mam absolutnie żadnego doświadczenia w używaniu geography, a z moich początkowych badań wynika, że ​​jest to przesada w moim scenariuszu.

Na przykład, aby pracować z szerokością i długością geograficzną zapisaną jako decimal(7,4), mogę to zrobić:

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

ale z geographyzrobiłbym to:

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

Chociaż nie jest to o wiele bardziej skomplikowane, po co dodawać złożoność, jeśli nie muszę?

Zanim porzucę pomysł używania geography, czy jest coś, co powinienem rozważyć? Czy wyszukiwanie lokalizacji przy użyciu indeksu przestrzennego byłoby szybsze niż indeksowanie pól szerokości i długości geograficznej? Czy są zalety używania geography, o których nie wiem? A może z drugiej strony, czy są zastrzeżenia, o których powinienem wiedzieć, a które zniechęciłyby mnie do używania geography?


Aktualizacja

@Erik Philips wspomniał o możliwości wyszukiwania w pobliżu geography, co jest bardzo fajne.

Z drugiej strony szybki test pokazuje, że proste selectuzyskanie szerokości i długości geograficznej jest znacznie wolniejsze podczas używania geography(szczegóły poniżej). , a komentarz dotyczący zaakceptowanej odpowiedzi na inne pytanie SO na temat geographymnie niepokoi:

@SaphuA Nie ma za co. Na marginesie należy BARDZO ostrożnie używać indeksu przestrzennego w kolumnie typu danych GEOGRAPHY dopuszczającej wartość null. Występują poważne problemy z wydajnością, więc spraw, aby kolumna GEOGRAPHY nie dopuszczała wartości null, nawet jeśli musisz przebudować swój schemat. - Tomas 18 czerwca o 11:18

Podsumowując, biorąc pod uwagę prawdopodobieństwo wykonania wyszukiwania zbliżeniowego w porównaniu z kompromisem w wydajności i złożoności, zdecydowałem się zrezygnować z użycia geographyw tym przypadku.


Szczegóły testu, który przeprowadziłem:

Utworzyłem dwie tabele, jedną używającą, geographya drugą używającą decimal(9,6)szerokości i długości geograficznej:

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

i wstawił pojedynczy wiersz z tymi samymi wartościami szerokości i długości geograficznej do każdej tabeli:

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

Wreszcie uruchomienie poniższego kodu pokazuje, że na moim komputerze wybranie szerokości i długości geograficznej jest około 5 razy wolniejsze podczas korzystania z 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

Wyniki:

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

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

Bardziej zaskakujące było to, że nawet jeśli nie wybrano żadnych wierszy, na przykład wybranie miejsca RowId = 2, w którym nie ma, geographybyło nadal wolniejsze:

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

AvgGeographyDuration AvgLatLongDuration
-------------------- ------------------
1608                 947
Jeff Ogata
źródło
4
Myślę o zrobieniu obu, zapisaniu Lat i Lon w ich własnych kolumnach i mam kolejną kolumnę dla obiektu Geography, więc jeśli potrzebuję tylko Lat / Lon, biorę je z kolumn, a jeśli potrzebuję wyszukiwania zbliżeniowego, ja Użyję Geography. Czy to mądre? Czy są jakieś wady (poza tym, że zajmuje więcej miejsca ...)?
Yuval A.
@YuvalA. to z pewnością brzmi rozsądnie i może być dobrym kompromisem. Jedynym problemem, który mam na głowie, jest to, czy posiadanie kolumny Geografia w tabeli ma jakikolwiek wpływ na zapytania w tabeli - nie mam z tym doświadczenia, więc musisz przetestować, aby zweryfikować.
Jeff Ogata
1
Dlaczego ciągle aktualizowałeś swoje pytanie nowymi pytaniami, zamiast zadawać nowe?
Czad
@ Nie jestem pewien, co masz na myśli. Raz zaktualizowałem treść pytania i nie chodziło o zadawanie więcej pytań.
Jeff Ogata,
6
Warto teraz zauważyć, że dla tych, którzy znaleźli to pytanie, SQL Server 2012 zapewnia znaczny wzrost wydajności dzięki indeksowaniu przestrzennemu. Warto również zauważyć, że dopóki przechowujesz informacje o lokalizacji, możesz później dodać informacje przestrzenne za pomocą usługi wyszukiwania, aby geokodować już zapisane adresy.
Volvox

Odpowiedzi:

66

Jeśli planujesz wykonywać jakiekolwiek obliczenia przestrzenne, EF 5.0 zezwala na wyrażenia LINQ, takie jak:

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

Jest też bardzo dobry powód, aby skorzystać z Geografii.

Wyjaśnienie przestrzenności w ramach Entity Framework .

Zaktualizowano o tworzenie wysokowydajnych przestrzennych baz danych

Jak zauważyłem w odpowiedzi Noela Abrahamsa :

Uwaga dotycząca spacji, każda współrzędna jest przechowywana jako liczba zmiennoprzecinkowa podwójnej precyzji, która ma 64 bity (8 bajtów), a 8-bajtowa wartość binarna jest w przybliżeniu równoważna 15 cyfrom dokładności dziesiętnej, więc porównanie liczby dziesiętnej (9 , 6), który ma tylko 5 bajtów, nie jest dokładnym porównaniem. Decimal musiałby mieć co najmniej Decimal (15,12) (9 bajtów) dla każdego LatLong (łącznie 18 bajtów) dla rzeczywistego porównania.

Porównanie typów pamięci:

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'

Wynik:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   560 KB

Typ danych geograficznych zajmuje o 30% więcej miejsca.

Ponadto typ danych geograficznych nie ogranicza się tylko do przechowywania punktu, można również przechowywać ciąg liniowy, ciąg kołowy, krzywa złożona, wielokąt, krzywaPolygon, zbiór geometryczny, wielopunktowy, ciąg wieloliniowy i wielobok i nie tylko . Każda próba zapisania nawet najprostszych typów geograficznych (takich jak szerokość / długość) poza punktem (na przykład instancja LINESTRING (1 1, 2 2)) spowoduje naliczenie dodatkowych wierszy dla każdego punktu, kolumny do sekwencjonowania dla kolejności każdego punktu i kolejna kolumna do grupowania linii. SQL Server ma również metody dla typów danych geograficznych, które obejmują obliczanie powierzchni , granicy, długości, odległości i innych .

Przechowywanie szerokości i długości geograficznej jako liczby dziesiętnej na serwerze Sql wydaje się nierozsądne.

Zaktualizuj 2

Jeśli planujesz wykonać jakiekolwiek obliczenia, takie jak odległość, powierzchnia itp., Prawidłowe obliczenie ich na powierzchni ziemi jest trudne. Każdy typ geograficzny przechowywany w programie SQL Server jest również przechowywany z identyfikatorem odniesienia przestrzennego . Te identyfikatory mogą pochodzić z różnych sfer (ziemia to 4326). Oznacza to, że obliczenia w programie SQL Server będą faktycznie obliczane poprawnie na powierzchni ziemi (zamiast w locie w locie, które mogłyby przebiegać przez powierzchnię ziemi).

wprowadź opis obrazu tutaj

Erik Philips
źródło
1
Aby dodać do tych informacji, użycie Geografia naprawdę rozszerza możliwości wyszukiwania sql od szerokości / długości między innymi szerokościami / długościami (zwykle tylko prostokąty), ponieważ typ danych Geografia umożliwia tworzenie wielu regionów o niemal dowolnym rozmiarze i kształcie.
Erik Philips,
1
dzięki jeszcze raz. Poprosiłem o powody, dla których warto rozważyć użycie, geographya ty podałeś kilka dobrych. Ostatecznie zdecydowałem się po prostu użyć decimalpól w tym przypadku (zobacz moją rozwlekłą aktualizację), ale dobrze jest wiedzieć, że mogę użyć, geographyjeśli kiedykolwiek będę musiał zrobić coś bardziej wyszukanego niż po prostu odwzorować współrzędne.
Jeff Ogata,
6

Inną rzeczą do rozważenia jest miejsce zajmowane przez każdą metodę. Typ geograficzny jest przechowywany jako VARBINARY(MAX). Spróbuj uruchomić ten skrypt:

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'

Wynik:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   400 KB

Typ danych geograficznych zajmuje prawie dwa razy więcej miejsca.

Noel Abrahams
źródło
2
Uwaga dotycząca spacji, każda współrzędna jest przechowywana jako liczba zmiennoprzecinkowa podwójnej precyzji, która ma 64 bity (8 bajtów), a 8-bajtowa wartość binarna jest w przybliżeniu równoważna 15 cyfrom dokładności dziesiętnej , więc porównanie liczby dziesiętnej (9 , 6), który ma tylko 5 bajtów , nie jest dokładnym porównaniem. Decimal musiałby mieć co najmniej Decimal (15,12) (9 bajtów) dla każdego LatLong (łącznie 18 bajtów) dla rzeczywistego porównania.
Erik Philips,
9
@ErikPhilips chodzi o to, po co używać ułamka dziesiętnego (15, 12), skoro wystarczy ułamek dziesiętny (9, 6)? Powyższe porównanie ma charakter praktyczny, a nie akademicki.
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
źródło
2
Nowe odpowiedzi są zawsze mile widziane, ale proszę dodać kontekst. Krótkie wyjaśnienie, w jaki sposób powyższe rozwiązuje problem, sprawia, że ​​odpowiedź jest bardziej użyteczna dla innych.
Leigh