Jakiego typu danych MySQL należy użyć dla szerokości i długości geograficznej z 8 miejscami po przecinku?

257

Pracuję z danymi map i Latitude/Longitudeobejmuje 8 miejsc po przecinku. Na przykład:

Latitude 40.71727401
Longitude -74.00898606

Widziałem w dokumencie Google, który używa:

lat FLOAT( 10, 6 ) NOT NULL,  
lng FLOAT( 10, 6 ) NOT NULL

Jednak ich miejsca dziesiętne wynoszą tylko 6.
Powinienem użyć FLOAT(10, 8)lub istnieje inna metoda do rozważenia, aby przechowywać te dane, więc są one dokładne. Będzie używany z obliczeniami mapy. Dzięki!

Edward
źródło
4
Czy naprawdę musisz przechowywać wartości na powierzchni ziemi z dokładnością do 1,1 mm ? Jeśli tak, to dlaczego przede wszystkim przechowujesz wartości w formacie latlng?
ovangle
2
Dokument Google jest NIEPRAWIDŁOWY! Nie używaj tego floattypu - ma on tylko 7 cyfr dokładności. Potrzebujesz co najmniej 9. Nie potrzebujesz 10 - dokumenty z jakiegoś dziwnego powodu liczą znak minus jako cyfrę. Wykonaj albo: double(9,6)albo decimal(9,6).
Ariel
5
Ile naprawdę potrzebujesz precyzji ? 6 miejsc po przecinku daje wystarczającą precyzję, aby rozróżnić dwie osoby, które się całują. 8 może rozdzielić palce. FLOATrozróżnia dwa elementy w odległości 1,7 m (5,6 stopy) Wszystkie są absurdalnie przesadne jak na aplikacje „mapujące”!
Rick James

Odpowiedzi:

594

DECIMAL to typ danych MySQL dla dokładnej arytmetyki. W przeciwieństwie do FLOAT, jego precyzja jest ustalona dla dowolnego rozmiaru liczby, więc używając go zamiast FLOAT można uniknąć błędów precyzji podczas wykonywania niektórych obliczeń. Jeśli tylko przechowywałeś i pobierałeś liczby bez obliczeń, w praktyce FLOAT byłby bezpieczny, chociaż korzystanie z DECIMAL nie szkodzi. Z obliczeń FLOAT nadal jest w większości w porządku, ale dla absolutnej pewności co do 8d.p. precyzji należy użyć DECIMAL.

Szerokość geograficzna wynosi od -90 do +90 (stopni), więc DECIMAL (10, 8) jest do tego odpowiedni, ale długości geograficzne wynoszą od -180 do +180 (stopni), więc potrzebujesz DECIMAL (11, 8). Pierwsza liczba to całkowita liczba zapisanych cyfr, a druga to liczba po przecinku.

W skrócie: lat DECIMAL(10, 8) NOT NULL, lng DECIMAL(11, 8) NOT NULL

To wyjaśnia, jak MySQL działa z zmiennoprzecinkowymi typami danych.

AKTUALIZACJA: MySQL obsługuje typy danych przestrzennych i Pointjest typem pojedynczej wartości, którego można używać. Przykład:

CREATE TABLE `buildings` (
  `coordinate` POINT NOT NULL,
  /* Even from v5.7.5 you can define an index for it */
  SPATIAL INDEX `SPATIAL` (`coordinate`)
) ENGINE=InnoDB;

