Logo Craft Homelab Docs Контакты Telegram
PostGIS в продакшене: как не положить сервис геозапросами Трендовые github проекты в нашем телеграм канале. Подпишись →
2 июня 2026 г.

Геосервис на PostgreSQL без сюрпризов в проде

Карты с пользовательскими отметками кажутся простой задачей: есть объект, координаты, статус и несколько фильтров. Но как только проект выходит за пределы демо, геоданные начинают вести себя иначе, чем обычные строки в таблице. Пользователи двигают карту, фронтенд отправляет много запросов на видимую область, записи обновляются почти в реальном времени, а база внезапно получает не один аккуратный lookup по id, а поток пространственных пересечений.

PostGIS отлично подходит для таких задач, но он не отменяет инженерную дисциплину. Если хранить геометки без понятной модели, строить фильтры поверх тяжёлых функций и забыть про индексы, даже небольшой сервис может уткнуться в CPU, долгие блокировки или слишком дорогие планы выполнения. Ниже — практический чеклист для homelab- и production-проектов, где карта является не украшением, а основным интерфейсом.

Сначала модель, потом карта

Главная ошибка — начинать с визуализации. На экране нужны маркеры, поэтому в таблицу быстро добавляют lat, lon, пару текстовых полей и начинают фильтровать всё подряд. Такой подход работает до первой нагрузки, но плохо масштабируется.

Лучше сразу разделить данные на несколько уровней:

  • сырые пользовательские сообщения или события;
  • нормализованные объекты, которые реально нужно показывать;
  • агрегаты для карты на разных масштабах;
  • служебные статусы: актуальность, доверие, время последнего обновления;
  • историю изменений, если важна модерация или аудит.

Для PostGIS обычно удобнее хранить точку в колонке geometry(Point, 4326) или geography(Point, 4326), а не держать широту и долготу как независимые числа. geometry чаще выбирают для быстрых операций на карте и bounding box-запросов, geography — когда критична точность расстояний на поверхности Земли. В большинстве веб-карт первым выбором будет geometry плюс явное понимание, где допустима приближённая геометрия.

Индекс — обязательная часть схемы

Геозапрос без пространственного индекса почти гарантированно станет проблемой. Для PostGIS типичный выбор — GiST:

CREATE INDEX places_geom_gix ON places USING GIST (geom);

Но наличие индекса само по себе не спасает. Запрос должен быть написан так, чтобы планировщик мог им воспользоваться. Для выборки объектов в видимой области карты лучше использовать оператор пересечения с envelope, а не вычислять расстояние до каждой точки:

SELECT id, title, status, updated_at, geom
FROM places
WHERE geom && ST_MakeEnvelope(:min_lng, :min_lat, :max_lng, :max_lat, 4326)
  AND status = 'active'
ORDER BY updated_at DESC
LIMIT 1000;

Оператор && работает с bounding box и хорошо ложится на GiST-индекс. Если нужно точное уточнение для сложных геометрий, его можно добавить вторым условием через ST_Intersects, но сначала стоит отфильтровать кандидатов дешёвым способом.

Отдельно проверьте обычные фильтры. Если почти каждый запрос добавляет status, city_id или updated_at, нужны дополнительные индексы или пересмотр модели. Часто полезен частичный индекс только по активным объектам:

CREATE INDEX places_active_geom_gix
ON places USING GIST (geom)
WHERE status = 'active';

Ограничивайте результат на уровне API

Карта провоцирует тяжёлые запросы. Пользователь может открыть всю страну одним движением, а фронтенд честно попросит все точки в огромном прямоугольнике. Если API возвращает десятки тысяч объектов, проблема уже не только в базе: растут JSON, сетевой трафик, память браузера и время отрисовки.

Поэтому у endpoint-а должны быть явные правила:

  • максимальная площадь bounding box;
  • обязательный LIMIT;
  • разные ответы для разных zoom level;
  • отказ от выдачи точечных объектов на слишком мелком масштабе;
  • серверная агрегация вместо попытки нарисовать всё.

