Опирается на правила:
PG-GIS-001…PG-GIS-095из PostgreSQL Style Guide → раздел PostGIS.
Важно знать
- PostGIS — стандарт для геоданных в PG. Закрывает 95% задач без отдельной геобазы.
- Для lat/lon без операций — PostGIS избыточен, достаточно
numeric.geography(Point, 4326)— default для глобальных приложений.POINT(lon lat)— долгота сначала, легко запутаться.- GiST-индекс обязателен — без него seq-scan, секунды на 1M точках.
ST_DWithinиспользует индекс;ST_Distanceбез WHERE — нет.- kNN-поиск через
<->оператор с GiST.ST_Contains/ST_Within/ST_Intersectsдля полигонов.- Composite GiST с
btree_gistдля фильтров (is_active + location).- Generated column
geographyиз существующихlat/lon— миграция без переписи кода.
PostGIS — расширение для географических данных. Стандарт индустрии (OpenStreetMap, Uber, gov-системы).
Когда нужен
PG-GIS-001..002:
Подходит:
- Поиск в радиусе («магазины в 5 км»).
- Расстояние между точками.
- Нахождение в полигоне.
- Маршруты, kNN.
- Геокодирование.
Не подходит (= numeric достаточно):
- Просто хранение
lat/lonбез операций.
Установка и типы
PG-GIS-010..012:
CREATE EXTENSION postgis;
Три типа:
geometry— плоская. Быстрее, точно только для small-area.geography— сферическая. Точно для Земли, default.raster— картинки, редко.
geography | geometry | |
|---|---|---|
| Точность | сферическая (Земля) | плоская |
| Скорость | медленнее | быстрее |
| Операции | ограниченные | полный набор |
| SRID | только 4326 (WGS84) | любой |
Default: geography(Point, 4326).
Хранение точек
PG-GIS-020..021:
CREATE TABLE shop (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
location geography(Point, 4326) NOT NULL
);
-- POINT(lon lat) — долгота СНАЧАЛА
INSERT INTO shop (name, location) VALUES (
'Магазин на Невском',
ST_GeogFromText('SRID=4326;POINT(30.3358 59.9343)')
);
INSERT INTO shop (name, location) VALUES (
'Магазин на Невском',
ST_MakePoint(30.3358, 59.9343)::geography
);
POINT(lon lat) — порядок легко перепутать. Многие туториалы делают наоборот.
Spatial индекс — обязательно
PG-GIS-030:
CREATE INDEX ix_shop_location_gist ON shop USING gist (location);
Без spatial-индекса ST_DWithin/ST_Contains → seq-scan, на 1M точек — секунды. С GiST — миллисекунды.
Поиск в радиусе
PG-GIS-040..041: ST_DWithin.
SELECT id, name,
ST_Distance(location, ST_GeogFromText('SRID=4326;POINT(30.3158 59.9398)')) AS distance_m
FROM shop
WHERE ST_DWithin(
location,
ST_GeogFromText('SRID=4326;POINT(30.3158 59.9398)'),
5000 -- метры
)
ORDER BY distance_m
LIMIT 50;
ST_DWithin использует индекс. ST_Distance без WHERE — не использует. Всегда оборачивай в ST_DWithin.
ST_Distance для geography возвращает метры. Для geometry — единицы SRID.
kNN-поиск
PG-GIS-042: оператор <->.
SELECT id, name
FROM shop
ORDER BY location <-> ST_GeogFromText('SRID=4326;POINT(30.3158 59.9398)')
LIMIT 10;
<-> использует GiST — быстро на миллионах точек.
Полигоны
PG-GIS-050..053:
CREATE TABLE district (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
boundary geography(Polygon, 4326) NOT NULL
);
CREATE INDEX ix_district_boundary_gist ON district USING gist (boundary);
-- Полигон из GeoJSON
INSERT INTO district (name, boundary) VALUES (
'Центральный',
ST_GeogFromGeoJSON('{"type":"Polygon","coordinates":[[...]]}')
);
-- «В каком районе?»
SELECT name FROM district
WHERE ST_Contains(boundary::geometry, ST_MakePoint(30.3158, 59.9398));
| Функция | Что |
|---|---|
Contains(A, B) | A полностью содержит B |
Within(A, B) | A внутри B (зеркально) |
Intersects(A, B) | A и B имеют общие точки (включая касание) |
Java/Spring
PG-GIS-060..062:
jOOQ через jooq-postgres-extensions (POINT, etc).
Без расширения — WKT-строка + парсинг через JTS (org.locationtech.jts):
public record GeoPoint(double lat, double lon) {
public String toWkt() { return "SRID=4326;POINT(%f %f)".formatted(lon, lat); }
}
ctx.insertInto(SHOP)
.set(SHOP.NAME, "...")
.set(SHOP.LOCATION, DSL.field("ST_GeogFromText({0})", String.class, point.toWkt()))
.execute();
JTS — стандарт Java для геометрии, совместим с PostGIS.
Производительность
PG-GIS-070..072:
Composite GiST с btree_gist для фильтров:
CREATE EXTENSION btree_gist;
CREATE INDEX ix_shop_active_loc ON shop USING gist (is_active, location);
SELECT * FROM shop
WHERE is_active = true
AND ST_DWithin(location, ?, 5000);
На 100K+ точек — materialized views для частых запросов.
Миграция lat/lon → geography
PG-GIS-080: generated column.
ALTER TABLE shop ADD COLUMN location geography(Point, 4326)
GENERATED ALWAYS AS (ST_MakePoint(lon, lat)::geography) STORED;
CREATE INDEX ix_shop_location_gist ON shop USING gist (location);
Старый код продолжает читать lat/lon, новый — location для spatial.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
ST_Distance без ST_DWithin в WHERE | PG-GIS-090 | ST_DWithin |
POINT(lat lon) (наоборот) | PG-GIS-091 | POINT(lon lat) |
lat/lon как varchar | PG-GIS-092 | numeric или geography |
| Haversine в Java | PG-GIS-093 | PostGIS нативно |
geometry для глобальных координат | PG-GIS-094 | geography |
| Без spatial-индекса | PG-GIS-095 | USING gist обязателен |
<-> без GiST | PG-GIS-042 | GiST для kNN |
Composite фильтры без btree_gist | PG-GIS-072 | CREATE EXTENSION btree_gist |
Куда дальше
- PG → PostGIS — нормативные формулировки.
- Типы индексов — GiST детали.
- Расширения —
postgis,btree_gistустановка. - Materialized views — кеш для геозапросов.
- Числа и точность —
numericдля координат. - Массивы и range —
EXCLUDEдля непересечения геозон.