Pourquoi utiliser le type de données de géographie SQL Server 2008?

105

Je suis en train de refondre une base de données clients et l'une des nouvelles informations que je souhaite stocker avec les champs d'adresse standard (rue, ville, etc.) est l'emplacement géographique de l'adresse. Le seul cas d'utilisation que j'ai à l'esprit est de permettre aux utilisateurs de cartographier les coordonnées sur Google Maps lorsque l'adresse ne peut pas être trouvée autrement, ce qui se produit souvent lorsque la zone est nouvellement développée ou se trouve dans un endroit éloigné / rural.

Ma première inclination était de stocker la latitude et la longitude sous forme de valeurs décimales, mais je me suis ensuite souvenu que SQL Server 2008 R2 avait un geographytype de données. Je n'ai absolument aucune expérience d'utilisation geography, et d'après mes recherches initiales, cela semble excessif pour mon scénario.

Par exemple, pour travailler avec la latitude et la longitude stockées sous decimal(7,4), je peux faire ceci:

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

mais avec geography, je ferais ceci:

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

Bien que ce ne soit pas ça beaucoup plus compliqué, pourquoi la complexité add si je n'ai pas?

Avant d'abandonner l'idée d'utiliser geography, y a-t-il quelque chose que je devrais considérer? Serait-il plus rapide de rechercher un emplacement en utilisant un index spatial plutôt qu'en indexant les champs Latitude et Longitude? Y a-t-il des avantages à utiliser geographyque je ne connais pas? Ou, d'un autre côté, y a-t-il des mises en garde que je devrais savoir et qui me décourageraient d'utiliser geography?


Mettre à jour

@Erik Philips a évoqué la possibilité d'effectuer des recherches de proximité avec geography , ce qui est très cool.

D'autre part, un test rapide montre qu'un simple selectpour obtenir la latitude et la longitude est beaucoup plus lent lors de l'utilisation geography(détails ci-dessous). , et un commentaire sur la réponse acceptée à une autre question SO sur geographyme fait méfier:

@SaphuA De rien. En guise de remarque, soyez TRES prudent d'utiliser un index spatial sur une colonne de type de données GEOGRAPHY Nullable. Il y a un problème de performances sérieux, alors rendez cette colonne GEOGRAPHY non nullable même si vous devez remodeler votre schéma. - Tomas 18 juin à 11:18

Dans l'ensemble, en pesant la probabilité de faire des recherches de proximité par rapport au compromis entre les performances et la complexité, j'ai décidé de renoncer à l'utilisation de geographydans ce cas.


Détails du test que j'ai effectué:

J'ai créé deux tableaux, l'un utilisant geographyet l'autre utilisant decimal(9,6)pour la latitude et la 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)
) 

et inséré une seule ligne en utilisant les mêmes valeurs de latitude et de longitude dans chaque tableau:

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

Enfin, l'exécution du code suivant montre que, sur ma machine, la sélection de la latitude et de la longitude est environ 5 fois plus lente lors de l'utilisation 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

Résultats:

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

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

Ce qui était plus surprenant, c'est que même lorsqu'aucune ligne n'est sélectionnée, par exemple sélectionner où RowId = 2, qui n'existe pas, geographyétait encore plus lent:

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

AvgGeographyDuration AvgLatLongDuration
-------------------- ------------------
1608                 947
Jeff Ogata
la source
4
Je pense faire les deux, enregistrer le Lat et Lon dans leurs propres colonnes et avoir une autre colonne pour un objet Geography, donc si j'ai juste besoin de Lat / Lon, je les prends dans les colonnes, et si j'ai besoin d'une recherche de proximité, je J'utiliserai la géographie. Est-ce sage? Y a-t-il des inconvénients (à part cela prend plus de place ...)?
Yuval A.
@YuvalA. cela semble certainement raisonnable et peut être un bon compromis. La seule préoccupation que j'ai par tête est de savoir si la colonne Géographie dans le tableau a un impact sur les requêtes sur la table - je n'ai aucune expérience avec cela, vous auriez donc besoin de tester pour vérifier.
Jeff Ogata
1
Pourquoi avez-vous continué à mettre à jour votre question avec de nouvelles questions au lieu de poser de nouvelles questions?
Chad
@Chad ne sais pas ce que tu veux dire. J'ai mis à jour le corps de la question une fois, et ce n'était pas pour poser plus de questions.
Jeff Ogata
6
Il convient de noter, maintenant, pour ceux qui trouvent cette question, que SQL Server 2012 inclut des améliorations significatives des performances avec l'indexation spatiale. Il convient également de noter que tant que vous stockez des informations de localisation, vous pouvez ajouter des informations spatiales plus tard à l'aide d'un service de recherche pour géocoder vos adresses déjà stockées.
Volvox

