Harita uygulamaları, lojistik sistemleri, gayrimenkul platformları veya hava durumu servisleri geliştiriyorsanız, er ya da geç coğrafi veri yönetimi meselesini çözmeniz gerekiyor. PostgreSQL’in güçlü uzantısı PostGIS, bu noktada devreye giriyor ve size sadece koordinat saklamaktan çok daha fazlasını sunuyor: gerçek anlamda mekansal sorgulama, mesafe hesaplama, alan analizi ve geometri işlemleri.
PostGIS Nedir ve Neden Kullanılır?
PostGIS, PostgreSQL’i tam anlamıyla bir mekansal veritabanına dönüştüren açık kaynaklı bir uzantıdır. 2001 yılından bu yana geliştirilen bu proje, OGC (Open Geospatial Consortium) standartlarına uyumludur ve endüstride en yaygın kullanılan coğrafi veritabanı çözümlerinden biri haline gelmiştir.
Düşünün ki bir e-ticaret şirketinin lojistik ekibindesiniz. Deponuzun 50 kilometre çevresindeki müşterilere aynı gün teslimat sunmak istiyorsunuz. Ya da bir perakende zinciri için en kalabalık mahallelere yakın mağaza konumlarını analiz ediyorsunuz. Belki de bir acil servis koordinasyon sisteminde en yakın ambulansı bulmak zorunda kalıyorsunuz. Tüm bu senaryolarda PostGIS olmadan işin içinden çıkmak oldukça zahmetli olur.
Standart bir veritabanında latitude ve longitude kolonları tutup Python veya Java tarafında mesafe hesabı yapabilirsiniz tabii. Ama milyonlarca kayıt söz konusu olduğunda bu yaklaşım hem yavaş hem de hata yapmaya açık olur. PostGIS, bu hesaplamaları veritabanı katmanına taşır ve coğrafi indeksler sayesinde inanılmaz hızlarda çalışır.
Kurulum ve İlk Adımlar
PostGIS kurulumu sisteme göre farklılık gösterse de çoğu dağıtımda oldukça basittir. Önce mevcut PostgreSQL sürümünüzü kontrol edin.
# PostgreSQL sürümü öğrenme
psql --version
# Ubuntu/Debian üzerinde PostGIS kurulumu (PostgreSQL 15 için)
sudo apt update
sudo apt install postgresql-15-postgis-3 postgresql-15-postgis-3-scripts
# RHEL/CentOS/Rocky Linux üzerinde
sudo dnf install postgis33_15
# macOS üzerinde Homebrew ile
brew install postgis
Kurulum tamamlandıktan sonra PostGIS uzantısını ilgili veritabanına aktif etmeniz gerekiyor.
# Veritabanına bağlanın
psql -U postgres -d mydb
# PostGIS uzantısını etkinleştir
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
# Kurulumu doğrula
SELECT PostGIS_Version();
SELECT PostGIS_Full_Version();
PostGIS_Version() fonksiyonu çalışıyorsa her şey yolunda demektir. Şimdi asıl işe girebiliriz.
Temel Geometri Tipleri ve Veri Yapıları
PostGIS birkaç temel geometri tipiyle çalışır. Bunları bilmeden verimli sorgular yazmak mümkün değil.
- POINT: Tek bir koordinat noktası. Mağaza konumu, kullanıcı lokasyonu gibi.
- LINESTRING: Birden fazla noktadan oluşan çizgi. Yol güzergahı, nehir akışı gibi.
- POLYGON: Kapalı bir alan. Mahalle sınırı, bina ayak izi gibi.
- MULTIPOINT: Birden fazla nokta koleksiyonu.
- MULTILINESTRING: Birden fazla çizgi koleksiyonu.
- MULTIPOLYGON: Birden fazla poligon koleksiyonu. İl sınırları genellikle bu tipte olur.
- GEOMETRYCOLLECTION: Karışık geometri koleksiyonu.
Koordinat referans sistemi (SRID) konusu da kritik. En çok kullanılan iki sistem:
- SRID 4326 (WGS84): GPS koordinatları bu formatı kullanır. Derece cinsinden koordinatlar.
- SRID 3857 (Web Mercator): Google Maps, OpenStreetMap bu sistemi kullanır. Metre cinsinden.
Mesafe hesaplarında doğruluğu artırmak için Türkiye’ye özel SRID 32636 (UTM Zone 36N) veya SRID 32637 (UTM Zone 37N) sistemlerini de kullanabilirsiniz.
Gerçek Dünya Senaryosu: Mağaza Lokasyon Yönetimi
Haydi pratik bir örnek üzerinden gidelim. Türkiye genelinde şubeleri olan bir market zincirinin veritabanını kuralım.
# Temel tablo yapısı
CREATE TABLE magazalar (
id SERIAL PRIMARY KEY,
ad VARCHAR(100) NOT NULL,
il VARCHAR(50),
ilce VARCHAR(50),
adres TEXT,
konum GEOMETRY(POINT, 4326),
acilis_tarihi DATE,
aktif BOOLEAN DEFAULT true
);
# Coğrafi indeks oluştur (bu kritik!)
CREATE INDEX idx_magazalar_konum ON magazalar USING GIST(konum);
# Örnek veri ekleyelim
INSERT INTO magazalar (ad, il, ilce, adres, konum) VALUES
('Kadıköy Şubesi', 'İstanbul', 'Kadıköy', 'Moda Caddesi No:1',
ST_SetSRID(ST_MakePoint(29.0228, 40.9903), 4326)),
('Beşiktaş Şubesi', 'İstanbul', 'Beşiktaş', 'Barbaros Bulvarı No:5',
ST_SetSRID(ST_MakePoint(29.0094, 41.0422), 4326)),
('Kızılay Şubesi', 'Ankara', 'Çankaya', 'Atatürk Bulvarı No:100',
ST_SetSRID(ST_MakePoint(32.8543, 39.9208), 4326)),
('Alsancak Şubesi', 'İzmir', 'Konak', 'Kıbrıs Şehitleri Caddesi No:50',
ST_SetSRID(ST_MakePoint(27.1389, 38.4192), 4326));
Dikkat edin, ST_MakePoint fonksiyonunda önce longitude (boylam), sonra latitude (enlem) yazılır. Bu sıralama çok sık karıştırılıyor ve yanlış konumlar ortaya çıkıyor. GPS koordinatlarında “enlem, boylam” derken, PostGIS “boylam, enlem” sıralaması bekler.
En Yakın Mağazayı Bulma
Kullanıcının konumuna en yakın mağazaları bulmak en sık karşılaşılan senaryolardan biri. Diyelim ki kullanıcı İstanbul’da Taksim’de, oraya en yakın 3 mağazayı listeleyelim.
-- Kullanıcı konumu: Taksim (29.0855, 41.0369)
-- ST_Distance coğrafi hesaplama için geography tipine cast yapıyoruz
-- Bu sayede sonuç metre cinsinden geliyor
SELECT
ad,
il,
ilce,
ROUND(
ST_Distance(
konum::geography,
ST_SetSRID(ST_MakePoint(29.0855, 41.0369), 4326)::geography
) / 1000, 2
) AS uzaklik_km
FROM magazalar
WHERE aktif = true
ORDER BY konum::geography <-> ST_SetSRID(ST_MakePoint(29.0855, 41.0369), 4326)::geography
LIMIT 3;
Burada operatörü çok önemli. Bu operatör KNN (K-Nearest Neighbor) indeks taraması yaparak GIST indeksini verimli kullanır. Eğer ORDER BY ST_Distance(...) kullansaydınız, tam tablo taraması yapardı ve milyonlarca kayıtta bu felaket demektir. ise indeksli arama yaparak çok daha hızlıdır.
Belirli Yarıçap İçindeki Noktalara Filtreleme
“50 km çevresindeki tüm mağazaları getir” gibi bir sorgu için ST_DWithin fonksiyonu kullanılır.
-- Ankara Kızılay'dan 200 km yarıçapındaki tüm mağazalar
SELECT
ad,
il,
ilce,
ROUND(
ST_Distance(
konum::geography,
ST_SetSRID(ST_MakePoint(32.8543, 39.9208), 4326)::geography
) / 1000, 1
) AS uzaklik_km
FROM magazalar
WHERE ST_DWithin(
konum::geography,
ST_SetSRID(ST_MakePoint(32.8543, 39.9208), 4326)::geography,
200000 -- 200.000 metre = 200 km
)
ORDER BY uzaklik_km;
ST_DWithin fonksiyonu, ST_Distance ile karşılaştırmaya göre çok daha hızlıdır çünkü GIST indeksini doğrudan kullanabilir.
Poligon Tabanlı Sorgular: Bölge İçindeki Noktalar
Lojistik senaryolarında teslimat bölgelerini poligon olarak tanımlayıp bu bölge içindeki müşterileri bulmak çok yaygın bir ihtiyaç.
-- Teslimat bölgesi tablosu
CREATE TABLE teslimat_bolgeleri (
id SERIAL PRIMARY KEY,
bolge_adi VARCHAR(100),
sehir VARCHAR(50),
sinir GEOMETRY(POLYGON, 4326),
aktif BOOLEAN DEFAULT true
);
CREATE INDEX idx_teslimat_sinir ON teslimat_bolgeleri USING GIST(sinir);
-- Müşteri tablosu
CREATE TABLE musteriler (
id SERIAL PRIMARY KEY,
ad VARCHAR(100),
telefon VARCHAR(20),
konum GEOMETRY(POINT, 4326)
);
CREATE INDEX idx_musteriler_konum ON musteriler USING GIST(konum);
-- Belirli bir teslimat bölgesi içindeki müşterileri bul
-- ST_Within: nokta poligonun içinde mi?
-- ST_Intersects: daha genel, kesişim var mı?
SELECT
m.ad,
m.telefon,
tb.bolge_adi
FROM musteriler m
JOIN teslimat_bolgeleri tb ON ST_Within(m.konum, tb.sinir)
WHERE tb.bolge_adi = 'Kadıköy Merkez'
AND tb.aktif = true;
Alan ve Çevre Hesaplamaları
Belediye projeleri, kadastro sistemleri veya mülk yönetimi uygulamalarında alan hesabı kritiktir.
-- Parsel tablosu örneği
CREATE TABLE parseller (
id SERIAL PRIMARY KEY,
parsel_no VARCHAR(50),
ada_no VARCHAR(50),
il VARCHAR(50),
sinir GEOMETRY(POLYGON, 4326)
);
-- Alan ve çevre hesaplama
-- geography tipine cast edince metre cinsinden sonuç alırız
SELECT
parsel_no,
ada_no,
ROUND(ST_Area(sinir::geography)::numeric, 2) AS alan_m2,
ROUND((ST_Area(sinir::geography) / 10000)::numeric, 4) AS alan_dekar,
ROUND(ST_Perimeter(sinir::geography)::numeric, 2) AS cevre_metre,
-- Bounding box merkez noktası
ST_X(ST_Centroid(sinir)) AS merkez_lon,
ST_Y(ST_Centroid(sinir)) AS merkez_lat
FROM parseller
WHERE il = 'İstanbul'
ORDER BY alan_m2 DESC
LIMIT 20;
Gelişmiş Senaryo: Ambulans Yönlendirme Sistemi
Daha karmaşık bir gerçek dünya senaryosu üzerinden gidelim. Bir şehirde acil servis koordinasyon sistemi kuruyorsunuz. Her ambulansın anlık konumunu takip ediyor ve gelen çağrıya en yakın müsait ambulansı yönlendirmeniz gerekiyor.
-- Ambulans takip tablosu
CREATE TABLE ambulanslar (
id SERIAL PRIMARY KEY,
plaka VARCHAR(20) UNIQUE NOT NULL,
durum VARCHAR(20) CHECK (durum IN ('müsait', 'görevde', 'bakımda')),
son_konum GEOMETRY(POINT, 4326),
son_guncelleme TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
baga_cikis GEOMETRY(POINT, 4326) -- Bağlı olduğu istasyon
);
CREATE INDEX idx_ambulans_konum ON ambulanslar USING GIST(son_konum);
-- Acil çağrı tablosu
CREATE TABLE acil_cagrilar (
id SERIAL PRIMARY KEY,
olay_tipi VARCHAR(50),
olay_konum GEOMETRY(POINT, 4326),
adres TEXT,
atanan_ambulans_id INTEGER REFERENCES ambulanslar(id),
cagri_zamani TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
durum VARCHAR(20) DEFAULT 'beklemede'
);
-- Yeni acil çağrıya en yakın müsait ambulansı bul
-- Örnek: Taksim'de kaza bildirimi
WITH olay AS (
SELECT ST_SetSRID(ST_MakePoint(29.0855, 41.0369), 4326)::geography AS konum
)
SELECT
a.id,
a.plaka,
ROUND(
ST_Distance(
a.son_konum::geography,
o.konum
) / 1000, 2
) AS uzaklik_km,
-- Tahmini varış süresi (ortalama 40 km/s kentsel hız varsayımıyla)
ROUND(
(ST_Distance(a.son_konum::geography, o.konum) / 1000) / 40 * 60
) AS tahmini_sure_dakika,
a.son_guncelleme
FROM ambulanslar a, olay o
WHERE a.durum = 'müsait'
AND ST_DWithin(a.son_konum::geography, o.konum, 20000) -- 20 km içinde
ORDER BY a.son_konum::geography <-> ST_SetSRID(ST_MakePoint(29.0855, 41.0369), 4326)::geography
LIMIT 3;
Kümeleme ve Yoğunluk Analizi
Büyük veri setlerinde noktaları kümeler halinde analiz etmek bazen hayat kurtarır. Örneğin suç olaylarının yoğunlaştığı bölgeleri tespit etmek veya mağaza açmak için potansiyel lokasyonları belirlemek.
-- Izgara bazlı yoğunluk analizi
-- İstanbul'u 1km x 1km karelere bölerek her karede kaç müşteri var?
WITH izgara AS (
SELECT
ST_SnapToGrid(konum, 0.009, 0.009) AS hucre_merkezi,
COUNT(*) AS musteri_sayisi
FROM musteriler
WHERE konum IS NOT NULL
GROUP BY hucre_merkezi
HAVING COUNT(*) > 5 -- En az 5 müşteri olan hücreler
)
SELECT
ST_X(hucre_merkezi) AS lon,
ST_Y(hucre_merkezi) AS lat,
musteri_sayisi,
CASE
WHEN musteri_sayisi > 100 THEN 'yüksek yoğunluk'
WHEN musteri_sayisi > 50 THEN 'orta yoğunluk'
ELSE 'düşük yoğunluk'
END AS yogunluk_seviyesi
FROM izgara
ORDER BY musteri_sayisi DESC;
ST_SnapToGrid fonksiyonu koordinatları belirtilen ızgara boyutuna yuvarlar. 0.009 derece yaklaşık 1 km’ye karşılık gelir (enlem farkına göre değişir).
İndeks Stratejileri ve Performans Optimizasyonu
PostGIS sorgu performansı büyük ölçüde doğru indeksleme stratejisine bağlı. Yanlış yapılandırılmış bir sistem, küçük bir veri setinde bile kabusa dönebilir.
-- Sorgu planını incele
EXPLAIN ANALYZE
SELECT ad, il
FROM magazalar
WHERE ST_DWithin(
konum::geography,
ST_SetSRID(ST_MakePoint(29.0855, 41.0369), 4326)::geography,
50000
);
-- Geometry vs Geography indeks karşılaştırması
-- GIST indeksi geometri kolonları için
CREATE INDEX idx_geometry ON tablo USING GIST(konum);
-- BRIN indeksi büyük, sıralı geometri veri setleri için (daha az yer kaplar)
-- Özellikle zaman serisi coğrafi verilerde işe yarar
CREATE INDEX idx_brin ON buyuk_tablo USING BRIN(konum);
-- Partial indeks: sadece aktif kayıtlar için
CREATE INDEX idx_aktif_magazalar
ON magazalar USING GIST(konum)
WHERE aktif = true;
-- İstatistikleri güncel tut
ANALYZE magazalar;
-- PostGIS spesifik istatistik güncelleme
SELECT UpdateGeometrySRID('magazalar', 'konum', 4326);
Birkaç kritik performans ipucu:
- GIST indeksini her zaman oluşturun: Coğrafi kolon içeren tablolarda GIST indeksi olmadan sorgular tam tablo taraması yapar.
- geography vs geometry seçimi:
geographytipi gerçek dünya mesafelerini doğru hesaplar ama biraz daha yavaştır. Küçük alanlar içingeometry+ uygun SRID tercih edebilirsiniz. operatörünü kullanın: Sıralama gerektiren en yakın komşu sorgularındaORDER BY ST_Distance(...)yerinekullanın.- Bounding box ön filtresi: PostGIS otomatik olarak önce bounding box kontrolü yapar, bu nedenle
&&operatörünü elle eklemek genellikle gereksizdir. - vacuum ve autovacuum ayarları: Coğrafi veriler sık güncelleniyorsa autovacuum agresif ayarlara ihtiyaç duyabilir.
Veri Dönüşümü ve GeoJSON Entegrasyonu
Modern web uygulamaları çoğunlukla GeoJSON formatında veri bekler. PostGIS bu dönüşümü doğrudan SQL’den yapabilir.
-- Tek bir mağazayı GeoJSON olarak döndür
SELECT ST_AsGeoJSON(konum, 6) AS geojson
FROM magazalar
WHERE id = 1;
-- Tüm mağazaları FeatureCollection olarak hazırla
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(konum)::json,
'properties', json_build_object(
'id', id,
'ad', ad,
'il', il,
'ilce', ilce
)
)
)
) AS feature_collection
FROM magazalar
WHERE aktif = true;
-- WKT (Well-Known Text) formatında çıktı
SELECT ST_AsText(konum) AS wkt_format
FROM magazalar;
-- GeoJSON'dan PostgreSQL geometrisine dönüşüm
INSERT INTO magazalar (ad, il, konum)
VALUES (
'Yeni Şube',
'Bursa',
ST_GeomFromGeoJSON('{"type":"Point","coordinates":[29.0606, 40.1885]}')
);
Bakım ve İzleme
Production ortamında PostGIS veritabanı bakımı için birkaç pratik komut:
# Geometri sütunları hakkında bilgi
SELECT * FROM geometry_columns;
# Tüm spatial indeksleri listele
SELECT
schemaname,
tablename,
indexname,
indexdef
FROM pg_indexes
WHERE indexdef ILIKE '%gist%';
# Tablo boyutları (geometri verileri yer kaplar)
SELECT
relname AS tablo,
pg_size_pretty(pg_total_relation_size(relid)) AS toplam_boyut,
pg_size_pretty(pg_relation_size(relid)) AS tablo_boyutu,
pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS indeks_boyutu
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;
# Geçersiz geometrileri tespit et
SELECT id, ST_IsValid(konum), ST_IsValidReason(konum)
FROM magazalar
WHERE NOT ST_IsValid(konum);
# Geçersiz geometrileri düzelt
UPDATE magazalar
SET konum = ST_MakeValid(konum)
WHERE NOT ST_IsValid(konum);
Sonuç
PostGIS, PostgreSQL’i dünya standartlarında bir mekansal veritabanına dönüştürüyor. Bu yazıda ele aldığımız konular buzdağının görünen kısmı: en yakın komşu sorguları, bölge içi filtreler, alan hesaplamaları ve GeoJSON entegrasyonu temel ihtiyaçları karşılar. Bunun ötesinde PostGIS’in sunduğu ST_Union, ST_Intersection, ST_Buffer, ST_ConvexHull gibi geometri işleme fonksiyonları ve pgRouting entegrasyonuyla tam yol ağı analizi yapabilirsiniz.
Sistem yöneticisi gözüyle bakıldığında, en kritik nokta her zaman GIST indeksi oluşturmak ve geography tipini doğru kullanmaktır. İndekssiz bir PostGIS tablosu, büyük veri setlerinde sunucunuzu felç edebilir. Ayrıca production ortamına geçmeden önce gerçekçi veri hacimleriyle EXPLAIN ANALYZE çıktılarını mutlaka inceleyin.
Coğrafi veri projeniz için PostGIS’i seçmek, uzun vadede hem geliştirici hem de sysadmin tarafında size büyük kolaylık sağlar. Tek bir SQL sorgusuyla yapabileceğiniz işler için harici kütüphanelere veya ayrı servislerepaz ihtiyacınız olmaz, veri tutarlılığı veritabanı katmanında kalır ve PostgreSQL’in tüm güvenlik, yedekleme ve replikasyon özelliklerinden yararlanırsınız.