← CC3088

Datos Geoespaciales

Semestre 01, 2026

El problema con coordenadas en columnas normales

Una app de delivery guarda la ubicación de sus restaurantes.

La tabla "obvia"


CREATE TABLE restaurantes (
    id      INT PRIMARY KEY,
    nombre  VARCHAR(200),
    lat     DECIMAL(9, 6),
    lng     DECIMAL(9, 6)
);
        

Funciona para almacenar. Pero las preguntas reales no son de almacenamiento.

Las preguntas reales

  • ¿Cuáles restaurantes están a menos de 3 km del usuario?
  • ¿Qué repartidores están dentro de la zona de cobertura?
  • ¿Cuál es la ruta más corta entre dos puntos?
  • ¿Este punto está dentro de este polígono (zona de despacho)?

Con columnas lat y lng separadas, cada consulta requiere cálculos trigonométricos complejos.

El cálculo sin extensión geoespacial


-- Distancia aproximada entre dos puntos (fórmula de Haversine)
SELECT nombre,
    6371 * 2 * ASIN(SQRT(
        POWER(SIN(RADIANS(lat - -14.6435) / 2), 2) +
        COS(RADIANS(-14.6435)) * COS(RADIANS(lat)) *
        POWER(SIN(RADIANS(lng - -90.5133) / 2), 2)
    )) AS distancia_km
FROM restaurantes
ORDER BY distancia_km
LIMIT 5;
        

Código propenso a errores, difícil de mantener y que no usa índices eficientemente.

Bases de datos geoespaciales

El motor relacional extendido con capacidades espaciales.

Tres extensiones clave

Tipos de dato

Para representar geometrías: puntos, líneas, polígonos.

Funciones

Para cálculos espaciales: distancia, área, intersección.

Índices

Optimizados para búsquedas espaciales.

Todo integrado en SQL estándar.

El estándar OGC / ISO

La industria usa el estándar Simple Features del Open Geospatial Consortium (OGC).

  • Define tipos de geometría y funciones con prefijo ST_ (Spatial Type).
  • PostgreSQL/PostGIS y MySQL implementan este estándar.
  • ST_Distance, ST_Contains, ST_Intersects — funcionan igual en ambos motores.

PostGIS

El motor geoespacial más completo y usado del ecosistema open source.

Soporta geometrías 2D, 3D, geografía esférica, raster y topología.


CREATE EXTENSION postgis;
                

Una sola línea habilita cientos de funciones.

Spatial integrado

Soporte geoespacial integrado desde la versión 5.7. No requiere extensión adicional.

Más limitado que PostGIS, pero suficiente para casos de uso comunes.

Tipos de geometría

Los tipos siguen el estándar WKT (Well-Known Text) para representarse.

POINT — Punto

La geometría más básica: una sola ubicación en el espacio.


-- Un punto en coordenadas (longitud, latitud)
POINT(-90.5133 14.6435)
                

LINESTRING — Línea

Una secuencia de puntos conectados que forman una línea.


-- Una ruta entre tres puntos
LINESTRING(
  -90.5133 14.6435,
  -90.5200 14.6500,
  -90.5300 14.6600
)
                

POLYGON — Polígono

Una región cerrada. El primer y último punto deben ser iguales para cerrar el anillo.


POLYGON((
  -90.50 14.64,
  -90.52 14.64,
  -90.52 14.66,
  -90.50 14.66,
  -90.50 14.64   -- igual al primero
))
        

Casos de uso: zona de cobertura de un restaurante, departamento, delimitación de un barrio.

Tipos compuestos

TipoDescripciónCaso de uso
MULTIPOINTColección de puntosSucursales de una empresa
MULTILINESTRINGColección de líneasRed de calles
MULTIPOLYGONColección de polígonosMunicipios de un departamento
GEOMETRYCOLLECTIONMezcla de geometríasDatos heterogéneos

SRID: Sistema de referencia de coordenadas

Cada geometría lleva asociado un SRID (Spatial Reference ID).

SRID 4326 — WGS 84

