¿Qué tipo de datos MySQL debe usarse para Latitud / Longitud con 8 decimales?

257

Estoy trabajando con datos de mapas, y se Latitude/Longitudeextiende a 8 decimales. Por ejemplo:

Latitude 40.71727401
Longitude -74.00898606

Vi en el documento de Google que usa:

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

sin embargo, sus lugares decimales solo van a 6.
¿Debo usar FLOAT(10, 8)o hay otro método a considerar para almacenar estos datos para que sea preciso? Se usará con los cálculos del mapa. ¡Gracias!

Eduardo
fuente
44
¿Realmente necesita almacenar valores en la superficie de la tierra con una precisión de 1.1 mm ? Si es así, ¿por qué está almacenando valores en latlng en primer lugar?
ovangle
2
¡El documento de Google está EQUIVOCADO! No use el floattipo, que solo tiene 7 dígitos de precisión. Necesita al menos 9. No necesita 10: los documentos por alguna extraña razón cuentan el signo menos como un dígito. Hacer ya sea: double(9,6)o decimal(9,6).
Ariel
55
¿Cuánta precisión necesitas realmente ? 6 decimales le dan la precisión suficiente para distinguir a dos personas que se besan. 8 puede distinguir tus dedos. FLOATdistingue dos elementos separados 1.7m (5.6ft). ¡Todos estos son ridículamente excesivos para las aplicaciones de "mapas"!
Rick James

Respuestas:

594

DECIMAL es el tipo de datos MySQL para la aritmética exacta. A diferencia de FLOAT, su precisión se fija para cualquier tamaño de número, por lo que al usarlo en lugar de FLOAT puede evitar errores de precisión al hacer algunos cálculos. Si solo estuviera almacenando y recuperando los números sin cálculo, en la práctica FLOAT sería seguro, aunque no hay ningún daño en usar DECIMAL. Con los cálculos, FLOAT todavía está bien, pero para estar absolutamente seguro de 8d.p. precisión debe usar DECIMAL.

Las latitudes varían de -90 a +90 (grados), por lo que DECIMAL (10, 8) está bien para eso, pero las longitudes varían de -180 a +180 (grados), por lo que necesita DECIMAL (11, 8). El primer número es el número total de dígitos almacenados, y el segundo es el número después del punto decimal.

En breve: lat DECIMAL(10, 8) NOT NULL, lng DECIMAL(11, 8) NOT NULL

Esto explica cómo funciona MySQL con los tipos de datos de punto flotante.

ACTUALIZACIÓN: MySQL admite tipos de datos espaciales y Pointes un tipo de valor único que se puede utilizar. Ejemplo:

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
fuente
11
Quizás mi respuesta hizo un mal uso de la palabra exacta, ya que DECIMAL sigue siendo tan precisa como la precisión que le das. Mi punto era que es así de preciso. Por supuesto, algunos cálculos expanden el error. Si tengo un DECMIAL x entonces sin (x ^ 100) va a estar muy lejos. Pero si (usando DECIMAL (10, 8) o FLOAT (10, 8)) calculo 0.3 / 3 entonces DECIMAL da 0.100000000000 (correcto), y float da 0.100000003974 (correcto a 8dp, pero sería incorrecto si se multiplicara). Entiendo que la principal diferencia está en cómo se almacenan los números. DECIMAL almacena los dígitos decimales, donde FLOAT almacena la aproximación binaria.
gandaliter
1
Por la duda de la precisión, voy a DOBLE.
Ratata Tata
1
8 decimales son 1,1 mm (menos de 1/16 de pulgada) de precisión. ¿Por qué necesitarías eso para latitud y longitud?
vartec
1
Facebook parece usar hasta 12 decimales para lat y 13 para lng. vartec escribió que 8 decimales es igual a 1.1 mm; ¿Qué hay de 7 y 6? (No soy bueno en matemáticas). Estoy usando el doble por ahora, pero me gustaría comprobar si podría ganar en los cálculos de distancia cambiando el tipo. Gracias.
Alain Zelink
44
Las respuestas a esta pregunta ( gis.stackexchange.com/questions/8650/… ) brindan información sobre la precisión que obtiene con diferentes números de lugares decimales de latitud y longitud.
gandaliter
16

Además, verá que los floatvalores son redondeados.

// por ejemplo: valores dados 41.0473112,29.0077011

flotador (11,7) | decimal (11,7)
---------------------------
41.0473099 | 41.0473112
29.0077019 | 29.0077011

K-Gun
fuente
1
Puede usar el doubletipo de datos, que tiene la precisión necesaria.
Ariel
1
Muéstrame un mapa útil que pueda distinguir esos dos puntos. Afirmo que ambas representaciones son "innecesariamente precisas".
Rick James
14

