Опирается на правила: PG-GIS-001PG-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 — картинки, редко.
geographygeometry
Точностьсферическая (Земля)плоская
Скоростьмедленнеебыстрее
Операцииограниченныеполный набор
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 в WHEREPG-GIS-090ST_DWithin
POINT(lat lon) (наоборот)PG-GIS-091POINT(lon lat)
lat/lon как varcharPG-GIS-092numeric или geography
Haversine в JavaPG-GIS-093PostGIS нативно
geometry для глобальных координатPG-GIS-094geography
Без spatial-индексаPG-GIS-095USING gist обязателен
<-> без GiSTPG-GIS-042GiST для kNN
Composite фильтры без btree_gistPG-GIS-072CREATE EXTENSION btree_gist

Куда дальше

  • PG → PostGIS — нормативные формулировки.
  • Типы индексов — GiST детали.
  • Расширения — postgis, btree_gist установка.
  • Materialized views — кеш для геозапросов.
  • Числа и точность — numeric для координат.
  • Массивы и range — EXCLUDE для непересечения геозон.