El más común: coordenadas geográficas (longitud, latitud). Es el sistema que usa GPS y Google Maps.


-- Punto en Guatemala City con SRID 4326
ST_SetSRID(ST_MakePoint(-90.5133, 14.6435), 4326)
        

Mezclar geometrías de distintos SRID produce resultados incorrectos.

Tablas con columnas geoespaciales

CREATE TABLE con geometría

PostgreSQL (PostGIS)


CREATE TABLE restaurantes (
    id       SERIAL PRIMARY KEY,
    nombre   VARCHAR(200) NOT NULL,
    telefono VARCHAR(20),
    ubicacion GEOMETRY(POINT, 4326) NOT NULL
);
                

GEOMETRY(POINT, 4326) — tipo punto, sistema WGS 84.

MySQL


CREATE TABLE restaurantes (
    id       INT AUTO_INCREMENT PRIMARY KEY,
    nombre   VARCHAR(200) NOT NULL,
    telefono VARCHAR(20),
    ubicacion POINT NOT NULL SRID 4326
) ENGINE=InnoDB;
                

Insertar un punto

PostgreSQL


INSERT INTO restaurantes (nombre, ubicacion)
VALUES (
    'Restaurante El Portal',
    ST_SetSRID(
        ST_MakePoint(-90.5133, 14.6435),
        4326
    )
);
                

MySQL


INSERT INTO restaurantes (nombre, ubicacion)
VALUES (
    'Restaurante El Portal',
    ST_GeomFromText(
        'POINT(-90.5133 14.6435)',
        4326
    )
);
                

Leer una geometría como texto


-- WKT: formato texto estándar
SELECT nombre, ST_AsText(ubicacion) AS coordenadas
FROM restaurantes;
-- POINT(-90.5133 14.6435)

-- GeoJSON: compatible con mapas web (Leaflet, Mapbox, Google Maps)
SELECT nombre, ST_AsGeoJSON(ubicacion) AS geojson
FROM restaurantes;
-- {"type":"Point","coordinates":[-90.5133,14.6435]}
        

Funciones de distancia

ST_Distance — distancia entre dos geometrías

Con GEOMETRY — devuelve grados


SELECT ST_Distance(
    ST_SetSRID(
        ST_MakePoint(-90.5133, 14.6435),
        4326
    ),
    ubicacion
) AS distancia  -- ¡grados, no metros!
FROM restaurantes;
                

Con GEOGRAPHY — devuelve metros reales


SELECT nombre,
    ST_Distance(
        ubicacion::geography,
        ST_SetSRID(
            ST_MakePoint(-90.5133, 14.6435),
            4326
        )::geography
    ) AS distancia_metros
FROM restaurantes
ORDER BY distancia_metros
LIMIT 5;
                

ST_DWithin — buscar dentro de un radio

La función más usada en aplicaciones de geolocalización.

PostGIS


-- Restaurantes a menos de 3 km del usuario
SELECT nombre
FROM restaurantes
WHERE ST_DWithin(
    ubicacion::geography,
    ST_SetSRID(
        ST_MakePoint(-90.5133, 14.6435),
        4326
    )::geography,
    3000  -- metros
);
                

MySQL


SELECT nombre,
    ST_Distance_Sphere(
        ubicacion,
        ST_GeomFromText(
            'POINT(-90.5133 14.6435)',
            4326
        )
    ) AS distancia_metros
FROM restaurantes
HAVING distancia_metros < 3000
ORDER BY distancia_metros;
                

Columnas numéricas vs columna GEOMETRY

Criteriolat/lng separadosColumna GEOMETRY
DistanciaFórmula de Haversine manualST_Distance / ST_DWithin
Índice espacialNo posible eficientementeÍndice GIST / R-Tree
Punto en polígonoAlgoritmo manualST_Contains
Estándar OGCNo

Funciones de relación espacial

ST_Contains — ¿el punto está dentro del polígono?


-- ¿La dirección del cliente está dentro de la zona de cobertura?
SELECT z.nombre AS zona
FROM zonas_cobertura z
WHERE ST_Contains(
    z.area,
    ST_SetSRID(ST_MakePoint(-90.5200, 14.6500), 4326)
);
        

