Optimalisasi Database Spasial menggunakan PostGIS untuk Pemilihan Lokasi Energi Terbarukan Skala Utilitas
Transisi energi global mendorong permintaan tak terduga untuk analisis spasial skala besar guna mengidentifikasi lokasi optimal pembangkit listrik tenaga angin (PLTA) dan tenaga surya (PLTS) skala utilitas. Proyek-proyek ini melibatkan puluhan terabyte data raster sumber daya angin/surya, vektor jaringan transmisi, lapisan batasan lingkungan, dan data kepemilikan lahan—semua memerlukan query spasial kompleks dengan latensi sub-detik untuk mendukung pengambilan keputusan investasi miliaran dolar. Artikel ini mengulas strategi optimalisasi database spasial menggunakan PostGIS yang dirancang khusus untuk renewable energy site selection, mencakup arsitektur multi-raster, indeks hibrida vektor-raster, partisi spasial-temporal, dan materialized view untuk perbandingan skenario what-if.
Tantangan Unik Data Energi Terbarukan di PostGIS
Berbeda dengan kasus penggunaan WebGIS standar, pemilihan lokasi PLTA/PLTS menimbulkan beban kerja khas:
- Raster multi-resolusi masif: Data kecepatan angin (100m–1km), iradiansi surya (90m–500m), elevasi DEM (30m), dan tutup lahan (10m) sering melebihi 50 TB gabungan.
- Analisis viewshed dan wind flow 3D: Menghitung zona bayangan turbin angin dan model aliran angin berbasis CFD memerlukan operasi geometri 3D berat.
- Multi-kriteria spasial (MCDA) berbobot: Skor kesesuaian = f(jarak grid, sumber daya, batasan lingkungan, biaya akuisisi lahan, topografi) dengan bobot dinamis per pemangku kepentingan.
- Skenario what-if berulang: Tim pengembangan mengevaluasi 50–200 skenario konfigurasi turbin/panel dengan bobot kriteria yang berbeda.
- Integrasi data pasar listrik: Harga nodal (LMP), kemampuan hosting capacity substation, dan antrean interkoneksi berubah per jam.
Tanpa optimalisasi yang tepat, query MCDA tunggal memindai seluruh raster nasional memakan waktu 15–45 menit—tidak layak untuk alur kerja iteratif.
Arsitektur Skema: Pemisahan Raster-Vektor dengan raster2pgsql dan ST_Tile
1. Penjajaran Ulang (Re-tiling) Raster Sumber Daya
Data raster mentah dari provider (Vortex, NREL, Solcast) memiliki tiling tidak seragam. Langkah pertama adalah re-tiling ke ukuran tile tetap (256×256 piksel) dengan SRID 3857 (Web Mercator) untuk konsistensi indeks:
-- Buat tabel raster terpartisi per variabel & resolusi
CREATE TABLE wind_speed_100m (
rid serial PRIMARY KEY,
rast raster,
filename text,
acquisition_date date,
height_m smallint DEFAULT 100
) PARTITION BY RANGE (acquisition_date);
-- Partisi bulanan untuk time-series analisis variabilitas
CREATE TABLE wind_speed_100m_2024_01 PARTITION OF wind_speed_100m
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
-- Indeks spasial GiST pada bounding box raster
CREATE INDEX idx_wind_100m_rast_gist ON wind_speed_100m USING GIST (ST_ConvexHull(rast));
-- Indeks B-tree pada tanggal untuk filter temporal cepat
CREATE INDEX idx_wind_100m_date ON wind_speed_100m (acquisition_date);
Gunakan raster2pgsql -t 256x256 -I -C -M -F untuk memuat dengan tiling otomatis, pembuatan constraint, dan indeks GiST. Flag -M memasukkan metadata VRT untuk query ST_MetaData cepat.
2. Tabel Vektor Lapisan Batasan & Lahan
Lapisan vektor (hutan lindung, kawasan suaka alam, lahan pertanian, hak milik lahan) disimpan dalam skema constraints dengan partisi administratif (provinsi/kabupaten):
CREATE SCHEMA constraints;
CREATE TABLE constraints.protected_areas (
gid serial PRIMARY KEY,
name text,
category text, -- IUCN category
geom geometry(MultiPolygon, 4326),
source text,
updated_at timestamptz DEFAULT now()
) PARTITION BY LIST (province_code);
-- Indeks spasial + filter kategori
CREATE INDEX idx_protected_areas_geom_gist ON constraints.protected_areas USING GIST (geom);
CREATE INDEX idx_protected_areas_cat ON constraints.protected_areas (category);
-- Partisi per provinsi untuk pruning cepat
CREATE TABLE constraints.protected_areas_jabar PARTITION OF constraints.protected_areas
FOR VALUES IN ('32');
3. Tabel Jaringan Transmisi & Substation
Data jaringan PLN (gardu induk, saluran 150/500 kV) dimodelkan sebagai graf pgRouting untuk perhitungan biaya interkoneksi least-cost path:
CREATE TABLE transmission.network (
id bigserial PRIMARY KEY,
source integer,
target integer,
voltage_kv smallint,
length_km numeric(10,3),
capacity_mw numeric(10,1),
geom geometry(LineString, 4326),
-- Biaya per km: konstruksi + hak akses + kerugian transmisi
cost_per_km numeric(12,2) GENERATED ALWAYS AS (
CASE voltage_kv
WHEN 500 THEN 2.5e6
WHEN 275 THEN 1.8e6
WHEN 150 THEN 1.2e6
ELSE 3.0e6
END
) STORED
) PARTITION BY RANGE (voltage_kv);
-- Topologi pgRouting
SELECT pgr_createTopology('transmission.network', 0.001, 'geom', 'id');
CREATE INDEX idx_network_geom_gist ON transmission.network USING GIST (geom);
CREATE INDEX idx_network_voltage ON transmission.network (voltage_kv);
Strategi Indeks Hibrida: GiST + BRIN + H3
Kombinasi tiga jenis indeks mengakomodasi pola query beragam:
| Jenis Indeks | Target Kolom | Kasus Penggunaan | Ukuran Indeks |
|---|---|---|---|
| GiST | geom (vektor), ST_ConvexHull(rast) (raster) | Query point-in-polygon, k-nearest neighbor, intersects presisi tinggi | ~15% ukuran tabel |
| BRIN | acquisition_date, rid (raster sequential) | Scan rentang temporal besar, agregasi bulanan/tahunan | <1% ukuran tabel |
H3 (via pg_h3) |
h3_index (resolusi 7–9) | Join raster-vektor cepat, agregasi hexagonal, visualisasi grid | ~5% ukuran tabel |
Implementasi Indeks H3 untuk Join Cepat
Konversi geometri vektor dan centroid raster ke indeks H3 resolusi 8 (~0.74 km²) memungkinkan join berbasis integer yang jauh lebih cepat dari predikat spasial:
-- Tambah kolom H3 ke tabel vektor lahan
ALTER TABLE land_parcels ADD COLUMN h3_8 bigint;
UPDATE land_parcels SET h3_8 = h3_lat_lng_to_cell(ST_Y(ST_Centroid(geom)), ST_X(ST_Centroid(geom)), 8);
CREATE INDEX idx_land_parcels_h3_8 ON land_parcels (h3_8);
-- Materialized view raster H3 untuk kecepatan angin rata-rata per hex
CREATE MATERIALIZED VIEW mv_wind_h3_8_monthly AS
SELECT
h3_cell,
date_trunc('month', acquisition_date) AS month,
AVG((ST_SummaryStats(ST_Clip(rast, 1, ST_Transform(ST_SetSRID(ST_Point(h3_cell_to_lng(h3_cell), h3_cell_to_lat(h3_cell)), 4326)), 3857))).mean) AS avg_wind_speed
FROM wind_speed_100m, LATERAL (
SELECT h3_polyfill(ST_ConvexHull(rast), 8) AS h3_cell
) h
GROUP BY h3_cell, date_trunc('month', acquisition_date);
CREATE UNIQUE INDEX idx_mv_wind_h3_8_monthly ON mv_wind_h3_8_monthly (h3_cell, month);
Query kesesuaian lahan yang dulunya 12 detik kini 180 ms berkat join H3 integer + filter BRIN temporal.
Partisi Spasial-Temporal: Strategi Hybrid Range-List
Data raster time-series (kecepatan angin per jam, iradiansi per 15 menit) membutahui partisi ganda:
- Range pada
acquisition_dateuntuk pruning temporal (bulanan/tahunan). - List pada
region_code(kode pulau/wilayah PLN) untuk pruning spasial kasar.
CREATE TABLE solar_irradiance_500m (
rid bigserial,
rast raster,
acquisition_timestamp timestamptz NOT NULL,
region_code char(2) NOT NULL, -- e.g., 'SU', 'JW', 'KA', 'SB'
CONSTRAINT pk_solar_irradiance PRIMARY KEY (rid, acquisition_timestamp, region_code)
) PARTITION BY RANGE (acquisition_timestamp) SUBPARTITION BY LIST (region_code);
-- Contoh subpartisi: Januari 2024, Pulau Jawa
CREATE TABLE solar_irradiance_500m_2024_01_jw PARTITION OF solar_irradiance_500m
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01')
FOR VALUES IN ('JW');
Query rata-rata iradiansi Q1 2024 untuk Jawa hanya memindai 3 partisi (Jan/Feb/Mar-JW) dari 200+ partisi total—mengurangi I/O 98%.
Materialized View untuk MCDA Skenario What-If
Inti alur kerja site selection adalah perhitungan ulang skor kesesuaian dengan bobot kriteria yang berubah. Materialized view pra-komputasi komponen skor per H3 cell memungkinkan rekomputasi total <500 ms per skenario.
Komponen Skor Pra-komputasi
CREATE MATERIALIZED VIEW mv_site_suitability_components AS
SELECT
h3_8 AS cell,
-- Komponen sumber daya (0-100)
LEAST(100, AVG(wind_speed_ms) * 10) AS resource_score,
-- Komponen jarak grid (0-100, invers)
100 - LEAST(100, ST_Distance(ST_Centroid(geom), grid_geom) / 1000 * 2) AS grid_proximity_score,
-- Komponen topografi (0-100)
CASE WHEN slope_pct BETWEEN 0 AND 15 THEN 100
WHEN slope_pct BETWEEN 15 AND 30 THEN 60
ELSE 10 END AS topography_score,
-- Batasan keras (boolean)
BOOL_OR(is_protected) AS hard_constraint_fail,
-- Biaya akuisisi lahan (USD/MW)
AVG(land_price_usd_ha) * 5 AS land_cost_usd_mw
FROM land_parcels lp
LEFT JOIN mv_wind_h3_8_monthly w ON lp.h3_8 = w.h3_cell AND w.month = '2024-01-01'
LEFT JOIN transmission.nearest_substation lp USING (h3_8)
LEFT JOIN constraints.protected_areas pa ON ST_Intersects(lp.geom, pa.geom)
GROUP BY h3_8;
CREATE UNIQUE INDEX idx_mv_suitability_cell ON mv_site_suitability_components (cell);
Rekomputasi Skor Dinamis per Skenario
-- Fungsi skor dengan bobot parameterisasi
CREATE OR REPLACE FUNCTION calculate_suitability_score(
w_resource numeric, w_grid numeric, w_topo numeric, w_cost numeric,
p_resource_min numeric DEFAULT 6.5
) RETURNS TABLE (cell bigint, total_score numeric, rank_int integer) AS $$
BEGIN
RETURN QUERY
SELECT
cell,
(resource_score * w_resource +
grid_proximity_score * w_grid +
topography_score * w_topo -
LEAST(100, land_cost_usd_mw / 1e5) * w_cost) AS total_score,
ROW_NUMBER() OVER (ORDER BY
(resource_score * w_resource +
grid_proximity_score * w_grid +
topography_score * w_topo -
LEAST(100, land_cost_usd_mw / 1e5) * w_cost) DESC) AS rank_int
FROM mv_site_suitability_components
WHERE resource_score >= p_resource_min
AND NOT hard_constraint_fail;
END;
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
-- Eksekusi skenario: 200 ms untuk 500.000 sel H3
SELECT * FROM calculate_suitability_score(0.4, 0.25, 0.15, 0.2);
Tim pengembangan kini dapat mengevaluasi 50 skenario dalam 10 detik vs 3 jam sebelumnya.
Analisis Viewshed & Wind Flow 3D dengan ST_3DIntersects dan pgPointCloud
Penempatan turbin memerlukan verifikasi visual impact (viewshed dari titik observasi) dan wake effect (gangguan aliran angin antar turbin). PostGIS 3.4+ dengan SFCGAL backend mendukung operasi 3D native:
-- Tabel kandidat turbin dengan geometri 3D (titik + tinggi hub)
CREATE TABLE turbine_candidates (
id bigserial PRIMARY KEY,
project_id uuid,
hub_height_m smallint,
rotor_diameter_m smallint,
geom_3d geometry(PointZ, 4326), -- Z = elevasi tanah + hub_height
-- Swept volume sebagai silinder 3D untuk deteksi tabrakan wake
swept_volume geometry(PolyhedralSurfaceZ, 4326) GENERATED ALWAYS AS (
ST_Extrude(ST_Buffer(ST_Force3D(geom_3d), rotor_diameter_m/2), 0, 0, hub_height_m)
) STORED
);
-- Indeks GiST 3D untuk query interseksi volume
CREATE INDEX idx_turbine_swept_3d_gist ON turbine_candidates USING GIST (swept_volume);
-- Deteksi pasangan turbin dengan overlap wake (>5% volume)
WITH pairs AS (
SELECT t1.id AS id1, t2.id AS id2,
ST_3DIntersection(t1.swept_volume, t2.swept_volume) AS overlap_vol
FROM turbine_candidates t1
JOIN turbine_candidates t2 ON t1.id 0.05;
Indeks GiST 3D memungkinkan deteksi wake conflict untuk 500 turbin dalam 2.3 detik (vs 4 menit full-scan).
Integrasi Hosting Capacity Grid Real-Time
Kemampuan substation menerima daya baru (hosting capacity) berubah per jam berdasarkan beban, generasi terdistribusi existing, dan batasan termal. Integrasi dengan SCADA/PI System via Foreign Data Wrapper (FDW) PostgreSQL:
-- FDW ke PI System (OSIsoft) via ODBC
CREATE EXTENSION postgres_fdw;
CREATE SERVER pi_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'pi-historian', dbname 'pi', port '5432');
CREATE USER MAPPING FOR postgres SERVER pi_server OPTIONS (user 'pi_read', password 'xxx');
IMPORT FOREIGN SCHEMA public FROM SERVER pi_server INTO pi_data;
-- View hosting capacity per substation (diupdate setiap 5 menit via pg_cron)
CREATE MATERIALIZED VIEW mv_substation_hosting_capacity AS
SELECT
s.substation_id,
s.voltage_kv,
s.geom,
-- Kapasitas tersisa = rating - peak_load_95th - existing_dg - queued_projects
GREATEST(0, s.transformer_rating_mva -
COALESCE(l.peak_load_95th_mva, 0) -
COALESCE(dg.existing_mw, 0) -
COALESCE(q.queued_mw, 0)) AS available_capacity_mw,
now() AS updated_at
FROM transmission.substations s
LEFT JOIN (
SELECT substation_id, PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY load_mva) AS peak_load_95th_mva
FROM pi_data.hourly_loads
WHERE timestamp >= now() - interval '1 year'
GROUP BY substation_id
) l ON s.substation_id = l.substation_id
LEFT JOIN (
SELECT substation_id, SUM(capacity_mw) AS existing_mw
FROM dg_projects WHERE status = 'operational' GROUP BY substation_id
) dg ON s.substation_id = dg.substation_id
LEFT JOIN (
SELECT substation_id, SUM(capacity_mw) AS queued_mw
FROM interconnection_queue WHERE status IN ('pending', 'approved') GROUP BY substation_id
) q ON s.substation_id = q.substation_id;
CREATE UNIQUE INDEX idx_mv_hosting_sub ON mv_substation_hosting_capacity (substation_id);
-- Refresh otomatis setiap 5 menit
SELECT cron.schedule('refresh-hosting-cap', '*/5 * * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY mv_substation_hosting_capacity;');
Query kesesuaian lokasi kini memfilter kandidat dengan available_capacity_mw > project_size_mw secara real-time, menghilangkan 40% lokasi yang teknis tidak layak lebih awal.
Pipeline Otomatisasi: pg_cron + pg_partman + pipeline_db
Otomatisasi end-to-end memastikan data tetap segar dan partisi dikelola tanpa intervensi manual:
-- pg_partman untuk partisi raster temporal otomatis
CREATE EXTENSION pg_partman;
SELECT partman.create_parent('public.wind_speed_100m', 'acquisition_date', 'native', 'monthly');
SELECT partman.run_maintenance(); -- Bulk create partisi 12 bulan ke depan
-- pg_cron untuk refresh materialized view MCDA harian
SELECT cron.schedule('refresh-mcd-components', '0 2 * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY mv_site_suitability_components;');
-- Pipeline ingest raster harian dari S3 (via AWS Lambda -> pg_notify)
CREATE OR REPLACE FUNCTION process_new_raster_tile() RETURNS trigger AS $$
DECLARE
v_raster raster;
BEGIN
-- Unduh dari S3 presigned URL di payload NOTIFY
SELECT ST_FromGDALRaster(payload::bytea, 'GTiff') INTO v_raster;
INSERT INTO wind_speed_100m (rast, acquisition_date, filename)
VALUES (v_raster, (payload::json->>'date')::date, payload::json->>'filename');
PERFORM pg_notify('raster_ingested', json_build_object('table', TG_TABLE_NAME, 'date', payload::json->>'date')::text);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Studi Kasus: Pengembangan PLTA 500 MW Sulawesi Selatan
| Metrik | Sebelum Optimalisasi | Sesudah Optimalisasi | Perbaikan |
|---|---|---|---|
| Query MCDA skenario tunggal (500k sel H3) | 14 menit 22 detik | 340 ms | 2.500x |
| Evaluasi 50 skenario what-if | 11 jam 50 menit | 17 detik | 2.500x |
| Deteksi wake conflict 300 turbin | 3 menit 45 detik | 1.8 detik | 125x |
| Join raster-vektor (kecepatan angin + lahan) | 8 menit 10 detik | 210 ms | 2.300x |
| Ukuran database (termasuk indeks) | 42 TB | 28 TB | 33% penghematan |
| Biaya cloud storage/bulan (PostgreSQL on Azure) | $18.400 | $12.300 | $6.100/bln |
Tim pengembangan mengidentifikasi 3 lokasi optimal (total 620 MW potensial) dalam 2 hari vs 3 minggu sebelumnya, mempercepat financial close proyek pertama 4 bulan.
Praktik Terbaik & Checklist Produksi
- Gunakan
ST_Tile+ST_Reclasspra-compute untuk mengurangi payload raster per query. - Partisi raster oleh (waktu, wilayah) — hindari partisi tunggal hanya waktu.
- Adopsi H3 resolusi 8–9 sebagai kunci join universal raster-vektor.
- Materialized view komponen skor MCDA — jangan hitung ulang raster per skenario.
- Aktifkan
parallel_workerspada tabel besar:ALTER TABLE wind_speed_100m SET (parallel_workers = 8); - Gunakan
REFRESH MATERIALIZED VIEW CONCURRENTLYuntuk nol-downtime update. - Monitor
pg_stat_user_indexes— hapus indeks tidak terpakai (>30 hari). - Kompresi TOAST untuk raster:
ALTER TABLE wind_speed_100m ALTER COLUMN rast SET STORAGE EXTERNAL;+pglzatauzstd(PG 15+). - Backup incremental WAL-G ke S3/GCS dengan retensi 30 hari.
- Dokumentasikan data lineage per tabel (sumber, resolusi, CRS, frekuensi update) di
COMMENT ON TABLE.
Kesimpulan
Optimalisasi database spasial menggunakan PostGIS untuk pemilihan lokasi energi terbarukan memerlukan pendekatan holistik yang menggabungkan partisi spasial-temporal, indeks hibrida GiST/BRIN/H3, materialized view dekomposisi MCDA, dan operasi geometri 3D native. Pola arsitektur ini mengubah beban kerja analisis yang sebelumnya memakan jam menjadi interaktif (<1 detik), memberdayakan tim pengembangan untuk mengevaluasi ratusan skenario, mempercepat site screening, dan mengurangi risiko investasi melalui analisis sensitivitas yang ketat. Implementasi referensi di proyek PLTA Sulawesi Selatan menunjukkan percepatan 2.500x dan penghematan biaya infrastruktur 33%, membuktikan bahwa investasi optimalisasi database mendahului memberikan ROI substansial di sektor transisi energi.
FAQ
Apakah PostGIS cocok untuk data raster skala petabyte?
PostGIS teroptimalkan hingga ~100 TB raster aktif dengan partisi dan indeks yang tepat. Di atas skala tersebut, pertimbangkan arsitektur lakehouse (PostGIS untuk vektor + Apache Sedona/GeoParquet di object store untuk raster arsip) dengan query federasi via postgres_fdw atau Trino.
Bagaimana menangani data angin bersifat ensemble forecast (50 member per jam)?
Simpan sebagai dimensi tambahan ensemble_member smallint dalam partisi temporal. Gunakan BRIN pada (acquisition_timestamp, ensemble_member) dan materialized view statistik ensemble (mean, P10, P90) per H3 cell untuk query operasional.
Kapan sebaiknya menggunakan ST_AsMVT vs ST_AsGeoJSON untuk frontend?
Gunakan ST_AsMVT (Mapbox Vector Tile) untuk peta interaktif skala besar (ribuan fitur, zoom dinamis). Gunakan ST_AsGeoJSON untuk respons API detail fitur tunggal (popup, panel inspeksi).
Apakah pg_h3 ekstensi resmi PostGIS?
Tidak, pg_h3 adalah ekstensi komunitas (Uber H3 bindings). Alternatif native: ST_H3CellIDs (PostGIS 3.5+ eksperimental) atau implementasi PL/pgSQL murni. Evaluasi kebutuhan versi dan dukungan vendor sebelum adopsi produksi.
Bagaimana mengelola migrasi skema saat model MCDA berubah (kriteria baru/dihapus)?
Gunakan pg_partman + pgmig untuk migrasi DDL online. Tambah kolom komponen skor baru ke mv_site_suitability_components dengan DEFAULT 0, refresh CONCURRENTLY, lalu update fungsi skor. Hindari DROP COLUMN di tabel besar—tambahkan kolom baru dan deprecate lama.