/* then for insertion you can */
INSERT INTO `buildings` 
(`coordinate`) 
VALUES
(POINT(40.71727401 -74.00898606));
gandaliter
źródło
11
Być może moja odpowiedź niewłaściwie wykorzystała słowo „dokładny”, ponieważ DECIMAL jest wciąż tak dokładny, jak precyzja, którą mu podajesz. Chodziło mi o to, że jest tak dokładne. Oczywiście niektóre obliczenia rozszerzają błąd. Jeśli mam DECMIAL x, sin (x ^ 100) będzie daleko. Ale jeśli (używając DECIMAL (10, 8) lub FLOAT (10, 8)) obliczę 0,3 / 3, to DECIMAL daje 0,100000000000 (poprawnie), a float daje 0,100000003974 (poprawne do 8dp, ale byłoby błędne, gdyby zostało pomnożone). Rozumiem, że główna różnica polega na sposobie przechowywania liczb. DECIMAL przechowuje cyfry dziesiętne, gdzie FLOAT przechowuje przybliżenie binarne.
gandaliter
1
Wątpię w precyzję, podwoję się.
Ratata Tata
1
8 miejsc po przecinku to precyzja 1,1 mm (mniej niż 1/16 cala). Dlaczego miałbyś tego potrzebować do szerokości i długości geograficznej?
vartec
1
Facebook wydaje się używać do 12 miejsc po przecinku dla lat i 13 dla lng. vartec napisał, że 8 miejsc po przecinku jest równe 1,1 mm; co z 7 i 6? (Nie jestem dobry z matematyki). Na razie używam podwójnego, ale chciałbym sprawdzić, czy mogę zyskać na obliczeniach odległości, zmieniając typ. Dziękuję Ci.
Alain Zelink
4
Odpowiedzi na to pytanie ( gis.stackexchange.com/questions/8650/... ) zawierają informacje o precyzji, jaką uzyskuje się przy różnej liczbie miejsc dziesiętnych szerokości i długości geograficznej.
gandaliter
16

Dodatkowo zobaczysz, że floatwartości są zaokrąglone.

// np .: podane wartości 41.0473112,29.0077011

pływak (11,7) | dziesiętny (11,7)
---------------------------
41,0473099 | 41,0473112
29,0077019 | 29,0077011

K-Gun
źródło
1
Możesz użyć doubletypu danych, który ma wymaganą precyzję.
Ariel
1
Pokaż mi przydatną mapę, która potrafi rozróżnić te dwa punkty. Twierdzę, że obie reprezentacje są „niepotrzebnie precyzyjne”.
Rick James
14

w laravel wykorzystano dziesiętny typ kolumny do migracji

$table->decimal('latitude', 10, 8);
$table->decimal('longitude', 11, 8);

Aby uzyskać więcej informacji, zobacz dostępny typ kolumny

Jignesh Joisar
źródło
7

Możesz ustawić swój typ danych jako liczbę całkowitą ze znakiem. Podczas przechowywania współrzędnych do SQL można ustawić jako lat * 10000000 i długi * 10000000. A kiedy wybierasz z odległością / promieniem, podzielisz współrzędne miejsca przechowywania na 10000000. Przetestowałem to z 300 000 wierszy, czas odpowiedzi na zapytanie jest dobry. (2 x 2,67 GHz, 2 GB pamięci RAM, MySQL 5.5.49)

Oğuzhan KURNUÇ
źródło
Który jest szybszy? Robisz to lub używasz liczb zmiennoprzecinkowych lub dziesiętnych?
Dinidiniz
1
@Dinidiniz - Różnica prędkości jest bardzo mała. Pobieranie wierszy przytłacza czas każdej akcji bazy danych.
Rick James
Dlaczego 10000000? Co się stanie, jeśli po wartości dziesiętnej zawiera więcej niż 6 cyfr? Czy zawsze zwróci 6 miejsc po przecinku.
Mahbub Morshed
@MahbubMorshed - masz na myśli 7 cyfr - pokazanych jest 7 cyfr zerowych. Ale tak, ta technika zawsze przechowuje dokładnie 7 cyfr, nigdy więcej. (Jeśli używasz 4-bajtowej liczby całkowitej, nie możesz zwiększyć mnożnika poza 7 cyfr, ponieważ wartość długości geograficznej może być tak duża, jak 180, i musisz unikać przepełnienia maksymalnej liczby całkowitej ze znakiem.) Są to 2 cyfry bardziej precyzyjne niż przechowywanie w liczbach zmiennoprzecinkowych o pojedynczej precyzji, który ma tylko około 5 cyfr po prawej stronie przecinka dziesiętnego przy dużych długościach geograficznych. (179,99998 i 179,99997 mogą przechowywać tę samą wartość zmienną; 179,99996 jest bezpiecznie daleki od 179,99998).)
ToolmakerSteve
To najlepszy kompromis, jaki kiedykolwiek widziałem. Tutaj pokazuję kod do użycia i potwierdzam , że podaje on 7 cyfr po przecinku, w 4-bajtowym znaku int, dla wartości długich / lat (więc w zakresie -180 .. + 180). Wielka precyzja (~ 1 cm) w małym rozmiarze (4B).
ToolmakerSteve
6