Caso de uso: verificar si una dirección de entrega está dentro del área de despacho de un restaurante.

ST_Within


-- ¿El restaurante está dentro de la zona habilitada?
SELECT r.nombre
FROM restaurantes r, zonas_habilitadas z
WHERE ST_Within(r.ubicacion, z.area);
                

Inverso de ST_Contains.
ST_Within(A, B)ST_Contains(B, A).

ST_Intersects


-- ¿La ruta del repartidor pasa
-- por la zona de alta demanda?
SELECT r.nombre AS repartidor
FROM repartidores r, zonas_alta_demanda z
WHERE ST_Intersects(r.ruta_actual, z.area);
                

Detectar repartidores cuya ruta atraviesa una zona específica.

Tabla de relaciones espaciales

Función¿Qué verifica?
ST_Contains(A, B)A contiene completamente a B
ST_Within(A, B)A está completamente dentro de B
ST_Intersects(A, B)A y B tienen al menos un punto en común
ST_Overlaps(A, B)A y B se superponen parcialmente
ST_Touches(A, B)A y B se tocan solo en el borde
ST_Disjoint(A, B)A y B no tienen ningún punto en común

Funciones de geometría

ST_Buffer — zona de influencia

Crea un polígono alrededor de una geometría a una distancia dada.


-- Zona de cobertura de 2 km alrededor de un restaurante (PostGIS)
SELECT ST_Buffer(
    ubicacion::geography,
    2000  -- 2000 metros
)::geometry AS zona_cobertura
FROM restaurantes
WHERE id = 1;
        

Caso de uso: calcular y almacenar el área de cobertura de cada sucursal.

ST_Centroid

Punto central de un polígono.


SELECT nombre,
    ST_AsText(ST_Centroid(area))
    AS centro
FROM zonas;
                
ST_Area

Área de un polígono en m² (usar ::geography para m² reales).


SELECT nombre,
    ST_Area(area::geography)
    / 1000000 AS area_km2
FROM zonas
ORDER BY area_km2 DESC;
                
ST_Union

Une varias geometrías eliminando superposiciones.


SELECT ST_AsText(ST_Union(area))
    AS cobertura_total
FROM zonas_cobertura
WHERE cadena_id = 5;
                
ST_Length

Longitud de una línea en metros.


SELECT nombre,
    ST_Length(ruta::geography)
    AS longitud_metros
FROM rutas_entrega
ORDER BY longitud_metros;
                

Índices espaciales

Sin índice espacial, cada consulta recorre toda la tabla.

Crear el índice

PostGIS — índice GiST


CREATE INDEX idx_restaurantes_ubicacion
ON restaurantes
USING GIST (ubicacion);
                

GiST: Generalized Search Tree. El optimizador lo usa automáticamente.

MySQL — índice SPATIAL


-- La columna debe ser NOT NULL
CREATE SPATIAL INDEX idx_restaurantes_ubicacion
ON restaurantes (ubicacion);
                

¿Cómo funciona el índice espacial?

  • El índice GiST / R-Tree divide el espacio en rectángulos delimitadores (bounding boxes).
  • Para buscar puntos en un radio, primero filtra por bounding box — muy rápido.
  • Luego aplica el cálculo exacto solo a los candidatos.
Sin índice
1,000,000 filas revisadas
Con índice
~20 candidatos en el área

Verificar que el índice se usa


-- PostgreSQL: ver el plan de ejecución
EXPLAIN ANALYZE
SELECT nombre FROM restaurantes
WHERE ST_DWithin(
    ubicacion::geography,
    ST_SetSRID(ST_MakePoint(-90.5133, 14.6435), 4326)::geography,
    3000
);
-- Debe mostrar: Bitmap Index Scan on idx_restaurantes_ubicacion
        

Si el plan muestra Seq Scan en lugar de Index Scan, el índice no existe o las estadísticas están desactualizadas.

Seguimiento de vehículos

