GIS

PostGIS Spatial Query untuk Aplikasi Web - Panduan Praktis PostgreSQL

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.

Apa yang Bikin PostGIS Spesial?

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:

  • Geometric vs Geography types: Geometry pakai koordinat datar (projected), Geography pakai koordinat bola bumi (lat/lng). Untuk hitung jarak di permukaan bumi, pakai Geography hasilnya akurat tanpa perlu konversi manual.
  • Spatial Index (GiST): Index khusus untuk data geometri. Query "cari dalam radius" atau "temukan yang beririsan" jadi super cepat karena nggak perlu scan semua baris.
  • 3000+ fungsi: Dari ST_Buffer, ST_Union, ST_Within, sampai ST_VoronoiPolygons. Hampir semua operasi geospasial ada.
  • Support GeoJSON, KML, WKT, WKB: Bisa import/export dari berbagai format. Tinggal panggil ST_AsGeoJSON() dan data siap dipakai di Leaflet atau OpenLayers.

Install PostGIS di Ubuntu

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.

Buat Tabel dengan Kolom Geometri

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.

Insert Data Spasial

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.

Query Spasial yang Sering Dipakai

1. Cari Semua Toko dalam Radius Tertentu

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.

2. Hitung Jarak Antara Dua Titik

-- 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. Cari Toko Terdekat dengan LIMIT

-- 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".

4. Cek Apakah Titik Ada di Dalam Polygon

-- 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;

5. Hitung Luas Area

-- 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 Joins Gabungkan Data dari Dua Tabel

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).

Konversi ke GeoJSON untuk Web Map

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);
    });

Buffer dan Analisis Area

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;

PostGIS dengan CodeIgniter 4

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);
    }
}

Tips Optimasi Performa

  • Selalu pakai spatial index: Tanpa GiST index, query spasial sama lambatnya dengan full table scan. Pastikan index dibuat setelah insert data dalam jumlah besar index maintenance itu ada cost-nya.
  • Pakai GEOGRAPHY untuk data GPS: Kalau data pakai koordinat lat/lng, pakai tipe GEOGRAPHY bukan GEOMETRY. Geography otomatis hitung di atas bola bumi, jadi hasilnya akurat tanpa proyeksi manual.
  • ST_DWithin lebih cepat dari ST_Distance + WHERE: ST_DWithin pakai index untuk filter, sedangkan WHERE ST_Distance(...) < N harus hitung jarak ke semua baris dulu baru filter.
  • Operator <-> untuk nearest neighbor: Kalau cuma butuh N hasil terdekat, pakai ORDER BY location <-> point LIMIT N. Ini memanfaatkan index untuk sort tanpa hitung semua jarak.
  • Vacuum dan analyze secara berkala: Setelah bulk insert/update besar, jalankan VACUUM ANALYZE stores; supaya statistik tabel update dan query planner bisa bikin rencana yang optimal.
  • Pakai ST_Simplify untuk data polygon besar: Polygon dengan ribuan titik bikin query lambat. ST_Simplify(polygon, tolerance) bisa kurangi jumlah titik tanpa mengubah bentuk secara signifikan.

Troubleshooting Umum

Beberapa masalah yang sering saya temui waktu kerja dengan PostGIS:

  • Koordinat terbalik: PostGIS pakai (longitude, latitude), Google Maps pakai (latitude, longitude). Kalau data muncul di Samudra Atlantik, coba swap koordinatnya.
  • SRID mismatch: Kalau error "Operation on mixed SRID geometries", pastikan semua data pakai SRID yang sama. Convert pakai ST_Transform(geom, 4326).
  • Geometry vs Geography casting: Beberapa fungsi cuma work di tipe Geometry. Convert pakai location::geometry atau geom::geography.
  • Memory error di buffer besar: 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.


You may also like


0 Comments


Leave a Reply

Comments with links or spam keywords will be rejected.
Scroll to Top