Semestre 01, 2026
Una app de delivery guarda la ubicación de sus restaurantes.
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.
Con columnas lat y lng separadas, cada consulta requiere cálculos trigonométricos complejos.
-- 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.
El motor relacional extendido con capacidades espaciales.
Para representar geometrías: puntos, líneas, polígonos.
Para cálculos espaciales: distancia, área, intersección.
Optimizados para búsquedas espaciales.
Todo integrado en SQL estándar.
La industria usa el estándar Simple Features del Open Geospatial Consortium (OGC).
ST_ (Spatial Type).ST_Distance, ST_Contains, ST_Intersects — funcionan igual en ambos motores.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.
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.
Los tipos siguen el estándar WKT (Well-Known Text) para representarse.
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)
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
)
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.
| Tipo | Descripción | Caso de uso |
|---|---|---|
MULTIPOINT | Colección de puntos | Sucursales de una empresa |
MULTILINESTRING | Colección de líneas | Red de calles |
MULTIPOLYGON | Colección de polígonos | Municipios de un departamento |
GEOMETRYCOLLECTION | Mezcla de geometrías | Datos heterogéneos |
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.
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;
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
)
);
-- 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]}
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;
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;
| Criterio | lat/lng separados | Columna GEOMETRY |
|---|---|---|
| Distancia | Fórmula de Haversine manual | ST_Distance / ST_DWithin |
| Índice espacial | No posible eficientemente | Índice GIST / R-Tree |
| Punto en polígono | Algoritmo manual | ST_Contains |
| Estándar OGC | No | Sí |
-- ¿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.
-- ¿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).
-- ¿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.
| 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 |
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.
Punto central de un polígono.
SELECT nombre,
ST_AsText(ST_Centroid(area))
AS centro
FROM zonas;
Á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;
Une varias geometrías eliminando superposiciones.
SELECT ST_AsText(ST_Union(area))
AS cobertura_total
FROM zonas_cobertura
WHERE cadena_id = 5;
Longitud de una línea en metros.
SELECT nombre,
ST_Length(ruta::geography)
AS longitud_metros
FROM rutas_entrega
ORDER BY longitud_metros;
Sin índice espacial, cada consulta recorre toda la tabla.
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);
-- 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.
Monitoreo de flota en tiempo real.
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);
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';
-- 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.
Una municipalidad analiza cobertura de servicios.
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.
-- 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';
Disparar una acción cuando un objeto entra o sale de una zona.
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()
);
-- 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.