Lewati ke isi
Profil penulisSeri Buku GIS Kehutanan dan Pertanian/ M2
Tampilkan bagian untuk:

BAB 2: Bertanya pada Peta: SQL Spasial

#Studi kasus: "Berapa kejadian di tiap petak?"

Kepala Seksi mengirim tiga pertanyaan sekaligus. Berapa kejadian di tiap petak? Berapa tebangan liar yang terjadi dekat jalan? Pos jaga mana yang paling dekat dengan tiap titik panas? Dengan SQL spasial, tiap pertanyaan itu cukup satu kueri, dan jawabannya keluar dalam hitungan milidetik.

#Konsep: SQL spasial dalam tiga kalimat

SQL spasial adalah SQL biasa ditambah fungsi yang mengerti letak dan bentuk, dan namanya selalu berawalan ST_. Bayangkan bertanya kepada pustakawan: "Buku mana yang ada di rak ini?" Di sini rak itu adalah poligon petak. Satu kueri bisa menggabungkan tabel berdasarkan letak, bukan hanya berdasarkan nilai kolom.

Istilah baru bab ini:

  • Predikat spasial: fungsi yang menjawab ya atau tidak, misalnya ST_Intersects (bersentuhan) dan ST_DWithin (berjarak paling jauh sekian).
  • Pengukur: fungsi yang menjawab angka, misalnya ST_Area dan ST_Distance.
  • Tetangga terdekat (KNN): mengurutkan hasil menurut kedekatan dengan operator <->.
  • View: kueri yang disimpan dan bisa dibuka seperti tabel.
  • EXPLAIN: perintah untuk melihat cara server menjalankan kueri.
Ilustrasi 2.1: Predikat spasial
Skema empat pertanyaan spasial: bersentuhan, di dalam, berjarak paling jauh d, dan tetangga terdekat

#Bagian A: QGIS

#Bagian A: SQL spasial di PostGIS, dijalankan lewat QGIS atau psql

Berkas m2_02a_kueri_dasar.sql berisi semua kueri di bawah. Anda bisa menjalankannya dengan psql -f, atau menempelkannya di DB Manager QGIS: Database ► DB Manager, pilih koneksi, lalu buka jendela SQL. Klik Execute. Centang Load as new layer bila hasilnya punya kolom bentuk dan kunci unik. [CEK: letak menu]

#A1. Mengukur: luas tiap petak

sql
SELECT kode, jenis_tegakan, ROUND((ST_Area(geom) / 10000.0)::numeric, 2) AS luas_ha
FROM kph.petak ORDER BY kode;

Hasil uji: 25 baris, mulai P-01 (Sengon, 17,52 ha). Jumlah luas semua petak 400,00 ha, sama dengan luas wilayah 2 km x 2 km. Angka itu bisa dipakai sebagai alat pemeriksa: bila totalnya tidak 400, ada petak tumpang tindih atau berlubang.

#A2. Menggabung berdasarkan letak: kejadian per petak

sql
SELECT p.kode, COUNT(k.gid) AS jumlah
FROM kph.petak AS p
LEFT JOIN kph.kejadian AS k ON ST_Intersects(p.geom, k.geom)
GROUP BY p.kode
ORDER BY jumlah DESC, p.kode
LIMIT 5;

LEFT JOIN menjaga petak yang tidak punya kejadian tetap muncul dengan angka nol. Hasil uji:

PetakKejadian
P-0413
P-0912
P-1212
P-089
P-017

Pemeriksaan: jumlah semua petak = 150 = jumlah baris kph.kejadian. Setiap kejadian jatuh di tepat satu petak.

#A3. Berjarak paling jauh: tebangan liar dekat jalan

sql
SELECT k.jenis,
       COUNT(*) AS total,
       COUNT(*) FILTER (WHERE EXISTS (
           SELECT 1 FROM kph.jalan j WHERE ST_DWithin(k.geom, j.geom, 100))) AS dekat_jalan
FROM kph.kejadian k
GROUP BY k.jenis ORDER BY k.jenis;

ST_DWithin(a, b, 100) berarti "a dan b berjarak paling jauh 100 meter". Hasil uji:

JenisTotalDalam 100 m dari jalanPersen
Longsor402460,0
Tebangan liar605998,3
Titik panas503672,0

Tebangan liar nyaris selalu dekat jalan (data sintetis sengaja dibuat begitu). Pada data nyata, pola seperti ini menjadi dasar menentukan rute patroli.

#A4. Tetangga terdekat: pos jaga untuk tiap titik panas

sql
SELECT pos.nama AS pos_terdekat, COUNT(*) AS jumlah_titik_panas,
       ROUND(AVG(pos.jarak)::numeric, 1) AS rata_jarak_m
FROM kph.kejadian AS k
CROSS JOIN LATERAL (
    SELECT f.nama, ST_Distance(f.geom, k.geom) AS jarak
    FROM kph.fasilitas AS f
    WHERE f.jenis = 'Pos'
    ORDER BY f.geom <-> k.geom
    LIMIT 1) AS pos
