spatial: пространственная аналитика
Геоданные — точки, линии, полигоны, координаты — встречаются в аналитике постоянно: магазины на карте, маршруты доставки, зоны покрытия, границы регионов. Расширение spatial превращает DuckDB в полноценный инструмент геоаналитики: оно добавляет геометрические функции, чтение и запись десятков ГИС-форматов и пространственные операции. Этот урок про то, что умеет spatial, как устроена пространственная геометрия в DuckDB — и про важную особенность: spatial единственное core-расширение, которое нельзя загрузить автоматически.
spatial требует явной загрузки
Начнём с того, о чём легко забыть. В уроке про систему расширений мы видели: большинство core-расширений autoloadable — первое обращение к их функциям подтягивает расширение само. spatial — исключение. Хотя это полноценное core-расширение, оно не autoloadable. Первый же вызов пространственной функции без предварительной загрузки даст ошибку «функция не найдена».
Поэтому работа с геоданными всегда начинается с двух явных команд:
INSTALL spatial;
LOAD spatial;
-- Только теперь пространственные функции доступны
SELECT ST_Point(13.405, 52.520) AS berlin;
Причина исключения — практическая: spatial велик и тянет внешние ГИС-библиотеки (GDAL для форматов, GEOS для геометрических операций, PROJ для проекций). Загружать такой объём кода молча, при случайном упоминании функции, было бы неожиданно для пользователя. Поэтому spatial требует осознанного LOAD.
Если запрос с пространственной функцией падает с ошибкой вида «Function ST_Point does not exist», первое, что надо проверить, — выполнен ли LOAD spatial в этой сессии. LOAD действует только в пределах сессии: каждое новое подключение к DuckDB стартует без загруженного spatial, и команду нужно повторять. Это самая частая ошибка новичков с геоданными в DuckDB.
Тип GEOMETRY
В основе геоаналитики лежит тип данных для пространственных объектов. Исторически тип GEOMETRY приходил вместе с расширением spatial. В DuckDB 1.5 GEOMETRY повышен до core-типа — он стал частью системы типов ядра, как INTEGER или VARCHAR. Но сами пространственные функции и форматы по-прежнему живут в расширении spatial; в core переехало только определение типа.
GEOMETRY — это контейнер для геометрических объектов разных видов: точка (POINT), линия (LINESTRING), полигон (POLYGON), а также их множественные варианты (MULTIPOINT, MULTIPOLYGON и так далее). Внутри значение хранится в формате WKB (Well-Known Binary) — компактном бинарном представлении геометрии, стандартном в ГИС-мире.
LOAD spatial;
-- Разные виды геометрии в одной колонке GEOMETRY
SELECT ST_GeomFromText('POINT(13.4 52.5)') AS a_point,
ST_GeomFromText('LINESTRING(0 0, 1 1, 2 0)') AS a_line,
ST_GeomFromText('POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))') AS a_polygon;
Текстовое представление геометрии называется WKT (Well-Known Text) — человекочитаемая запись POINT(13.4 52.5). WKT удобен для ввода и отладки, WKB — для хранения. Функции ST_GeomFromText и ST_AsText переводят между ними.
Пространственные функции
spatial добавляет большое семейство функций с префиксом ST_ (это стандартное соглашение из спецификации Simple Features, общее для всех ГИС-систем). Их можно разбить на группы.
Конструкторы создают геометрию: ST_Point(x, y), ST_GeomFromText(wkt), ST_GeomFromWKB(wkb).
Измерения считают величины: ST_Area(geom) — площадь полигона, ST_Length(geom) — длина линии, ST_Distance(a, b) — расстояние между геометриями.
Предикаты отношений возвращают логическое значение: ST_Contains(a, b) — содержит ли A геометрию B, ST_Intersects(a, b) — пересекаются ли, ST_Within(a, b) — лежит ли A внутри B, ST_DWithin(a, b, d) — находятся ли A и B в пределах расстояния d.
Преобразования строят новую геометрию из старой: ST_Buffer(geom, d) — зона на расстоянии d вокруг геометрии, ST_Centroid(geom) — центр тяжести, ST_Intersection(a, b) — общая часть двух геометрий.
Типичный аналитический запрос — «сколько магазинов попадает в зону 2 км вокруг точки»:
LOAD spatial;
-- Магазины в радиусе 2000 метров от центра города
SELECT name, ST_Distance(location, ST_Point(13.405, 52.520)) AS dist_m
FROM stores
WHERE ST_DWithin(location, ST_Point(13.405, 52.520), 2000)
ORDER BY dist_m;
┌──────────────────┬──────────┐
│ name │ dist_m │
│ varchar │ double │
├──────────────────┼──────────┤
│ Mitte Flagship │ 340.18 │
│ Alexanderplatz │ 910.55 │
│ Hackescher Markt │ 1620.07 │
└──────────────────┴──────────┘
И пространственный джойн — «к каждой точке доставки найти район, в котором она лежит»:
-- Пространственный JOIN: точка внутри полигона района
SELECT d.order_id, r.district_name
FROM deliveries d
JOIN districts r ON ST_Contains(r.boundary, d.point);
Шейринг геометрии и статистика
GEOMETRY — это не просто blob, который DuckDB хранит как непрозрачные байты. Движок понимает структуру геометрии и применяет к ней оптимизации хранения, как и к обычным колонкам.
Для колонок с однородной геометрией (например все значения — точки) DuckDB применяет приём, который в документации называют шейрингом (shredding) геометрии: вместо того чтобы хранить каждое значение как самостоятельный WKB-блоб, повторяющаяся структура раскладывается покомпонентно, и колонка сжимается заметно лучше — для однородных колонок выигрыш порядка трёхкратного.
Кроме того, для каждого row group DuckDB хранит bounding box — прямоугольник, охватывающий все геометрии этого row group. Это пространственный аналог zonemap из обычного Parquet: по bounding box можно отсечь целые row groups при пространственном фильтре. Если запрос ищет геометрии в некоторой области, а bounding box row group с ней не пересекается — row group пропускается без чтения данных.
Форматы геоданных
ГИС-мир накопил множество форматов файлов, и spatial умеет читать большинство из них через встроенную библиотеку GDAL. Функция ST_Read открывает геопространственный файл:
LOAD spatial;
-- Чтение GeoJSON
SELECT * FROM ST_Read('regions.geojson') LIMIT 5;
-- Чтение Shapefile (классический ГИС-формат)
SELECT * FROM ST_Read('boundaries.shp');
Через ST_Read доступны GeoJSON, Shapefile, GeoPackage, KML и десятки других форматов. Запись геоданных делается через COPY ... TO с указанием драйвера GDAL — так можно выгрузить результат пространственного запроса обратно в GeoJSON или GeoPackage.
Отдельно стоит GeoParquet — соглашение о хранении геометрии в обычных Parquet-файлах с пространственными метаданными. spatial читает и пишет GeoParquet, и это удобный мост между геоаналитикой и остальным Parquet-конвейером курса: геоданные лежат в том же формате, что и всё прочее, но несут пространственную семантику.
Системы координат — отдельная важная тема геоаналитики. Координаты могут быть в градусах широты и долготы (система WGS84), а могут быть в метрах в проекции конкретного региона. Функция ST_Distance в градусах и в метрах даёт совершенно разные числа. spatial умеет преобразовывать координаты между системами через ST_Transform, опираясь на библиотеку PROJ. Если в запросе расстояния выглядят странно — почти всегда дело в том, что геометрии в разных системах координат или не в той системе.
Попробуй сам
Геоаналитику легко попробовать на небольших данных.
- Выполните
INSTALL spatial; LOAD spatial;. Затем намеренно откройте новую сессию, не делайте LOAD и выполнитеSELECT ST_Point(0,0). Получите ошибку. Это подтверждает: spatial не autoloadable, LOAD нужен в каждой сессии. - Создайте таблицу с колонкой GEOMETRY и несколькими точками городов (
ST_Point(долгота, широта)). ПосчитайтеST_Distanceмежду парами и сравните с реальными расстояниями на карте. - Постройте полигон-прямоугольник через
ST_GeomFromText('POLYGON(...)')и проверьте функциейST_Contains, какие из ваших точек лежат внутри него. Это основа пространственного джойна «точка в районе». - Скачайте небольшой GeoJSON-файл с границами регионов, прочитайте его через
ST_Read, и сделайте пространственный JOIN ваших точек с этими полигонами поST_Contains. Объясните себе, чем такой JOIN отличается от обычного JOIN по равенству ключей.