Uygulamanızda "bana en yakın 10 şubeyi göster" gibi bir özellik gerekiyorsa ve konumları iki ayrı float sütununda tutup Haversine formülünü SQL içinde elle yazıyorsanız, bir noktadan sonra bu yaklaşım çöker: her sorgu tüm tabloyu tarar, indeks kullanamaz ve 50 bin kayıtta bile saniyeler sürer. PostGIS, PostgreSQL'e gerçek coğrafi veri tipleri, uzamsal indeksler ve yüzlerce hazır fonksiyon ekleyen bir uzantıdır ve bu problemi kökünden çözer.
Bu rehberde PostGIS kurulumunu, en çok kafa karıştıran geometry ile geography ayrımını, SRID kavramını ve neden 4326 ile 3857 arasında seçim yapmak zorunda kaldığınızı, GiST indekslerinin nasıl kurulacağını, yakınlık ve kapsama sorgularının doğru yazımını, GeoJSON ile veri alışverişini ve gerçek dünyada karşılaşacağınız performans tuzaklarını anlatacağım. Örnekler Türkiye koordinatlarıyla verilmiştir ve doğrudan çalıştırılabilir.
PostGIS Kurulumu ve İlk Yapılandırma#
PostGIS, PostgreSQL çekirdeğinin parçası değildir; ayrı bir paket olarak kurulup her veritabanında CREATE EXTENSION ile etkinleştirilir. Debian ve Ubuntu tabanlı sistemlerde paket adı PostgreSQL ana sürümüne bağlıdır.
# Kurulu PostgreSQL sürümünü öğren
psql --version
# PostGIS paketini kur (sürüm numarasını kendi kurulumunuza göre değiştirin)
sudo apt update
sudo apt install postgresql-16-postgis-3 postgresql-16-postgis-3-scripts
Uzantı, kullanılacağı her veritabanında ayrı ayrı etkinleştirilir. Sunucuya bir kez kurmak yetmez, bu adımı atlamak yeni bir veritabanı oluşturduğunuzda "type geometry does not exist" hatasının sebebidir:
-- Ana uzantı
CREATE EXTENSION IF NOT EXISTS postgis;
-- Adres eşleme (geocoding) gerekiyorsa
CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder;
-- Topoloji işlemleri gerekiyorsa
CREATE EXTENSION IF NOT EXISTS postgis_topology;
-- Kurulumu doğrula
SELECT PostGIS_Full_Version();
Çıktıda POSTGIS="3.x.x" ve altında GEOS, PROJ, GDAL sürümlerini görürsünüz. Bu üç kütüphane PostGIS'in altında çalışır: GEOS geometrik hesaplamaları, PROJ koordinat sistemi dönüşümlerini, GDAL ise raster ve dosya formatı desteğini sağlar. Uzantıyı kurduktan sonra spatial_ref_sys adında binlerce satırlık bir tablo oluştuğunu görürsünüz; bu, dünya üzerindeki tüm tanımlı koordinat referans sistemlerinin kataloğudur. PostgreSQL'i ilk kez kuruyorsanız Ubuntu'da PostgreSQL kurulumu yazısındaki adımları önce tamamlayın.
geometry mi geography mi#
PostGIS iki temel veri tipi sunar ve doğru olanı seçmek performans ile doğruluk arasındaki dengeyi belirler. geometry tipi koordinatları düzlem üzerinde ele alır; hesaplamalar Öklid geometrisiyle yapılır, hızlıdır ama dünyanın küresel olduğunu bilmez. geography tipi ise koordinatları küre üzerinde ele alır; mesafeler metre cinsinden ve gerçek yeryüzü eğriliğine göre hesaplanır, doğrudur ama daha yavaştır ve daha az fonksiyon destekler.
Pratik kural şudur: veriniz tüm dünyaya yayılıyorsa veya mesafeleri metre olarak doğru hesaplamanız gerekiyorsa geography; veriniz tek bir şehir ya da ülke ölçeğindeyse ve karmaşık geometrik işlemler yapacaksanız (kesişim, birleşim, tampon) uygun bir projeksiyonla geometry kullanın.
| Konu | geometry | geography |
|---|---|---|
| Koordinat modeli | Düzlem (Kartezyen) | Küre |
| Mesafe birimi | Koordinat sisteminin birimi (derece veya metre) | Daima metre |
| Hız | Hızlı | Yaklaşık 2-5 kat yavaş |
| Fonksiyon sayısı | Tam kütüphane | Sınırlı alt küme |
| Tipik SRID | 4326, 3857, 5254 | Yalnızca 4326 |
| İdeal kullanım | Şehir ölçeği, geometrik analiz | Küresel veri, doğru mesafe |
En sık yapılan hata, geometry(Point, 4326) tipinde saklanan veriyi ST_Distance ile ölçüp sonucu metre sanmaktır. SRID 4326 birimi derecedir; ST_Distance size 0.0134 gibi bir sayı döndürür ve bu enlem-boylam derecesi cinsindendir, metre değildir. Türkiye enleminde bir derece boylam yaklaşık 85 km, bir derece enlem ise yaklaşık 111 km eder — yani sayıyı sabit bir çarpanla metreye çevirmek de doğru sonuç vermez.
SRID Seçimi ve Koordinat Sistemleri#
SRID (Spatial Reference Identifier), koordinatlarınızın hangi referans sistemine göre yazıldığını söyleyen bir tam sayıdır. Yanlış SRID, verinizin dünyanın bambaşka bir yerine düşmesine ya da mesafelerin onda bir çıkmasına yol açar.
| SRID | Ad | Birim | Ne zaman |
|---|---|---|---|
| 4326 | WGS 84 (GPS) | Derece | Veri girişi, GeoJSON, mobil cihaz koordinatları |
| 3857 | Web Mercator | Metre | Harita karo servisleri (Leaflet, Mapbox, OSM) |
| 5254 | TUREF / TM30 | Metre | Türkiye'de yüksek doğruluklu ölçüm |
Standart yaklaşım şudur: veriyi 4326 ile saklayın, çünkü GPS cihazları, tarayıcı Geolocation API'si ve GeoJSON hep bu sistemi kullanır. Metre cinsinden hesaplama gerektiğinde sorgu anında dönüştürün ya da doğrudan geography tipine cast edin.
-- Şube tablosu: 4326 ile saklıyoruz
CREATE TABLE subeler (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ad text NOT NULL,
il text,
konum geometry(Point, 4326) NOT NULL
);
-- Nokta ekleme: ST_MakePoint(BOYLAM, ENLEM) — sıralama böyle, tersi değil
INSERT INTO subeler (ad, il, konum) VALUES
('Kadıköy Şube', 'İstanbul', ST_SetSRID(ST_MakePoint(29.0270, 40.9900), 4326)),
('Çankaya Şube', 'Ankara', ST_SetSRID(ST_MakePoint(32.8597, 39.9208), 4326)),
('Konak Şube', 'İzmir', ST_SetSRID(ST_MakePoint(27.1287, 38.4189), 4326));
ST_MakePoint fonksiyonunda argüman sırası boylam, enlem şeklindedir; yani X ekseni önce gelir. GPS cihazları ve çoğu API ise koordinatları "enlem, boylam" sırasıyla verir. Bu ters sıra, PostGIS ile çalışan herkesin en az bir kez düştüğü tuzaktır: İstanbul'daki bir şube birdenbire Hint Okyanusu'nda görünür. Şüpheye düştüğünüzde noktayı geri okuyup kontrol edin:
SELECT ad, ST_AsText(konum) FROM subeler;
-- Beklenen: POINT(29.027 40.99) → ilk sayı boylam, ikinci enlem
Metre cinsinden hesap gerektiğinde iki seçeneğiniz var. Ya sütunu geography olarak tanımlarsınız ya da sorguda cast edersiniz:
-- Cast ile: sonuç METRE cinsinden gelir
SELECT ST_Distance(
(SELECT konum FROM subeler WHERE ad = 'Kadıköy Şube')::geography,
(SELECT konum FROM subeler WHERE ad = 'Çankaya Şube')::geography
) AS mesafe_metre;
-- Yaklaşık 350000 (350 km) civarında bir değer döner
GiST İndeksi ve Yakınlık Sorguları#
Uzamsal veriyi indekssiz sorgulamak, milyonlarca satırlık bir tabloyu her seferinde baştan sona taramak demektir. PostGIS, geometri sütunları için GiST (Generalized Search Tree) indeksini kullanır ve bu indeks sınırlayıcı kutu (bounding box) mantığıyla çalışarak aday kümesini hızla daraltır.
-- Uzamsal indeks: uzamsal her sütun için ŞART
CREATE INDEX idx_subeler_konum ON subeler USING GIST (konum);
-- Büyük tablolarda daha küçük ve hızlı bir alternatif (PostGIS 2.3+)
-- CREATE INDEX idx_subeler_konum ON subeler USING SPGIST (konum);
ANALYZE subeler;
İndeksin gerçekten kullanılması için sorgunuzu doğru yazmanız gerekir. En yaygın hata, mesafeyi hesaplayıp sonucu filtrelemektir:
-- YANLIŞ: her satır için mesafe hesaplanır, indeks kullanılamaz
SELECT ad FROM subeler
WHERE ST_Distance(konum::geography, ST_MakePoint(29.02,41.01)::geography) < 5000;
-- DOĞRU: ST_DWithin indeksi kullanır, aday kümesini önce daraltır
SELECT ad FROM subeler
WHERE ST_DWithin(
konum::geography,
ST_SetSRID(ST_MakePoint(29.02, 41.01), 4326)::geography,
5000 -- metre
);
ST_DWithin fonksiyonu içeride önce bir sınırlayıcı kutu karşılaştırması yapar ve bu karşılaştırma GiST indeksinden faydalanabilir; sonra yalnızca hayatta kalan az sayıda satır için gerçek mesafeyi hesaplar. Bu ikili yapı sayesinde milyonlarca satırlık tabloda bile milisaniyeler içinde cevap alırsınız. Farkı görmek için sorguyu EXPLAIN ANALYZE ile karşılaştırın; çıktı okuma yöntemini EXPLAIN ANALYZE çıktısını okuma rehberinde bulabilirsiniz.
"En yakın N kayıt" sorusu için ise <-> mesafe operatörü kullanılır. Bu operatör GiST indeksinin K-nearest-neighbour yeteneğini tetikler ve tabloyu hiç taramadan sıralı sonuç verir:
-- Verilen noktaya en yakın 10 şube
SELECT ad, il,
ST_Distance(konum::geography, ST_SetSRID(ST_MakePoint(29.02,41.01),4326)::geography) AS metre
FROM subeler
ORDER BY konum <-> ST_SetSRID(ST_MakePoint(29.02, 41.01), 4326)
LIMIT 10;
Dikkat: ORDER BY içindeki ifade indeksle aynı tipte olmalıdır. konum sütunu geometry ise operatörün sağ tarafı da geometry olmalı; cast ederseniz indeks devre dışı kalır. Bu yüzden yukarıdaki sorguda sıralama geometry üzerinde, mesafe gösterimi ise geography üzerinde yapılıyor.
Poligonlar, Kapsama Sorguları ve GeoJSON#
Nokta verisi kadar sık ihtiyaç duyulan bir diğer işlem, bir noktanın belirli bir alanın içinde olup olmadığını sorgulamaktır: teslimat bölgesi, hizmet alanı, mahalle sınırı. Bunun için poligon geometrileri ve ST_Contains / ST_Within fonksiyonları kullanılır.
CREATE TABLE teslimat_bolgeleri (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ad text NOT NULL,
alan geometry(Polygon, 4326) NOT NULL
);
CREATE INDEX idx_bolge_alan ON teslimat_bolgeleri USING GIST (alan);
-- Basit bir dörtgen bölge (köşeler kapalı olmalı: ilk nokta = son nokta)
INSERT INTO teslimat_bolgeleri (ad, alan) VALUES
('Merkez Bölge', ST_GeomFromText(
'POLYGON((28.95 40.95, 29.10 40.95, 29.10 41.05, 28.95 41.05, 28.95 40.95))', 4326));
-- Bir müşteri adresi hangi bölgeye düşüyor?
SELECT b.ad
FROM teslimat_bolgeleri b
WHERE ST_Contains(b.alan, ST_SetSRID(ST_MakePoint(29.02, 41.01), 4326));
Poligonlarda en sık karşılaşılan hata, halkanın kapatılmamasıdır: ilk ve son koordinat çifti birebir aynı olmalıdır, aksi halde PostGIS geometriyi reddeder. İkinci sık hata ise kendisiyle kesişen (self-intersecting) poligonlardır; bunlar teknik olarak saklanabilir ama fonksiyonlarda beklenmedik sonuç verir. Geçerliliği her zaman kontrol edin ve gerekirse onarın:
-- Geçersiz geometrileri bul
SELECT id, ad, ST_IsValidReason(alan)
FROM teslimat_bolgeleri
WHERE NOT ST_IsValid(alan);
-- Onar (PostGIS 3.0+)
UPDATE teslimat_bolgeleri SET alan = ST_MakeValid(alan) WHERE NOT ST_IsValid(alan);
Web uygulamalarıyla veri alışverişinde GeoJSON standarttır ve PostGIS doğrudan üretebilir. Bu, harita katmanını beslemenin en pratik yoludur:
-- Tek bir kaydı GeoJSON olarak al
SELECT ST_AsGeoJSON(konum) FROM subeler WHERE ad = 'Kadıköy Şube';
-- {"type":"Point","coordinates":[29.027,40.99]}
-- Tüm tabloyu tek bir FeatureCollection olarak üret
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(konum)::json,
'properties', json_build_object('ad', ad, 'il', il)
))
) FROM subeler;
Çıktıyı incelemek veya biçimlendirmek için JSON formatlayıcı aracımızı kullanabilirsiniz. GeoJSON çıktısını doğrudan uygulamanıza döndürürseniz, sunucu tarafında dönüşüm kodu yazmaktan kurtulursunuz.
Performans Tuzakları ve Sık Yapılan Hatalar#
Cast yüzünden kaybolan indeks. WHERE konum::geography && ... yazdığınızda geometry üzerine kurulu GiST indeksi kullanılamaz. İki çözüm var: ya sütunu baştan geography tanımlayın, ya da cast edilmiş ifade üzerinde ayrı bir indeks kurun: CREATE INDEX ON subeler USING GIST ((konum::geography));
Yanlış koordinat sırası. ST_MakePoint(enlem, boylam) yazmak en sık hatadır. PostGIS X (boylam), Y (enlem) sırası bekler. Veriyi yükledikten sonra birkaç kaydı ST_AsText ile okuyup gözle doğrulayın; Türkiye için boylam 26-45, enlem 36-42 aralığında olmalıdır.
SRID uyuşmazlığı. İki farklı SRID'deki geometriyi karşılaştırmaya çalışırsanız PostGIS "Operation on mixed SRID geometries" hatası verir. Sütunu geometry(Point, 4326) gibi SRID kısıtıyla tanımlamak bu hatayı veri girişi anında yakalar; SRID'siz geometry tanımı sorunu sorgu anına erteler.
ANALYZE unutmak. PostGIS istatistikleri uzamsal dağılımı da içerir ve planlayıcı bunlara göre indeks kullanıp kullanmayacağına karar verir. Toplu veri yüklemesinden sonra ANALYZE çalıştırmazsanız planlayıcı tabloyu boş sanıp Seq Scan seçebilir.
Aşırı ayrıntılı poligonlar. Ülke veya il sınırı gibi on binlerce köşesi olan geometrilerle yapılan kesişim sorguları çok pahalıdır. Harita üzerinde göstermek için ST_Simplify ile basitleştirilmiş bir kopya tutun, hassas hesap gerektiğinde orijinali kullanın. İki farklı amaca iki farklı sütun ayırmak, tek bir "her işe yarayan" geometriden çok daha hızlıdır.
Bellek ve disk beklentisi. PostGIS tabloları, özellikle poligon ve çok noktalı geometriler saklıyorsa hızla büyür ve GiST indeksleri de kayda değer yer kaplar. Uzamsal iş yükü planlarken diskin yanı sıra shared_buffers ve work_mem değerlerini de gözden geçirin.
Sıkça Sorulan Sorular#
PostGIS ücretsiz mi#
Evet, PostGIS tamamen açık kaynaklıdır ve GPL v2 lisansıyla dağıtılır; ticari kullanım dahil hiçbir lisans ücreti yoktur. Altında çalışan GEOS, PROJ ve GDAL kütüphaneleri de açık kaynaktır. Tek maliyetiniz, uzamsal verinin gerektirdiği disk alanı ve hesaplama kaynağıdır. Paylaşımlı hosting paketlerinde uzantı kurulumuna genelde izin verilmediği için kendi sunucunuza ihtiyaç duyarsınız.
geometry mi geography mi kullanmalıyım#
Verileriniz tek bir ülke veya şehir ölçeğindeyse ve kesişim, tampon, birleşim gibi geometrik işlemler yapacaksanız geometry daha hızlı ve daha çok fonksiyon destekler. Veriler kıtalar arasına yayılıyorsa veya mesafeleri metre cinsinden doğru hesaplamanız kritikse geography kullanın. Yaygın bir orta yol, sütunu geometry(Point, 4326) olarak saklayıp mesafe gerektiren sorgularda ::geography cast'i uygulamaktır.
ST_Distance neden metre yerine garip küçük sayılar döndürüyor#
Çünkü sütununuz SRID 4326 ile tanımlı bir geometry ve bu sistemin birimi derecedir. ST_Distance size derece cinsinden düzlemsel bir uzaklık verir; 0.0134 gibi bir sayı görürsünüz. Metre istiyorsanız ya iki tarafı da ::geography ile cast edin ya da ST_Transform ile metrik bir projeksiyona (örneğin 3857 veya Türkiye için 5254) dönüştürün. Sabit bir çarpanla dereceyi metreye çevirmeye çalışmak enleme bağlı hata üretir.
Uzamsal indeksin kullanıldığını nasıl doğrularım#
Sorgunuzu EXPLAIN ANALYZE ile çalıştırın ve planda Index Scan using idx_..._konum veya Bitmap Index Scan ifadesini arayın. Seq Scan görüyorsanız ya indeks yoktur, ya cast yüzünden devre dışı kalmıştır, ya da tablo istatistikleri güncel değildir. Küçük tablolarda planlayıcının bilinçli olarak Seq Scan seçtiğini de unutmayın; testi gerçekçi büyüklükte bir veri kümesiyle yapın.
PostGIS ile ne kadar büyük veri saklanabilir#
PostgreSQL'in kendi sınırları geçerlidir ve pratikte darboğaz genelde disk ile bellektir, satır sayısı değil. On milyonlarca nokta içeren tablolar uygun GiST indeksleriyle rahatça çalışır. Sorun yaratan şey satır sayısından çok geometrilerin karmaşıklığıdır; on binlerce köşeli poligonlarla yapılan kesişim işlemleri, milyonlarca basit noktadan çok daha pahalıdır. Büyük veri kümelerinde tabloyu bölgeye göre bölümlemek de düşünülebilir.
Adresten koordinat üretmek (geocoding) PostGIS ile mümkün mü#
PostGIS'in postgis_tiger_geocoder uzantısı bu işi yapar ama ABD adres veri kümesi üzerine kuruludur ve Türkiye adresleri için doğrudan kullanışlı değildir. Türkiye'de pratik yol, harici bir geocoding servisinden koordinatı alıp PostGIS'te saklamaktır; PostGIS'i adres çözümleyici olarak değil, koordinat saklama ve uzamsal sorgulama katmanı olarak konumlandırın. Kendi adres veri kümenizi kurup arama yapmak isterseniz pg_trgm uzantısıyla metin benzerliği araması iyi bir tamamlayıcıdır.
Kapanış#
PostGIS ile çalışırken aklınızda tutmanız gereken birkaç şey var: koordinatları 4326 ile saklayın ve metre gerektiğinde geography'ye cast edin, ST_MakePoint çağrısında boylamı önce yazın, her uzamsal sütuna GiST indeksi kurun ve mesafe filtrelerini ST_Distance ile değil ST_DWithin ile yazın. Bu dört alışkanlık, uzamsal sorgularınızın hem doğru hem hızlı olmasını neredeyse garantiler; geri kalan her şey fonksiyon kütüphanesini keşfetmekten ibarettir.
PostGIS bir uzantı olduğu için kurulumu paylaşımlı hosting yerine kendi sunucunuzu gerektirir. Root erişimli VDS ya da kaynağı büyütülebilen bulut sunucu paketlerimizde uzantıyı kurup shared_buffers gibi ayarları uzamsal iş yüküne göre ayarlayabilirsiniz; ağır geometrik hesaplamalar için dedicated sunucu daha kararlı bir CPU tabanı verir. Kurulum, indeks tasarımı ve düzenli bakımı devretmek isterseniz sunucu yönetimi hizmetimiz bu yükü üstlenir.