Nie używaj pływaka ... Zaokrągli twoje współrzędne, powodując dziwne zdarzenia.

Użyj dziesiętnego

Sam Sabey
źródło
4

Uważam, że najlepszym sposobem przechowywania Lat / Lng w MySQL jest posiadanie kolumny POINT (typ danych 2D) z indeksem SPATIAL.

CREATE TABLE `cities` (
  `zip` varchar(8) NOT NULL,
  `country` varchar (2) GENERATED ALWAYS AS (SUBSTRING(`zip`, 1, 2)) STORED,
  `city` varchar(30) NOT NULL,
  `centre` point NOT NULL,
  PRIMARY KEY (`zip`),
  KEY `country` (`country`),
  KEY `city` (`city`),
  SPATIAL KEY `centre` (`centre`)
) ENGINE=InnoDB;


INSERT INTO `cities` (`zip`, `city`, `centre`) VALUES
('CZ-10000', 'Prague', POINT(50.0755381, 14.4378005));
ΔO „deltazero”
źródło
0

Korzystanie z migracji ruby ​​na szynach

class CreateNeighborhoods < ActiveRecord::Migration[5.0]
  def change
    create_table :neighborhoods do |t|
      t.string :name
      t.decimal :latitude, precision: 15, scale: 13
      t.decimal :longitude, precision: 15, scale: 13
      t.references :country, foreign_key: true
      t.references :state, foreign_key: true
      t.references :city, foreign_key: true

      t.timestamps
    end
  end
end
gilcierweb
źródło
Czy to nie ograniczy długości geograficznej do -99..99? To wyklucza znaczną część Pacyfiku!
Rick James
To przykład, którego nie należy traktować jako absolutnej prawdy. Możesz użyć innej dokładności dziesiętnej (20, 18) i tak dalej ... Jeśli chcesz zapisać dane geograficzne i przestrzenne, możesz w tym celu skorzystać z bazy danych Postgis. Rozszerzenia przestrzenne MySQL są dobrą alternatywą, ponieważ są zgodne z modelem geometrycznym OpenGIS. Nie korzystałem z nich, ponieważ musiałem mieć przenośną bazę danych. postgis.net
gilcierweb
(20,18)również kończy się na +/- 99.
Rick James
To przykład, którego nie należy traktować jako absolutnej prawdy. Możesz użyć innej dokładności dziesiętnej (20, 18) i tak dalej ... Jeśli chcesz zapisać dane geograficzne i przestrzenne, możesz w tym celu skorzystać z bazy danych Postgis. Rozszerzenia przestrzenne MySQL są dobrą alternatywą, ponieważ są zgodne z modelem geometrycznym OpenGIS. Nie korzystałem z nich, ponieważ musiałem mieć przenośną bazę danych. postgis.net
gilcierweb
Koleś, to tylko przykład, możesz użyć precyzji, którą chcesz, jeśli liczba dziesiętna nie pomaga w korzystaniu z postgis bazy danych stworzonej tylko dla danych geograficznych i przestrzennych
gilcierweb
-1

Kod używający / potwierdzający precyzję odpowiedzi Oğuzhan KURNUÇ .

PODSUMOWANIE:
Wielka precyzja (~ 1 cm) w małym rozmiarze (4B).