На низком zoom полезнее отдавать кластеры или heatmap-агрегаты. На высоком — конкретные объекты. Это не только быстрее, но и честнее с точки зрения UX: пользователь всё равно не сможет осмысленно разобрать 50 тысяч маркеров на экране.

Агрегации лучше готовить заранее

Если данные обновляются часто, возникает соблазн считать всё на лету: группы по тайлам, количество объектов, статусы, свежесть. На небольшом датасете это удобно, но при росте нагрузки такие запросы быстро становятся горячей точкой.

Практичный вариант — хранить предрасчитанные агрегаты для популярных масштабов или тайлов. Например, можно завести таблицу с ключом тайла, zoom level и счётчиками по статусам. Обновлять её можно фоновым воркером, очередью событий или периодическим job-ом. Да, данные будут слегка отставать, зато карта останется отзывчивой.

Для проектов с real-time ожиданиями стоит разделить два потока: свежие изменения показывать отдельным лёгким слоем, а основную массу объектов брать из агрегатов. Так пользователь видит актуальность, но база не пересчитывает весь мир при каждом pan/zoom.

Защита от пользовательского шторма

Геосервис легко перегрузить без злого умысла. Достаточно, чтобы фронтенд отправлял запрос при каждом пикселе движения карты. Поэтому производительность начинается не в SQL, а в контракте клиента и сервера.

Минимальный набор защиты:

  • debounce на клиенте при перемещении карты;
  • отмена устаревших HTTP-запросов;
  • кеширование одинаковых bbox-запросов на короткое время;
  • rate limit на пользователя или IP;
  • таймауты запросов к базе;
  • отдельный read pool для картографических endpoint-ов.

В PostgreSQL полезно выставить statement_timeout для роли приложения. Лучше вернуть пользователю аккуратную ошибку или упрощённый ответ, чем позволить одному запросу держать ресурсы слишком долго.

Наблюдаемость: EXPLAIN должен быть привычкой

PostGIS-запросы нужно смотреть через EXPLAIN (ANALYZE, BUFFERS), особенно после добавления новых фильтров. Важно проверять не только общее время, но и количество прочитанных строк, использование индекса, сортировки, временные файлы и расхождение между оценкой планировщика и реальностью.

Если планировщик сильно ошибается, проверьте ANALYZE, статистику по колонкам и распределение данных. Геометки часто распределены неравномерно: в центре города плотность огромная, за его пределами почти пусто. Из-за этого один и тот же SQL может быть быстрым на тестовом bbox и тяжёлым на популярной области.

В метриках приложения отдельно отслеживайте:

  • p95/p99 latency для map endpoint-ов;
  • число объектов в ответе;
  • размер JSON;
  • частоту запросов по zoom level;
  • таймауты и отмены;
  • самые дорогие SQL через pg_stat_statements.

Без этих данных оптимизация превращается в угадывание.

План восстановления важнее героической оптимизации

Даже аккуратная схема может упасть из-за неудачного релиза: новый фильтр, забытый индекс, массовая загрузка, изменение поведения фронтенда. Поэтому нужен простой план отката.

Перед изменениями в геозапросах стоит иметь:

  • миграции индексов с CONCURRENTLY, если таблица живая;
  • feature flag для нового endpoint-а;
  • возможность временно отключить тяжёлые слои карты;
  • лимиты на максимальный bbox и размер ответа;
  • read replica или хотя бы отдельный пул соединений для чтения;
  • dashboard, где видно деградацию сразу после релиза.

В homelab это может выглядеть проще: Docker Compose, PostgreSQL, Grafana, pg_stat_statements и скрипт для быстрого отключения экспериментального слоя. Смысл тот же: карта не должна иметь возможность положить всю систему одним неудачным запросом.

Итог

PostGIS — мощный инструмент, но для карты с пользовательскими данными он должен быть частью продуманной архитектуры. Пространственный индекс, ограничение bbox, разные ответы по zoom level, предрасчёт агрегатов и наблюдаемость обычно дают больше пользы, чем попытка «дотюнить» один SQL до идеала.

Если проект начинается как небольшой сервис с отметками на карте, заложите эти правила заранее. Тогда рост аудитории, новые фильтры и всплески активности будут инженерной задачей, а не аварией в базе данных.