Monitoreo de flota en tiempo real.

Esquema


CREATE TABLE vehiculos (
    id        SERIAL PRIMARY KEY,
    placa     VARCHAR(20) NOT NULL,
    conductor VARCHAR(200)
);

CREATE TABLE posiciones (
    id          SERIAL PRIMARY KEY,
    vehiculo_id INT REFERENCES vehiculos(id),
    posicion    GEOMETRY(POINT, 4326) NOT NULL,
    velocidad   DECIMAL(5, 2),
    timestamp   TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_pos_ubic   ON posiciones USING GIST (posicion);
CREATE INDEX idx_pos_tiempo ON posiciones (timestamp);
        

¿Qué vehículos están en una zona restringida?


WITH zona AS (
    SELECT ST_GeomFromText(
        'POLYGON((-90.53 14.58, -90.52 14.58, -90.52 14.59, -90.53 14.59, -90.53 14.58))',
        4326
    ) AS area
)
SELECT v.placa, v.conductor, p.timestamp
FROM posiciones p
JOIN vehiculos v ON v.id = p.vehiculo_id
CROSS JOIN zona
WHERE ST_Within(p.posicion, zona.area)
  AND p.timestamp > NOW() - INTERVAL '5 minutes';
        

Distancia recorrida por un vehículo hoy


-- Construir la ruta del vehículo y calcular su longitud total
SELECT v.placa,
    ST_Length(
        ST_MakeLine(p.posicion ORDER BY p.timestamp)::geography
    ) AS metros_recorridos
FROM posiciones p
JOIN vehiculos v ON v.id = p.vehiculo_id
WHERE v.id = 7
  AND p.timestamp::date = CURRENT_DATE
GROUP BY v.id, v.placa;
        

ST_MakeLine une los puntos en orden cronológico y forma la ruta. ST_Length calcula su longitud en metros.

Análisis urbano

Una municipalidad analiza cobertura de servicios.

Colonias sin centro de salud a menos de 1 km


SELECT c.nombre AS colonia, c.zona
FROM colonias c
WHERE NOT EXISTS (
    SELECT 1
    FROM centros_salud cs
    WHERE ST_DWithin(
        cs.ubicacion::geography,
        ST_Centroid(c.perimetro)::geography,
        1000
    )
)
ORDER BY c.zona, c.nombre;
        

ST_Centroid calcula el centro geográfico de cada colonia para representarla como un punto.

Área total de cobertura de los centros de salud


-- Buffer de 1 km alrededor de cada centro
-- ST_Union elimina las superposiciones entre áreas
SELECT ST_Area(
    ST_Union(
        ST_Buffer(ubicacion::geography, 1000)::geometry
    )::geography
) / 1000000 AS km2_cubiertos
FROM centros_salud
WHERE tipo = 'hospital';
        

Geofencing

Disparar una acción cuando un objeto entra o sale de una zona.

Esquema


CREATE TABLE geofences (
    id          SERIAL PRIMARY KEY,
    nombre      VARCHAR(200),
    descripcion TEXT,
    area        GEOMETRY(POLYGON, 4326) NOT NULL
);

CREATE TABLE eventos_geofence (
    id          SERIAL PRIMARY KEY,
    vehiculo_id INT REFERENCES vehiculos(id),
    geofence_id INT REFERENCES geofences(id),
    tipo        VARCHAR(10) CHECK (tipo IN ('entrada', 'salida')),
    timestamp   TIMESTAMPTZ DEFAULT NOW()
);
        

Generar evento de entrada


-- Al recibir una nueva posición, verificar si está en alguna geofence
INSERT INTO eventos_geofence (vehiculo_id, geofence_id, tipo)
SELECT
    7 AS vehiculo_id,
    g.id AS geofence_id,
    'entrada' AS tipo
FROM geofences g
WHERE ST_Contains(
    g.area,
    ST_SetSRID(ST_MakePoint(-90.5200, 14.6500), 4326)
);
        

La lógica vive en la base de datos — el backend solo envía la nueva coordenada y la base registra el evento.