Réponses:

66

Si vous prévoyez d'effectuer un calcul spatial, EF 5.0 autorise les expressions LINQ telles que:

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

Ensuite, il y a une très bonne raison d'utiliser la géographie.

Explication du spatial dans Entity Framework .

Mis à jour avec création de bases de données spatiales hautes performances

Comme je l'ai noté sur la réponse de Noel Abraham :

Une note sur l'espace, chaque coordonnée est stockée sous forme de nombre à virgule flottante double précision d'une longueur de 64 bits (8 octets) et une valeur binaire de 8 octets équivaut à peu près à 15 chiffres de précision décimale, comparant ainsi un nombre décimal (9 , 6) qui ne fait que 5 octets, n'est pas exactement une comparaison juste. Decimal devrait être un minimum de Decimal (15,12) (9 octets) pour chaque LatLong (total de 18 octets) pour une comparaison réelle.

Donc, comparer les types de stockage:

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'

Résultat:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   560 KB

Le type de données géographie occupe 30% d'espace en plus.

De plus, le type de données geography ne se limite pas au stockage d'un point, vous pouvez également stocker LineString, CircularString, CompoundCurve, Polygon, CurvePolygon, GeometryCollection, MultiPoint, MultiLineString et MultiPolygon et plus encore . Toute tentative de stocker même le plus simple des types de géographie (comme Lat / Long) au-delà d'un point (par exemple, LINESTRING (1 1, 2 2) instance) entraînera des lignes supplémentaires pour chaque point, une colonne pour le séquençage pour l'ordre de chaque point et une autre colonne pour le regroupement des lignes. SQL Server propose également des méthodes pour les types de données Géographie qui incluent le calcul de la zone, de la limite, de la longueur, des distances, etc. etc.

Il semble imprudent de stocker la latitude et la longitude sous forme décimale dans Sql Server.

Mise à jour 2

Si vous prévoyez de faire des calculs tels que la distance, la superficie, etc., il est difficile de les calculer correctement sur la surface de la terre. Chaque type de géographie stocké dans SQL Server est également stocké avec un ID de référence spatiale . Ces identifiants peuvent être de différentes sphères (la terre est 4326). Cela signifie que les calculs dans SQL Server seront en fait calculés correctement sur la surface de la Terre (au lieu de voler à vol d'oiseau qui pourrait traverser la surface de la Terre).

entrez la description de l'image ici

Erik Philips
la source
1
Pour ajouter à ces informations, l'utilisation de la géographie étend vraiment la capacité des recherches SQL à partir d'un lat / long entre d'autres lat / longs (généralement juste des rectangles), car le type de données Geography vous permet de créer plusieurs régions de presque toutes les tailles et formes.
Erik Philips le
1
Merci encore. J'ai demandé des raisons d'envisager l'utilisation geographyet vous en avez fourni quelques-unes. En fin de compte, j'ai décidé de n'utiliser que des decimalchamps dans ce cas (voir ma longue mise à jour), mais il est bon de savoir que je peux l'utiliser geographysi jamais j'ai besoin de faire quelque chose de plus sophistiqué que de simplement cartographier les coordonnées.
Jeff Ogata le
6

Une autre chose à considérer est l'espace de stockage occupé par chaque méthode. Le type de géographie est stocké sous forme de fichier VARBINARY(MAX). Essayez d'exécuter ce 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'

Résultat:

name    rows    data     
Geo     20000   728 KB   
LatLon  20000   400 KB

Le type de données de géographie occupe presque deux fois plus d'espace.

Noel Abrahams
la source
2
Une note sur l'espace, chaque coordonnée est stockée sous forme de nombre à virgule flottante double précision d'une longueur de 64 bits (8 octets) et une valeur binaire de 8 octets équivaut à peu près à 15 chiffres de précision décimale , comparant ainsi un nombre décimal (9 , 6) qui ne fait que 5 octets , n'est pas exactement une comparaison juste. Decimal devrait être un minimum de Decimal (15,12) (9 octets) pour chaque LatLong (total de 18 octets) pour une comparaison réelle.
Erik Philips
9
@ErikPhilips le point est pourquoi utiliser un décimal (15, 12) alors que tout ce dont vous avez besoin est un décimal (9, 6)? La comparaison ci-dessus est pratique - pas un exercice académique.
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
la source
2
Les nouvelles réponses sont toujours les bienvenues, mais veuillez ajouter un peu de contexte. Expliquer brièvement comment ce qui précède résout le problème rend la réponse plus utile aux autres.
Leigh