Dokładność wynosi (bardzo blisko) 7 cyfr dziesiętnych dla wartości z zakresu [-180, 180].
To 7 cyfr na prawo od miejsca po przecinku (~ 1 cm) , w sumie 9 cyfr (lub 10 cyfr, licząc początkowe „1” z „180”) w pobliżu + -180.
Porównaj to z 4-bajtową liczbą zmiennoprzecinkową , która ma tylko ~ 7 cyfr łącznie, więc ~ 5 cyfr po prawej stronie dziesiętnej w pobliżu + = 180 (~ 1m) .

Metody zastosowania tego podejścia:

const double Fixed7Mult = 10000000;

public static int DecimalDegreesToFixed7(double degrees)
{
    return RoundToInt(degrees * Fixed7Mult);
}

public static double Fixed7ToDecimalDegrees(int fixed7)
{
    return fixed7 / (double)Fixed7Mult;
}

Testy precyzji:

/// <summary>
/// This test barely fails in 7th digit to right of decimal point (0.0000001 as delta).
/// Passes with 0.0000002 as delta.
/// </summary>
internal static void TEST2A_LatLongPrecision()
{
    //VERY_SLOW_TEST Test2A_ForRange(-180, 360, 0.0000001);
    //FAILS Test2A_ForRange(-180, 0.1, 0.0000001);

    Test2A_ForRange(-180, 0.1, 0.0000002);
    Test2A_ForRange(0, 0.1, 0.0000002);
    Test2A_ForRange(179.9, 0.1, 0.0000002);
}

/// <summary>
/// Test for the smallest difference.  A: 9.9999994E-08.
/// </summary>
internal static void TEST2B_LatLongPrecision()
{
    double minDelta = double.MaxValue;
    double vAtMinDelta = 0;
    //VERY_SLOW_TEST Test2B_ForRange(-180, 360, ref minDelta, ref vAtMinDelta);
    Test2B_ForRange(-180, 0.1, ref minDelta, ref vAtMinDelta);
    Test2B_ForRange(0, 0.1, ref minDelta, ref vAtMinDelta);
    Test2B_ForRange(179.9, 0.1, ref minDelta, ref vAtMinDelta);

    // Fails. Smallest delta is 9.9999994E-08; due to slight rounding error in 7th decimal digit.
    //if (minDelta < 0.0000001)
    //  throw new InvalidProgramException($"Fixed7 has less than 7 decimal digits near {vAtMinDelta}");

    // Passes.
    if (minDelta < 0.000000099)
        throw new InvalidProgramException($"Fixed7 has less than 7 decimal digits near {vAtMinDelta}");
}

Metody pomocnicze stosowane w testach:

private static void Test2A_ForRange(double minV, double range, double deltaV)
{
    double prevV = 0;
    int prevFixed7 = 0;
    bool firstTime = true;
    double maxV = minV + range;
    for (double v = minV; v <= maxV; v += deltaV) {
        int fixed7 = DecimalDegreesToFixed7(v);
        if (firstTime)
            firstTime = false;
        else {
            // Check for failure to distinguish two values that differ only in 7th decimal digit.
            // Fails.
            if (fixed7 == prevFixed7)
                throw new InvalidProgramException($"Fixed7 doesn't distinguish between {prevV} and {v}");
        }
        prevV = v;
        prevFixed7 = fixed7;
    }
}

private static void Test2B_ForRange(double minV, double range, ref double minDelta, ref double vAtMinDelta)
{
    int minFixed7 = DecimalDegreesToFixed7(minV);
    int maxFixed7 = DecimalDegreesToFixed7(minV + range);

    bool firstTime = true;
    double prevV = 0;   // Initial value is ignored.
    for (int fixed7 = minFixed7; fixed7 < maxFixed7; fixed7++) {
        double v = Fixed7ToDecimalDegrees(fixed7);
        if (firstTime)
            firstTime = false;
        else {
            double delta = Math.Abs(v - prevV);
            if (delta < minDelta) {
                minDelta = delta;
                vAtMinDelta = v;
            }
        }
        prevV = v;
    }
}
ToolmakerSteve
źródło