Pertama kali saya coba bikin query "cari semua toko dalam radius 5 km dari lokasi user" pakai MySQL biasa, hasilnya... bikin pusing. Harus hitung Haversine manual, pakai SIN, COS, RADIANS query-nya panjang banget dan lambat. Pas ketemu PostGIS, rasanya kayak nemu cheat code. Satu baris query, hasilnya presisi, dan cepat banget.
PostGIS itu extension untuk PostgreSQL yang nambahin kemampuan spatial bisa nyimpan data geometri, query berdasarkan lokasi, hitung jarak, cari intersection, dan banyak lagi. Kalau kamu pernah pakai Google Maps API atau Leaflet, PostGIS ini versi database-nya. Semua perhitungan geospasial terjadi di level database, jadi aplikasi tinggal terima hasilnya.
MySQL memang punya spatial functions, tapi fiturnya terbatas banget. PostGIS support ratusan fungsi spatial, mulai dari yang sederhana kayak ST_Distance sampai yang kompleks kayak ST_ClusterDBSCAN buat clustering data. Plus, performanya jauh lebih baik karena pakai spatial index berbasis R-Tree.
Beberapa hal yang bikin PostGIS unggul:
ST_Buffer, ST_Union, ST_Within, sampai ST_VoronoiPolygons. Hampir semua operasi geospasial ada.ST_AsGeoJSON() dan data siap dipakai di Leaflet atau OpenLayers.Kalau kamu sudah punya PostgreSQL, tinggal tambahin extension-nya. Kalau belum, install dulu:
# Install PostgreSQL dan PostGIS
sudo apt update
sudo apt install postgresql postgresql-contrib postgis postgresql-16-postgis-3
# Cek versi PostGIS yang terinstall
apt list --installed | grep postgis
# Restart PostgreSQL biar extension ke-load
sudo systemctl restart postgresql
Setelah itu, masuk ke database dan enable PostGIS:
# Login sebagai user postgres
sudo -u postgres psql
# Buat database khusus untuk project GIS
CREATE DATABASE gis_project;
# Connect ke database baru
\c gis_project
# Enable PostGIS extension
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
# Verifikasi instalasi
SELECT PostGIS_Version();
Output-nya kurang lebih kayak gini: 3.4 USE_GEOS=1 USE_PROJ=1 USE_STATS=1. Kalau muncul versi, berarti PostGIS sudah aktif dan siap dipakai.
PostGIS nambahin tipe data khusus: GEOMETRY dan GEOGRAPHY. Untuk sebagian besar kasus, pakai GEOGRAPHY kalau data kamu pakai koordinat lat/lng (WGS84 / SRID 4326). Pakai GEOMETRY kalau sudah punya data terproyeksi (misalnya UTM).
-- Tabel toko dengan kolom lokasi tipe GEOGRAPHY
CREATE TABLE stores (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
address TEXT,
category VARCHAR(50),
location GEOGRAPHY(POINT, 4326),
created_at TIMESTAMP DEFAULT NOW()
);
-- Buat spatial index untuk query cepat
CREATE INDEX idx_stores_location ON stores USING GIST (location);
-- Tabel area layanan (polygon)
CREATE TABLE service_areas (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
coverage GEOGRAPHY(POLYGON, 4326),
priority_level INT DEFAULT 1
);
CREATE INDEX idx_service_areas_coverage ON service_areas USING GIST (coverage);
Perhatikan GIST (location) ini spatial index yang bikin query berbasis lokasi jadi cepat. Tanpa index ini, query radius 5 km ke tabel dengan 1 juta baris bisa makan waktu puluhan detik. Dengan index, hitungan milidetik.
Ada beberapa cara insert data geometri ke PostGIS. Yang paling umum pakai WKT (Well-Known Text) atau GeoJSON:
-- Insert pakai ST_MakePoint (longitude, latitude)
INSERT INTO stores (name, address, category, location) VALUES
('Kopi Kenangan Sudirman', 'Jl. Jend. Sudirman Kav. 52-53', 'cafe',
ST_SetSRID(ST_MakePoint(106.809861, -6.208761), 4326)::geography),
('Warung Tekko SCBD', 'SCBD Lot 8', 'restaurant',
ST_SetSRID(ST_MakePoint(106.810500, -6.210200), 4326)::geography),
('Gramedia Grand Indonesia', 'Grand Indonesia Lt. 3', 'bookstore',
ST_SetSRID(ST_MakePoint(106.819400, -6.195100), 4326)::geography),
('Indomaret Karet', 'Jl. Karet Pasar Baru', 'minimarket',
ST_SetSRID(ST_MakePoint(106.807200, -6.205400), 4326)::geography),
('Alfamart Setiabudi', 'Jl. Setiabudi Raya', 'minimarket',
ST_SetSRID(ST_MakePoint(106.821100, -6.207800), 4326)::geography);
-- Insert pakai ST_GeomFromText (WKT format)
INSERT INTO service_areas (name, coverage) VALUES
('Area Sudirman', ST_GeomFromText(
'POLYGON((106.805 -6.215, 106.815 -6.215, 106.815 -6.200, 106.805 -6.200, 106.805 -6.215))',
4326
)::geography);
Penting: perhatikan urutan koordinat. PostGIS pakai format (longitude, latitude), bukan (latitude, longitude). Ini sering bikin orang bingung karena Google Maps pakai kebalikannya. Kalau koordinat kamu terbalik, data akan muncul di tempat yang salah pernah saya debug hampir 2 jam gara-gara ini.
Ini query paling populer cari lokasi terdekat dari posisi user:
-- Cari semua toko dalam radius 2 km dari titik tertentu
-- Titik: Bundaran HI (-6.1900, 106.8220)
SELECT
name,
category,
address,
ST_Distance(
location,
ST_SetSRID(ST_MakePoint(106.8220, -6.1900), 4326)::geography
) AS distance_meters
FROM stores
WHERE ST_DWithin(
location,
ST_SetSRID(ST_MakePoint(106.8220, -6.1900), 4326)::geography,
2000 -- radius dalam meter
)
ORDER BY distance_meters ASC;
ST_DWithin itu kuncinya dia pakai spatial index untuk filter, jadi nggak perlu scan semua baris. Beda dengan bikin query manual pakai Haversine yang harus hitung ke setiap baris dulu. Di tabel 1 juta data, bedanya bisa 100x lebih cepat.
-- Jarak antara dua toko (dalam meter)
SELECT
a.name AS toko_asal,
b.name AS toko_tujuan,
ST_Distance(a.location, b.location) AS jarak_meter
FROM stores a, stores b
WHERE a.name = 'Kopi Kenangan Sudirman'
AND b.name = 'Gramedia Grand Indonesia';
-- 3 toko terdekat dari lokasi user
SELECT
name,
category,
address,
ROUND(ST_Distance(
location,
ST_SetSRID(ST_MakePoint(106.8220, -6.1900), 4326)::geography
)::numeric, 0) AS distance_meters
FROM stores
ORDER BY location <-> ST_SetSRID(ST_MakePoint(106.8220, -6.1900), 4326)::geography
LIMIT 3;
Operator <-> itu "nearest neighbor" operator dia pakai spatial index untuk sort berdasarkan jarak tanpa perlu hitung ke semua baris. Super efisien untuk "cari N terdekat".
-- Cek apakah lokasi user ada di dalam area layanan
SELECT
name,
ST_Covers(
coverage,
ST_SetSRID(ST_MakePoint(106.8100, -6.2080), 4326)::geography
) AS is_inside
FROM service_areas;
-- Luas area layanan dalam meter persegi
SELECT
name,
ROUND(ST_Area(coverage)::numeric, 2) AS area_sqm,
ROUND((ST_Area(coverage) / 1000000)::numeric, 4) AS area_sqkm
FROM service_areas;
Spatial join itu fitur powerful yang nggak dimiliki database biasa. Kamu bisa gabungkan data dari dua tabel berdasarkan hubungan spasial misalnya "toko mana yang ada di dalam area layanan tertentu":
-- Toko yang berada di dalam area layanan "Area Sudirman"
SELECT
s.name AS store_name,
s.category,
sa.name AS service_area
FROM stores s
JOIN service_areas sa ON ST_Covers(sa.coverage, s.location);
-- Jumlah toko per area layanan
SELECT
sa.name AS service_area,
COUNT(s.id) AS store_count
FROM service_areas sa
LEFT JOIN stores s ON ST_Covers(sa.coverage, s.location)
GROUP BY sa.name
ORDER BY store_count DESC;
Spatial join juga bisa dipakai untuk kasus yang lebih kompleks. Misalnya kamu punya tabel kecamatan (polygon) dan tabel pelanggan (point), mau tahu ada berapa pelanggan per kecamatan tinggal JOIN ... ON ST_Within(customer.location, district.boundary).
Salah satu alasan utama pakai PostGIS: data bisa langsung dikonversi ke GeoJSON. Format ini yang dipakai Leaflet, OpenLayers, Mapbox, dan hampir semua library web map:
-- Konversi hasil query ke GeoJSON
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(location)::json,
'properties', json_build_object(
'id', id,
'name', name,
'category', category,
'address', address
)
)
)
) AS geojson
FROM stores;
Hasilnya bisa langsung dipakai di Leaflet:
// Fetch GeoJSON dari API endpoint yang query PostGIS
fetch('/api/stores/geojson')
.then(res => res.json())
.then(data => {
L.geoJSON(data, {
pointToLayer: (feature, latlng) => {
return L.marker(latlng);
},
onEachFeature: (feature, layer) => {
layer.bindPopup(`
<strong>${feature.properties.name}</strong><br>
${feature.properties.category}<br>
${feature.properties.address}
`);
}
}).addTo(map);
});
ST_Buffer bikin polygon baru dari titik/line/polygon dengan jarak tertentu. Ini sangat berguna untuk analisis misalnya "buat area 500 meter di sepanjang jalan utama":
-- Buat buffer 500m di sekitar setiap toko
SELECT
name,
ST_AsGeoJSON(ST_Buffer(location, 500))::json AS buffer_polygon
FROM stores;
-- Cari toko yang overlap area buffer-nya (saling dekat)
SELECT
a.name AS toko_1,
b.name AS toko_2,
ST_Distance(a.location, b.location) AS jarak_meter
FROM stores a, stores b
WHERE a.id < b.id
AND ST_DWithin(a.location, b.location, 1000)
ORDER BY jarak_meter;
Buat yang pakai CI4, integrasi PostGIS cukup mudah. Pastikan driver database pakai PostgreSQL, lalu query spasial biasa:
<?php
namespace App\Models;
use CodeIgniter\Model;
class StoreModel extends Model
{
protected $table = 'stores';
protected $primaryKey = 'id';
protected $allowedFields = ['name', 'address', 'category', 'location'];
// Cari toko terdekat dari koordinat user
public function findNearby(float $lat, float $lng, int $radiusMeters = 2000, int $limit = 10)
{
$point = "ST_SetSRID(ST_MakePoint({$lng}, {$lat}), 4326)::geography";
return $this->select("
id, name, address, category,
ST_Y(location::geometry) AS latitude,
ST_X(location::geometry) AS longitude,
ROUND(ST_Distance(location, {$point})::numeric, 0) AS distance_meters
")
->where("ST_DWithin(location, {$point}, {$radiusMeters})")
->orderBy("location <-> {$point}")
->limit($limit)
->findAll();
}
// Insert toko dengan koordinat
public function insertWithLocation(array $data, float $lat, float $lng)
{
$data['location'] = "ST_SetSRID(ST_MakePoint({$lng}, {$lat}), 4326)::geography";
return $this->insert($data);
}
}
GEOGRAPHY bukan GEOMETRY. Geography otomatis hitung di atas bola bumi, jadi hasilnya akurat tanpa proyeksi manual.ST_DWithin pakai index untuk filter, sedangkan WHERE ST_Distance(...) < N harus hitung jarak ke semua baris dulu baru filter.ORDER BY location <-> point LIMIT N. Ini memanfaatkan index untuk sort tanpa hitung semua jarak.VACUUM ANALYZE stores; supaya statistik tabel update dan query planner bisa bikin rencana yang optimal.ST_Simplify(polygon, tolerance) bisa kurangi jumlah titik tanpa mengubah bentuk secara signifikan.Beberapa masalah yang sering saya temui waktu kerja dengan PostGIS:
ST_Transform(geom, 4326).location::geometry atau geom::geography.ST_Buffer dengan radius sangat besar ke polygon kompleks bisa habisin memory. Kurangi radius atau simplify polygon dulu.PostGIS itu game-changer buat siapa yang butuh kemampuan geospasial di level database. Dari sekadar "cari terdekat" sampai analisis spasial kompleks, semuanya bisa dihandle di PostgreSQL. Nggak perlu lagi bikin perhitungan jarak manual di aplikasi biarkan database yang kerja keras.