WHERE k.jenis = 'Titik panas'
GROUP BY pos.nama ORDER BY pos.nama;

LATERAL membuat subkueri dijalankan sekali untuk tiap titik panas. Di dalamnya, ORDER BY f.geom <-> k.geom LIMIT 1 mengambil pos terdekat. Hasil uji: Pos Jaga 4 melayani 29 dari 50 titik panas (rata-rata jarak 629,0 m), Pos Jaga 1 hanya 5 titik. Beban pos tidak seimbang.

#A5. Buffer, gabung, dan potong

Berapa luas tiap petak yang berada dalam 50 m dari jalan?

sql
WITH sabuk AS (SELECT ST_Union(ST_Buffer(geom, 50)) AS g FROM kph.jalan)
SELECT p.kode, ROUND((ST_Area(ST_Intersection(p.geom, s.g)) / 10000.0)::numeric, 2) AS ha_dekat_jalan
FROM kph.petak p, sabuk s
ORDER BY ha_dekat_jalan DESC, p.kode LIMIT 5;

Hasil uji: P-03 9,28 ha, P-02 8,73 ha, P-09 8,21 ha, P-08 8,15 ha, P-17 7,67 ha. ST_Union melebur semua buffer jadi satu bidang, jadi bagian yang tumpang tindih tidak dihitung dua kali.

Dua kueri serupa menjawab pertanyaan "dissolve" dan "overlay" dari Seri I1:

sql
-- luas tiap jenis tegakan (dissolve)
SELECT jenis_tegakan, COUNT(*) AS jumlah_petak,
       ROUND((ST_Area(ST_Union(geom)) / 10000.0)::numeric, 2) AS luas_ha
FROM kph.petak GROUP BY jenis_tegakan ORDER BY luas_ha DESC;

Hasil uji: Jati 160,97 ha (10 petak), Mahoni 82,71 ha, Sengon 78,86 ha, Akasia 42,62 ha, Lahan kosong 34,84 ha. Overlay petak dengan tanah memakai ST_Intersection dan hasilnya: Aluvial 46,58 ha, Latosol 146,02 ha, Podsolik 116,70 ha, Regosol 90,70 ha. Totalnya 400,00 ha.

#A6. Mengelompokkan titik

sql
SELECT cid, COUNT(*) FROM (
  SELECT ST_ClusterDBSCAN(geom, eps := 150, minpoints := 3) OVER () AS cid
  FROM kph.kejadian WHERE jenis = 'Titik panas') s
GROUP BY cid ORDER BY cid NULLS LAST;

Hasil uji: satu gerombol berisi 25 titik panas, dan 25 titik lain tersebar (cid kosong). Gerombol itulah yang layak dipantau lebih dulu.

#A7. Indeks spasial dalam angka

Penulis membuat tabel uji 500.000 titik acak, lalu mencari titik di jendela 50 m x 50 m. Skrip: m2_02b_indeks_dan_view.sql. Ketik EXPLAIN di depan kueri untuk melihat rencananya.

KeadaanRencana serverWaktu (satu kali ukur)
Tanpa indeksParallel Seq Scan (baca semua baris)122,8 ms
Dengan indeks gistBitmap Index Scan pada indeks3,9 ms

Keduanya mengembalikan jumlah titik yang sama. Selisihnya sekitar 30 kali pada ukuran ini, dan makin besar pada tabel yang makin besar. Waktu di komputer Anda akan berbeda. Yang penting adalah pola: indeks mengubah "baca semua" menjadi "baca bagian yang perlu".

Ilustrasi 2.2: Hasil ukur indeks spasial
Diagram batang waktu kueri tanpa indeks sekitar 123 milidetik dan dengan indeks sekitar 4 milidetik

#A8. View dan view terwujud

View menyimpan kueri. Isinya selalu terbaru karena dihitung ulang tiap dibuka. View terwujud (materialized view) menyimpan hasilnya, cepat tetapi harus disegarkan.

sql
CREATE VIEW kph.v_kejadian_per_petak AS
SELECT p.gid, p.kode, p.geom, COUNT(k.gid) AS jumlah_kejadian
FROM kph.petak p LEFT JOIN kph.kejadian k ON ST_Intersects(p.geom, k.geom)
GROUP BY p.gid, p.kode, p.geom;

CREATE MATERIALIZED VIEW kph.mv_kejadian_per_petak AS SELECT * FROM kph.v_kejadian_per_petak;
REFRESH MATERIALIZED VIEW kph.mv_kejadian_per_petak;

Hasil uji: jumlah awal 150. Setelah satu kejadian ditambahkan, view biasa menunjukkan 151, view terwujud masih 150, dan baru menjadi 151 setelah REFRESH. Kejadian uji itu lalu dihapus.

#A9. SQL yang sama, tanpa server

Bila PostGIS belum tersedia, GeoPackage bisa dikueri dengan SQL bergaya SpatiaLite lewat GDAL. Skrip m2_02c_silang_geopackage.py menjalankan kueri yang sama di sana. Hasil uji: total luas 400,0 ha, tiga petak teratas (P-04 13, P-09 12, P-12 12), dan 59 tebangan liar dekat jalan, semuanya sama persis dengan PostGIS.

