PostGIS — расширение PostgreSQL для работы с географическими данными. Стандарт индустрии (используется в OpenStreetMap, Uber, многих gov-системах). Если в продукте есть карты, расстояния, поиск «рядом» — PostGIS закрывает 95% задач без отдельной БД для гео.
Эта статья — практический минимум для разработчика. Правила пронумерованы кодами PG-GIS-NNN.
1. Когда нужен PostGIS
PG-GIS-001 — PostGIS оправдан, когда:
- Поиск в радиусе («магазины в 5 км»).
- Расстояние между точками.
- Нахождение в полигоне («в каком районе адрес?»).
- Маршруты, kNN («10 ближайших ресторанов»).
- Геокодирование/обратное (через расширения).
PG-GIS-002 — Для двух колонок lat/lon без операций — PostGIS избыточен
Достаточно numeric(9,6) и numeric(10,6).
2. Установка и базовые типы
PG-GIS-010 — Расширение:
CREATE EXTENSION postgis;
PostGIS добавляет три основных типа:
geometry— плоская геометрия. Координаты «как есть», расчёты в евклидовой плоскости.geography— сферическая. Координаты в lat/lon, расчёты учитывают кривизну Земли.raster— растровые данные (карты-картинки). Редко.
PG-GIS-011 — geography vs geometry:
geography | geometry | |
|---|---|---|
| Точность | сферическая (точно для Земли) | плоская (точно только для small-area) |
| Скорость | медленнее (тригонометрия) | быстрее |
| Операции | ограниченный набор | полный набор |
| SRID | только 4326 (WGS84) | любой (UTM, локальные системы) |
PG-GIS-012 — Default выбор: geography
Точно для глобальных приложений, проще (не надо думать про SRID и проекции). geometry — когда производительность критична и зона работы локальная (один город, регион — можно перевести в UTM).
3. Хранение точек
PG-GIS-020 — Базовая таблица с точками:
CREATE TABLE shop (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
location geography(Point, 4326) NOT NULL
);
-- вставка точки (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
);
PG-GIS-021 — Порядок координат: POINT(lon lat) — долгота сначала, широта потом
Запутаться легко. Многие туториалы делают наоборот, путаница ведёт к багам.
4. Spatial индекс — обязательно
PG-GIS-030 — GiST-индекс на geography/geometry-колонке:
CREATE INDEX ix_shop_location_gist ON shop USING gist (location);
Без spatial-индекса любой ST_DWithin/ST_Contains идёт seq-scan, на 1M точках — секунды. С индексом — миллисекунды.
PG-GIS-031 — SPGiST для специфических случаев
(точки в равномерной сетке) — редко.
5. Базовые операции
PG-GIS-040 — Поиск в радиусе — ST_DWithin:
-- магазины в 5 км от точки (Дворцовая площадь)
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;
PG-GIS-041 — ST_DWithin использует индекс. ST_Distance без WHERE — нет
Всегда оборачивай в ST_DWithin для фильтрации.
PG-GIS-042 — kNN-поиск (ближайшие N) — оператор <->:
-- 10 ближайших магазинов
SELECT id, name
FROM shop
ORDER BY location <-> ST_GeogFromText('SRID=4326;POINT(30.3158 59.9398)')
LIMIT 10;
<-> использует GiST-индекс — быстро даже на миллионах точек.
PG-GIS-043 — ST_Distance(a, b) для geography возвращает метры
Для geometry — единицы SRID (часто метры в UTM, градусы в WGS84-как-geometry, что почти не используется).
6. Полигоны и попадание в зону
PG-GIS-050 — Хранение полигонов:
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);
PG-GIS-051 — Полигон из координат — обычно из GeoJSON:
INSERT INTO district (name, boundary) VALUES (
'Центральный',
ST_GeogFromGeoJSON('{"type":"Polygon","coordinates":[[[30.30,59.93],[30.32,59.93],[30.32,59.95],[30.30,59.95],[30.30,59.93]]]}')
);
PG-GIS-052 — «В каком районе адрес?» — ST_Contains/ST_Intersects:
-- найти district для точки
SELECT name FROM district
WHERE ST_Contains(boundary::geometry, ST_MakePoint(30.3158, 59.9398));
-- ::geometry потому что ST_Contains для geography ограничено
PG-GIS-053 — Различие ST_Contains / ST_Within / ST_Intersects:
Contains(A, B)— A полностью содержит B.Within(A, B)— A находится внутри B (зеркально).Intersects(A, B)— A и B имеют общие точки (включая касание).
7. Java/Spring + PostGIS
PG-GIS-060 — jOOQ умеет читать PostGIS-типы через расширение jooq-postgres-extensions
(PostgresExtensions.POINT, etc.).
PG-GIS-061 — Без специальных типов — храни как WKT-строку и парси на стороне Java
(org.locationtech.jts):
public record GeoPoint(double lat, double lon) {
public String toWkt() { return "SRID=4326;POINT(%f %f)".formatted(lon, lat); }
}
// jOOQ:
ctx.insertInto(SHOP)
.set(SHOP.NAME, "...")
.set(SHOP.LOCATION, DSL.field("ST_GeogFromText({0})", String.class, point.toWkt()))
.execute();
PG-GIS-062 — JTS (Java Topology Suite)
— стандартная библиотека Java для геометрии. PostGIS совместим, можно гонять Geometry-объекты туда-обратно.
8. Производительность
PG-GIS-070 — Spatial-операции тяжелее обычных
Профилируй с EXPLAIN ANALYZE обычным способом.
PG-GIS-071 — На 100K+ точек кешируй частые запросы или используй materialized views
(см. Materialized views).
PG-GIS-072 — Для геопоиска с фильтрами по другим полям — composite GiST с btree_gist extension:
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);
9. Обратная совместимость с lat/lon
PG-GIS-080 — Если уже есть lat numeric, lon numeric колонки:
- Можно жить без PostGIS, считая через формулу haversine, но индекс не поможет — seq-scan.
- Лучше — добавить generated column
geography:
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.
10. Антипаттерны
PG-GIS-090 — ST_Distance без ST_DWithin в WHERE
— seq-scan, медленно.
PG-GIS-091 — POINT(lat lon) (наоборот)
— distance отрицателен или просто врёт. Помни порядок: (lon lat).
PG-GIS-092 — Хранение lat/lon как varchar
— не сравнить, не индексировать. numeric или geography(Point).
PG-GIS-093 — Расчёт расстояния через haversine в коде Java
— медленно, неиндексируемо. PostGIS делает это нативно.
PG-GIS-094 — geometry для глобальных координат
— ошибка на больших расстояниях (земля не плоская).
PG-GIS-095 — Spatial-индекс отсутствует
— все запросы seq-scan.
Чек-лист
- [ ] Если есть spatial-операции — установлен
CREATE EXTENSION postgis. - [ ] Тип колонки —
geography(Point, 4326), неlat/lonотдельно. - [ ] GiST-индекс на geo-колонке.
- [ ] Поиск в радиусе через
ST_DWithin, неST_DistanceвWHERE. - [ ] Поиск ближайших через
ORDER BY <->, не вычисление distance в коде. - [ ] Координаты в
POINT(lon lat)— долгота сначала. - [ ] При мульти-фильтре — composite GiST с
btree_gist.
Связанные
- Типы индексов — GiST для PostGIS.
- Расширения —
btree_gist,pg_trgm, другие полезные.