PostgreSQL kann Geo-Daten — auf zwei Ebenen. Built-in gibt es ein paar einfache Geometrie-Typen wie point und polygon (gut für „ist Punkt X im Rechteck Y?”). Für ernsthafte Geo-Anwendungen ist aber PostGIS die de-facto Standardlösung. Dieser Artikel zeigt das eingebaute Minimum und worauf man bei PostGIS achten sollte.
Eingebaute Geometrie-Typen
Postgres bringt von Haus aus diese Typen mit:
| Typ | Beschreibung | Beispiel |
|---|---|---|
point | 2D-Punkt | '(13.40, 52.52)' |
line | unendliche Linie | '{1, 1, 0}' (Form Ax+By+C=0) |
lseg | Liniensegment (zwei Punkte) | '[(0,0), (1,1)]' |
box | Achsen-paralleler Rechteck | '((0,0), (1,1))' |
path | offene oder geschlossene Polylinie | '[(0,0),(1,1),(2,0)]' |
polygon | Polygon | '((0,0),(1,0),(1,1),(0,1))' |
circle | Kreis (Mittelpunkt + Radius) | '<(0,0), 1>' |
CREATE TABLE locations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
pos point NOT NULL -- (Längengrad, Breitengrad)
);
INSERT INTO locations (name, pos) VALUES
('Berlin Mitte', point(13.40, 52.52)),
('München Marienplatz', point(11.58, 48.14)),
('Hamburg Rathaus', point(9.99, 53.55));Built-in-Operatoren
myapp=> SELECT
name,
pos <-> point(13.40, 52.52) AS dist_to_berlin
FROM locations
ORDER BY dist_to_berlin;
name | dist_to_berlin
----------------------+-----------------
Berlin Mitte | 0
Hamburg Rathaus | 4.092…
München Marienplatz | 4.778…<-> ist der „Distanz”-Operator. Aber Achtung: dieser rechnet euklidisch — als wäre die Erde flach. Für kleine Bereiche ist das eine Näherung, für größere total daneben. Die obigen Werte sind nicht „Kilometer”, sondern Grad — und nicht mal das ist sinnvoll, weil Längen- und Breitengrad-Distanzen unterschiedlich groß sind.
Für ernsthafte Distanzen → PostGIS.
Trotzdem — für lokale Anwendungen kann’s reichen:
-- Welche Locations liegen in Deutschland (vereinfachte Bounding-Box)?
SELECT name, pos
FROM locations
WHERE pos <@ box(point(5.87, 47.27), point(15.04, 55.06));<@ heißt „enthalten in”. Auf einem Polygon-Typ funktioniert das genauso für „Punkt im Polygon”-Tests.
Wann reicht das Built-in?
Die Built-in-Typen sind okay für:
- Sehr kleine Bereiche (eine Stadt, ein Gebäude-Grundriss), wo die euklidische Näherung kein großer Fehler ist.
- Spielerische Anwendungen: 2D-Grafik, Layout-Engines, Zeichenprogramme.
- Containment-Checks ohne metrische Distanzen: „Ist dieser Punkt im Rechteck?” ohne Aussage über Längeneinheiten.
Sie sind nicht geeignet für:
- Reale Geo-Anwendungen (Karten, Routing, Geo-Suche).
- Distanzen über mehr als ein paar Kilometer.
- Korrekte Erddarstellung (Kugel statt Ebene).
- Geo-Indexe — die Built-in-Typen unterstützen GIST-Indexe, aber ohne PostGIS’ Optimierungen.
Für all das gibt’s PostGIS.
PostGIS — die richtige Geo-Lösung
PostGIS ist eine Extension, die PostgreSQL um echte Geo-Funktionalität erweitert: Erd-Kugel-bewusste Distanzen, Routing, Karten-Tile-Generierung, Topologie. Wenn du irgendwas mit Karten, GPS-Daten oder Geo-Suche machst — PostGIS.
CREATE EXTENSION postgis;Damit bekommst du zwei neue Typen: geometry (planares 2D) und geography (Erd-Kugel).
CREATE TABLE locations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
location geography(POINT, 4326) -- Erd-Punkt, WGS84
);
INSERT INTO locations (name, location) VALUES
('Berlin', ST_MakePoint(13.40, 52.52)),
('Paris', ST_MakePoint(2.35, 48.86)),
('Rom', ST_MakePoint(12.50, 41.90));SRID 4326 ist WGS84 — das Koordinatensystem, das GPS und Google Maps verwenden. ST_MakePoint(longitude, latitude) ist der Konstruktor; die Reihenfolge Longitude, Latitude überrascht oft (mathematisch ist’s (x, y) = (lng, lat)).
Distanz auf der Erdkugel
myapp=> SELECT
name,
ST_Distance(
location,
ST_MakePoint(13.40, 52.52)::geography
)::int AS distance_meters
FROM locations
ORDER BY distance_meters;
name | distance_meters
--------+-----------------
Berlin | 0
Paris | 878477
Rom | 1183587Mit geography rechnet PostGIS auf der Erdkugel — die Werte sind echte Meter. 878 km von Berlin nach Paris, 1184 km nach Rom. Korrekt.
„Locations im Umkreis von X” — der Klassiker
-- Alle Locations innerhalb 1000 km von Berlin
SELECT name, ST_Distance(location, berlin)::int AS m
FROM locations,
(SELECT ST_MakePoint(13.40, 52.52)::geography AS berlin) b
WHERE ST_DWithin(location, berlin, 1000000) -- 1 Mio. Meter = 1000 km
ORDER BY location <-> berlin; -- nach Distanz sortierenST_DWithin ist die Magie — es kann Indexe nutzen. Ein simples WHERE ST_Distance(...) < 1000000 müsste die Distanz für jede Zeile ausrechnen (Sequential Scan). ST_DWithin mit einem GIST-Index auf location springt direkt zu den infrage kommenden Zeilen.
CREATE INDEX locations_geo_idx ON locations USING GIST (location);Mit Index ist eine Umkreis-Suche in Millionen-Zeilen-Tabellen oft unter 100 ms.
geometry vs. geography
PostGIS hat zwei Haupt-Typen:
| Typ | Welt-Modell | Distanzeinheit | Performance | Wann nehmen |
|---|---|---|---|---|
geometry | flache Ebene | Koordinaten-Einheiten | sehr schnell | Lokale Karten, ein Land/Region, eigene Koordinatensysteme |
geography | Erdkugel | Meter | langsamer (komplexere Berechnung) | Globale Anwendungen, GPS-Daten, „echte Distanzen” |
Für globale Apps mit weltweit verteilten Koordinaten: geography. Für lokale Karten in einem Ländchen: geometry mit einem passenden lokalen SRID (z. B. ETRS89/UTM für Mitteleuropa) — schneller und genauer im Lokalmaßstab.
Polygone und komplexere Shapes
PostGIS unterstützt das volle OGC-Geometrie-Modell: Linien, Polygone (mit Löchern), Multi-Polygone, Sammlungen.
CREATE TABLE districts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
shape geometry(POLYGON, 4326)
);
-- WKT-Notation (Well-Known Text):
INSERT INTO districts (name, shape) VALUES
('Mitte', ST_GeomFromText(
'POLYGON((13.39 52.50, 13.42 52.50, 13.42 52.53, 13.39 52.53, 13.39 52.50))',
4326
));
-- Welcher Bezirk enthält diesen Punkt?
SELECT name FROM districts
WHERE ST_Contains(shape, ST_GeomFromText('POINT(13.40 52.52)', 4326));ST_Contains, ST_Intersects, ST_Within etc. sind die Operatoren für Geometrie-Vergleiche. Mit GIST-Index sehr schnell, auch auf Millionen-Polygon-Datensätzen.
Interessantes
point() ist (x, y) — also (Lng, Lat).
Postgres’ point(a, b) und PostGIS’ ST_MakePoint(a, b) erwarten beide (Longitude, Latitude), also Längengrad zuerst. Das überrascht Leute, die Adressen wie „52.52, 13.40” lesen (Lat zuerst). Klassischer Bug-Auslöser. Im Zweifel: ST_MakePoint(LON, LAT) mit Variablen-Namen.
SRID 4326 ist GPS/WGS84.
Die Standard-Wahl für globale Daten. Andere SRIDs gibt’s für lokale Projektionen (EPSG.io listet Tausende). Wer nicht weiß, was SRID nehmen — 4326 ist fast immer richtig, solange du mit Lat/Lng-Daten arbeitest.
Cloud-Postgres und PostGIS.
AWS RDS, Cloud SQL, Azure Database und Heroku bieten PostGIS standardmäßig an — CREATE EXTENSION postgis reicht. Bei kleineren Anbietern erst prüfen. Spezielle PostGIS-Cloud-Anbieter (Crunchy Bridge) bieten besser optimierte Setups für Geo-Workloads.
Karten-Tiles direkt aus PostGIS.
Mit ST_AsMVT() (Mapbox Vector Tiles) liefert PostGIS Vector-Tiles direkt aus der DB — keine Extra-Tile-Server-Software nötig. Tools wie pg_tileserv und Martin sind dünne Wrapper darum.
Built-in vs. PostGIS — keine Migrations-Brücke.
Wer mit Built-in-point startet und später nach PostGIS migrieren will, muss alle Spalten umstellen. Empfehlung: bei jedem Geo-Projekt direkt PostGIS — auch wenn man am Anfang nur 100 Locations hat. Spätere Migration ist Mehrarbeit.
GIST-Index ist Pflicht für Geo-Performance.
Ohne GIST-Index laufen Geo-Queries als Sequential Scan — bei großen Tabellen unbenutzbar. CREATE INDEX … USING GIST (location) muss bei jeder Tabelle mit Geo-Spalte sein. Bei sehr großen Daten lohnt sich auch SP-GIST oder das experimentelle postgis_tiger_geocoder-Pattern.
Weiterführende Ressourcen
Externe Quellen
- Geometric Types – PostgreSQL Documentation
- Geometric Functions and Operators
- PostGIS — offizielle Doku
- PostGIS Workshop — Crunchy Data
- EPSG.io — Koordinatensysteme nachschlagen