en laravel utiliza el tipo de columna decimal para la migración

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

para más información ver tipo de columna disponible

Jignesh Joisar
fuente
7

Puede establecer su tipo de datos como entero con signo. Cuando almacena coordenadas en SQL, puede establecerlas como lat * 10000000 y long * 10000000. Y cuando selecciona con distancia / radio, dividirá las coordenadas de almacenamiento en 10000000. Lo probé con 300K filas, el tiempo de respuesta de la consulta es bueno. (2 x 2.67GHz CPU, 2 GB de RAM, MySQL 5.5.49)

Oğuzhan KURNUÇ
fuente
¿Cual es mas rápido? ¿Haciendo esto o usando flotante o decimal?
Dinidiniz
1
@Dinidiniz: la diferencia de velocidad es muy pequeña. Obtener filas abruma el tiempo de cualquier acción de la base de datos.
Rick James
¿Por qué 10000000? ¿Qué sucede si contiene más de 6 dígitos después del valor decimal? O siempre devolverá 6 puntos decimales.
Mahbub Morshed
@MahbubMorshed - quieres decir 7 dígitos - se muestran 7 dígitos cero. Pero sí, esta técnica siempre almacena exactamente 7 dígitos, no más. (Si usa un entero de 4 bytes, no puede aumentar el multiplicador más allá de 7 dígitos porque el valor de longitud puede ser tan grande como 180, y debe evitar el desbordamiento del número entero con signo máximo). Esto es 2 dígitos más precisos que almacenar en flotante de precisión simple, que solo tiene aproximadamente 5 dígitos a la derecha del punto decimal en valores de longitud grandes. (179.99998 y 179.99997 pueden almacenarse con el mismo valor flotante; 179.99996 está lejos de 179.99998).)
ToolmakerSteve
Esta es la mejor compensación que he visto en ningún lado. Aquí muestro el código para usar y para confirmar que proporciona 7 dígitos después del punto decimal, en un int con signo de 4 bytes, para valores largos / lat (por lo tanto, dentro del rango -180 .. + 180). Gran precisión (~ 1 cm) en un tamaño pequeño (4B).
ToolmakerSteve
6

No use flotador ... Redondeará sus coordenadas, dando como resultado algunos sucesos extraños.

Usar decimal

Sam sabey
fuente
4

Creo que la mejor manera de almacenar Lat / Lng en MySQL es tener una columna POINT (tipo de datos 2D) con un índice ESPACIAL.

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'
fuente
0

Usando migrate ruby ​​on rails

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
fuente
¿Esto no limitará las longitudes a -99..99? ¡Esto excluye gran parte del Pacífico!
Rick James
Ese es un ejemplo que no debe tomarse como verdad absoluta. Puede usar otra precisión decimal DECIMAL (20, 18) y así sucesivamente ... Si necesita guardar datos geográficos y espaciales, puede usar la base de datos postgis para este propósito. Las extensiones espaciales de MySQL son una buena alternativa porque siguen el modelo de geometría OpenGIS. No los usé porque necesitaba mantener mi base de datos portátil. postgis.net
gilcierweb
(20,18)también supera en +/- 99.
Rick James
Ese es un ejemplo que no debe tomarse como verdad absoluta. Puede usar otra precisión decimal DECIMAL (20, 18) y así sucesivamente ... Si necesita guardar datos geográficos y espaciales, puede usar la base de datos postgis para este propósito. Las extensiones espaciales de MySQL son una buena alternativa porque siguen el modelo de geometría OpenGIS. No los utilicé porque necesitaba mantener mi base de datos portátil. postgis.net
gilcierweb
Tio esto es sólo un ejemplo, puede utilizar la precisión que desee, si decimal no está ayudando a utilizar una base de datos PostGIS hecho sólo para los datos geográficos y espaciales
gilcierweb
-1

Código para usar / probar la precisión de la respuesta de Oğuzhan KURNUÇ .

RESUMEN:
Gran precisión (~ 1 cm) en un tamaño pequeño (4B).

La precisión es (muy cercana a) 7 dígitos decimales para valores superiores al rango [-180, 180].
Eso es 7 dígitos a la derecha del decimal (~ 1 cm) , para un total de 9 dígitos (o 10 dígitos, si se cuenta el "1" inicial de "180") cerca de + -180.
Contraste esto con un flotante de 4 bytes , que tiene solo ~ 7 dígitos en total, por lo que ~ 5 dígitos a la derecha del decimal cerca de + = 180 (~ 1m) .

Métodos para usar este enfoque:

const double Fixed7Mult = 10000000;

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

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

Pruebas de precisión:

/// <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}");
}

Métodos auxiliares utilizados por las pruebas:

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
fuente