Dua perbedaan yang perlu Anda ketahui. Pada uji penulis, jarak ditulis ST_Distance(...) <= 100, karena ST_DWithin dan operator <-> tidak dicoba di dialek itu. Kolom bentuk bernama geom, dan kolom kunci bernama fid.

QGIS punya dua padanan tanpa server (skrip m2_02d_qgis_setara.py):

  • Count points in polygon (Processing ► Toolbox): hasil P-04 13, P-09 12, P-12 12.
  • Execute SQL (kueri pada layer maya): tabel masukan bernama input1, input2, dan seterusnya. Hasilnya sama.

#Bagian B: ArcGIS Pro

#Bagian B: ArcGIS Pro

Pro punya dua jalan. Jalan pertama memakai alat bawaan tanpa menulis SQL. Jalan kedua memakai SQL.

  1. Menggabung berdasarkan letak: alat Spatial Join atau Summarize Within. Pilih petak sebagai sasaran dan kejadian sebagai gabungan. Hasilnya jumlah kejadian per petak. [CEK]
  2. Berjarak paling jauh: alat Select Layer By Location dengan hubungan Within a distance. [CEK]
  3. Tetangga terdekat: alat Near atau Generate Near Table. [CEK]
  4. SQL: untuk tabel di geodatabase perusahaan, Pro memakai fungsi SQL milik Esri (ST_Geometry) bila jenis ruangnya ST_Geometry. Tabel PostGIS dapat dibaca lewat Query Layer (Map ► Add Data ► Query Layer). Fungsi ST_ PostGIS dijalankan oleh PostgreSQL di dalam kueri itu. [CEK]

Pro tidak punya padanan langsung untuk ST_ClusterDBSCAN. Alat Density-based Clustering dalam kotak peralatan Spatial Statistics mengerjakan hal serupa. [CEK]

#Bagian C: ArcMap 10.8

#Bagian C: ArcMap 10.8

  1. Spatial Join (klik kanan layer ► Joins and Relates ► Join ► Join data from another layer based on spatial location) menghitung kejadian per petak. [CEK]
  2. Select By Location dengan pilihan are within a distance of the source layer feature. [CEK]
  3. Near (kotak peralatan Analysis) menghitung jarak ke fasilitas terdekat. [CEK]
  4. Query Layer: File ► Add Data ► Add Query Layer untuk SQL ke basis data. [CEK]

#Cek paham

  1. Mengapa kueri A2 memakai LEFT JOIN, bukan JOIN biasa?
  2. Mana yang lebih baik di klausa WHERE: ST_Distance(a, b) < 100 atau ST_DWithin(a, b, 100)? Mengapa?
  3. View terwujud menunjukkan 150 kejadian, padahal sudah ada 151 di tabel. Apa yang harus dilakukan?

Jawaban:

  1. LEFT JOIN mempertahankan petak tanpa kejadian dengan hitungan nol. JOIN biasa membuang petak itu dari hasil.
  2. ST_DWithin, karena bisa memakai indeks spasial.
  3. Jalankan REFRESH MATERIALIZED VIEW. View terwujud hanya diperbarui saat disegarkan.

#Kesalahan umum

  • Menghitung luas pada data berkoordinat derajat. Hasilnya satuan derajat persegi. Reproyeksi dulu ke UTM, atau pakai tipe geography.
  • Lupa indeks spasial pada tabel hasil buatan sendiri. Tabel baru dari CREATE TABLE AS tidak punya indeks. Buat dengan CREATE INDEX ... USING gist (geom).
  • Memakai `JOIN` padahal ingin semua petak muncul. Pakai LEFT JOIN.
  • Menggabung dua tabel ber-SRID berbeda. PostGIS akan menolak. Samakan dengan ST_Transform.

#Ringkasan dan latihan

Ringkasan: SQL spasial menggabung tabel berdasarkan letak. Predikat menjawab ya atau tidak, pengukur menjawab angka, <-> mengurutkan menurut kedekatan. Indeks spasial dan ST_DWithin membuatnya cepat.

Latihan: tulis kueri yang menghitung, untuk tiap petak, jumlah longsor dalam jarak 200 m dari sungai. (Tabel sungai dibuat di Bab 4; sementara itu pakai jalan.) Periksa bahwa jumlah semua petak tidak melebihi 40.

#Tabel perbandingan: pekerjaan yang sama di tiga perangkat

PekerjaanQGIS dan PostGISArcGIS ProArcMap 10.8
Hitung kejadian per petakST_Intersects + GROUP BY, atau Count points in polygonSummarize Within [CEK]Spatial Join [CEK]
Dalam jarak tertentuST_DWithinSelect Layer By Location [CEK]Select By Location [CEK]
TerdekatORDER BY <-> LIMIT 1Generate Near Table [CEK]Near [CEK]
SQL pada basis dataDB Manager, Execute SQLQuery Layer [CEK]Add Query Layer [CEK]