Pertanyaan seperti “cabang toko mana yang paling dekat dengan pelanggan ini?” atau “apakah titik kirim ini berada di area layanan kita?” adalah pertanyaan geometris. PostgreSQL menyimpan jawabannya secara alami lewat keluarga tipe geometris bawaan: point untuk koordinat, box untuk area persegi, circle untuk radius layanan, sampai polygon untuk wilayah berbatas banyak sisi — lengkap dengan operator jarak, kontainment, dan perpotongan. Pada chapter ini kita memetakan delapan bentuk tersebut, menguji operatornya, lalu membangun query cabang terdekat yang bisa langsung dipakai di aplikasi.

Delapan Bentuk Geometri Dasar #

Semua tipe geometri PostgreSQL bekerja pada bidang datar dua dimensi — koordinat (x, y) biasa, bukan permukaan bola bumi. Ringkasan kedelapan tipe:

TipePenyimpananBentukNotasi contoh
point16 bytesatu titik(1,2)
line32 bytegaris tak terbatas[(1,1),(2,2)]
lseg32 byteruas garis berujung dua titik[(1,1),(3,3)]
box32 bytepersegi panjang((1,1),(3,3))
path16+16n bytejalur banyak titik, terbuka atau tertutup[(1,1),(3,1)] / ((1,1),(3,1))
polygon40+16n bytepoligon tertutup((1,1),(3,1),(3,3),(1,3))
circle24 bytelingkaran berpusat dan berjari-jari<(1,2),5>

Kolom point adalah yang paling sering dipakai di aplikasi bisnis — misalnya menyimpan koordinat kantor cabang — sehingga contoh tabel di akhir chapter memakai tipe tersebut. Bentuk-bentuk lainnya berguna saat query Anda memang berbicara tentang area dan garis.

Notasi Penulisan Bentuk #

Setiap bentuk punya notasi literalnya sendiri, dan hasil penyimpanan server kadang berbeda dari yang kita tulis — bukan karena data berubah, melainkan karena server menormalkannya ke bentuk kanonik:

sql tipe-dasar.sql
-- Titik: sepasang koordinat (x, y)
SELECT point '(1,2)' AS titik;

-- Ruas garis: dua titik ujung
SELECT lseg '[(1,1),(3,3)]' AS ruas_garis;

-- Garis tak terbatas: ditulis lewat dua titik yang dilaluinya
SELECT line '[(1,1),(2,2)]' AS garis_tak_terbatas;

-- Persegi panjang: dua pojok berseberangan
SELECT box '((1,1),(3,3))' AS persegi_panjang;

-- Path terbuka memakai kurung siku
SELECT path '[(1,1),(3,1),(3,3)]' AS jalur_terbuka;

-- Path tertutup memakai kurung biasa
SELECT path '((1,1),(3,1),(3,3))' AS jalur_tertutup;

-- Poligon: titik-titik sudut, sisi terakhir menutup otomatis
SELECT polygon '((1,1),(3,1),(3,3),(1,3))' AS poligon;

-- Lingkaran: pusat dan jari-jari
SELECT circle '<(1,2),5>' AS lingkaran;

Output yang Dihasilkan:

text
 titik 
-------
 (1,2)
(1 row)

  ruas_garis   
---------------
 [(1,1),(3,3)]
(1 row)

 garis_tak_terbatas 
--------------------
 {1,-1,0}
(1 row)

 persegi_panjang 
-----------------
 (3,3),(1,1)
(1 row)

    jalur_terbuka    
---------------------
 [(1,1),(3,1),(3,3)]
(1 row)

   jalur_tertutup    
---------------------
 ((1,1),(3,1),(3,3))
(1 row)

          poligon          
---------------------------
 ((1,1),(3,1),(3,3),(1,3))
(1 row)

 lingkaran 
-----------
 <(1,2),5>
(1 row)

Tiga normalisasi menonjol pada hasil di atas. Pertama, line '[(1,1),(2,2)]' ditulis lewat dua titik yang dilewatinya, tetapi server menyimpannya sebagai koefisien persamaan garis {A, B, C} — nilai {1,-1,0} berarti persamaan x - y = 0, yaitu garis diagonal melalui titik nol. Kedua, box yang kita tulis ((1,1),(3,3)) ditampilkan kembali sebagai (3,3),(1,1) — bentuk kanoniknya selalu menyebut pojok kanan atas lebih dulu. Ketiga, path membedakan jalur terbuka (kurung siku, ujung tidak menyatu) dan jalur tertutup (kurung biasa, sisi terakhir otomatis menutup kembali ke titik awal) — perbedaan satu karakter kurung yang mengubah makna bentuk.

Operator Jarak, Kontainment, dan Perpotongan #

Operator geometris inilah alasan utama memakai tipe-tipe ini alih-alih dua kolom numeric terpisah untuk x dan y. Yang paling penting:

OperatorMaknaContoh
<->jarak antar dua objekpoint '(3,4)' <-> point '(0,0)'
@>objek kiri mengandung objek kanancircle '<(0,0),1>' @> point '(0.5,0.5)'
<@objek kiri terkandung di kananpoint '(2,2)' <@ box '((1,1),(3,3))'
&&tumpang tindih (box, circle, polygon)box '((1,1),(3,3))' && box '((2,2),(5,5))'
?#berpotonganlseg '[(2,0),(2,5)]' ?# box '((1,1),(3,3))'
##titik terdekat pada objek kananpoint '(0,3)' ## lseg '[(1,1),(3,1)]'
~=sama persispoint '(1,2)' ~= point '(1,2)'

Berikut peragannya satu per satu:

-- Jarak antar objek: <-> (server yang menghitung Pythagoras)
SELECT point '(3,4)' <-> point '(0,0)' AS jarak_titik;

SELECT point '(0,3)' <-> circle '<(0,0),1>' AS jarak_ke_lingkaran;

-- Kontainment: @> mengandung, <@ terkandung di dalam
SELECT circle '<(0,0),1>' @> point '(0.5,0.5)' AS lingkaran_mengandung_titik;

SELECT point '(2,2)' <@ box '((1,1),(3,3))' AS titik_dalam_persegi;

-- Tumpang tindih && (box/box, circle/circle, polygon/polygon)
SELECT box '((1,1),(3,3))' && box '((2,2),(5,5))' AS persegi_bertumpang;

SELECT circle '<(0,0),1>' && circle '<(1.5,0),2>' AS lingkaran_bertumpang;

-- Perpotongan ?# : lseg yang menembus persegi
SELECT lseg '[(2,0),(2,5)]' ?# box '((1,1),(3,3))' AS ruas_memotong_persegi;

-- Titik terdekat ## dan kesamaan persis ~=
SELECT point '(0,3)' ## lseg '[(1,1),(3,1)]' AS titik_terdekat;

SELECT point '(1,2)' ~= point '(1,2)' AS sama_persis;

Output yang Dihasilkan:

text
 jarak_titik 
-------------
           5
(1 row)

 jarak_ke_lingkaran 
--------------------
                  2
(1 row)

 lingkaran_mengandung_titik 
----------------------------
 t
(1 row)

 titik_dalam_persegi 
---------------------
 t
(1 row)

 persegi_bertumpang 
--------------------
 t
(1 row)

 lingkaran_bertumpang 
----------------------
 t
(1 row)

 ruas_memotong_persegi 
-----------------------
 t
(1 row)

 titik_terdekat 
----------------
 (1,1)
(1 row)

 sama_persis 
-------------
 t
(1 row)

Beberapa hasil pantas dibedah. Jarak point '(3,4)' ke titik asal bernilai tepat 5 — teorema Pythagoras dihitung server, bukan di kode aplikasi. Jarak titik (0,3) ke lingkaran berjari-jari 1 bernilai 2: jarak ke pusat (3) dikurangi jari-jari (1); jika titik berada di dalam lingkaran hasilnya 0. Operator ## mengembalikan point (1,1) — titik pada ruas garis yang paling dekat dengan titik (0,3) — berguna untuk proyeksi, misalnya mencari titik jalan terdekat dari sebuah alamat.

Query Cabang Terdekat #

Sekarang kita rakit semuanya menjadi query yang dipakai aplikasi nyata. Tabel cabang_toko menyimpan koordinat lima cabang Toko Maju Jaya dalam kolom point, lalu dua query menjawab dua pertanyaan berbeda:

CREATE TABLE cabang_toko (
    id     integer PRIMARY KEY,
    nama   text NOT NULL,
    lokasi point NOT NULL
);

-- Koordinat (lintang, bujur) lima cabang di wilayah Jabodetabek
INSERT INTO cabang_toko (id, nama, lokasi) VALUES
    (1, 'Maju Jaya Menteng',   point '(-6.196, 106.823)'),
    (2, 'Maju Jaya Bekasi',    point '(-6.238, 106.976)'),
    (3, 'Maju Jaya Depok',     point '(-6.402, 106.817)'),
    (4, 'Maju Jaya Tangerang', point '(-6.178, 106.640)'),
    (5, 'Maju Jaya Bogor',     point '(-6.595, 106.817)');

-- Tiga cabang terdekat dari gudang di (-6.2, 106.8)
SELECT id, nama, lokasi,
       lokasi <-> point '(-6.2, 106.8)' AS jarak
FROM cabang_toko
ORDER BY lokasi <-> point '(-6.2, 106.8)', id
LIMIT 3;

-- Cabang mana saja yang berada di dalam kotak area Jakarta Pusat?
SELECT id, nama
FROM cabang_toko
WHERE lokasi <@ box '((-6.25, 106.75), (-6.15, 106.9))'
ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 5
 id |        nama         |      lokasi      |        jarak         
----+---------------------+------------------+----------------------
  1 | Maju Jaya Menteng   | (-6.196,106.823) | 0.023345235059853774
  4 | Maju Jaya Tangerang | (-6.178,106.64)  |  0.16150541786577602
  2 | Maju Jaya Bekasi    | (-6.238,106.976) |  0.18005554698481438
(3 rows)

 id |       nama        
----+-------------------
  1 | Maju Jaya Menteng
(1 row)

Query pertama adalah pola k-nearest neighbors: ORDER BY pada ekspresi jarak <-> lalu LIMIT 3. Kolom jarak ikut ditampilkan supaya hasilnya bisa diaudit — Menteng unggul dengan jarak 0.023 karena koordinatnya memang hampir berimpit dengan gudang. Kolom id ditambahkan di akhir ORDER BY sebagai pemecah seri: dua cabang dengan jarak sama persis akan terurut stabil berdasarkan id, bukan bergantung kebetulan fisik penyimpanan.

Query kedua memakai kontainment <@ dengan box — cepat dan tepat untuk pertanyaan “dalam area persegi mana”. Kalau area layanan Anda berbentuk radius, ganti box dengan circle dan operatornya tetap <@.

Menghitung Luas dan Panjang #

Untuk bentuk yang punya interior dan tepi, PostgreSQL menyediakan fungsi ukur:

-- Luas dengan area(): tersedia untuk box, circle, dan path tertutup
SELECT area(box '((1,1),(3,3))') AS luas_persegi;

SELECT area(circle '<(0,0),2>') AS luas_lingkaran;

SELECT area(path '((1,1),(3,1),(3,3))') AS luas_jalur;

-- Panjang total dengan @-@ : lseg dan path
SELECT @-@ lseg '[(1,1),(3,3)]' AS panjang_ruas;

SELECT @-@ path '((1,1),(3,1),(3,3))' AS keliling_jalur;

-- Pengurai circle dan box
SELECT center(circle '<(1,2),5>') AS pusat,
       radius(circle '<(1,2),5>') AS jari_jari;

SELECT height(box '((1,1),(3,5))') AS tinggi,
       width(box '((1,1),(3,5))') AS lebar;

Output yang Dihasilkan:

text
 luas_persegi 
--------------
            4
(1 row)

   luas_lingkaran   
--------------------
 12.566370614359172
(1 row)

 luas_jalur 
------------
          2
(1 row)

    panjang_ruas    
--------------------
 2.8284271247461903
(1 row)

  keliling_jalur  
------------------
 6.82842712474619
(1 row)

 pusat | jari_jari 
-------+-----------
 (1,2) |         5
(1 row)

 tinggi | lebar 
--------+-------
      4 |     2
(1 row)

Luas persegi ((1,1),(3,3)) bernilai 4 (2×2), dan luas lingkaran berjari-jari 2 adalah π×4 ≈ 12.57 — fungsi area() mengembalikan float8 sehingga nilainya penuh digit desimal. Operator @-@ mengukur panjang total: ruas diagonal (1,1)-ke-(3,3) sepanjang √8 ≈ 2.83, dan path tertutup tiga titik berkeliling ≈ 6.83. Fungsi center(), radius(), height(), dan width() memecah bentuk kembali menjadi angka-angka penyusunnya.

Kesamaan Geometris Pakai ~=, Bukan = #

Satu kejutan bagi pemula: operator = tidak ada untuk tipe geometris. Mencoba membandingkan dua point dengan = langsung ditolak server:

SELECT point '(1,2)' = point '(1,2)';

Output yang Dihasilkan:

text
psql:kesamaan-invalid.sql:1: ERROR:  operator does not exist: point = point
LINE 1: SELECT point '(1,2)' = point '(1,2)';
                             ^
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.

Operator yang benar adalah ~= (dibaca same as), seperti yang kita pakai pada file operator-dasar.sql. Desain ini bukan tanpa alasan — komparasi = yang bermakna “urutan” memang sulit didefinisikan untuk bentuk geometri, sehingga PostgreSQL hanya menyediakan kesamaan persis. Konsekuensinya: ORDER BY pada kolom geometri juga tidak tersedia; urutkan lewat ekspresi jarak <-> seperti contoh query cabang terdekat.

Batas Tipe Geometri Bawaan dan PostGIS #

Tipe geometri bawaan dirancang untuk data ukuran sedang dan perhitungan bidang datar: ratusan cabang toko, ratusan titik sensor, area layanan berupa persegi dan lingkaran. Ia tidak dilengkapi index spasial — query jarak pada tabel jutaan baris akan memindai seluruh tabel, dan koordinat bumi tidak dihitung sebagai bola.

Saat kebutuhan Anda melampaui batas itu, jawabannya adalah ekstensi PostGIS: tipe geometry berindex GiST/R-tree yang mendukung jarak meter di bumi, proyeksi peta, union wilayah, hingga analisis jaringan jalan. Materi PostGIS dibahas tuntas di chapter PostGIS pada Section Extensions dan Ekosistem; untuk sekarang, kenali dulu primitifnya lewat tipe bawaan ini.

Dengan bentuk dan jarak selesai, tinggal satu keluarga tipe khusus yang belum dibedah: rentang nilai. Tipe range seperti int4range dan daterange menyimpan interval — dari jadwal sewa ruangan sampai masa berlaku harga — dan mengganti pola dua kolom mulai/selesai dengan satu kolom yang punya operator tumpang tindih bawaan. Itulah topik chapter Range dan Multirange Types.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

kesamaan-invalid.sql

Unduh berkas
sql
SELECT point '(1,2)' = point '(1,2)';

luas-panjang.sql

Unduh berkas
sql
-- Luas dengan area(): tersedia untuk box, circle, dan path tertutup
SELECT area(box '((1,1),(3,3))') AS luas_persegi;

SELECT area(circle '<(0,0),2>') AS luas_lingkaran;

SELECT area(path '((1,1),(3,1),(3,3))') AS luas_jalur;

-- Panjang total dengan @-@ : lseg dan path
SELECT @-@ lseg '[(1,1),(3,3)]' AS panjang_ruas;

SELECT @-@ path '((1,1),(3,1),(3,3))' AS keliling_jalur;

-- Pengurai circle dan box
SELECT center(circle '<(1,2),5>') AS pusat,
       radius(circle '<(1,2),5>') AS jari_jari;

SELECT height(box '((1,1),(3,5))') AS tinggi,
       width(box '((1,1),(3,5))') AS lebar;

operator-dasar.sql

Unduh berkas
sql
-- Jarak antar objek: <-> (server yang menghitung Pythagoras)
SELECT point '(3,4)' <-> point '(0,0)' AS jarak_titik;

SELECT point '(0,3)' <-> circle '<(0,0),1>' AS jarak_ke_lingkaran;

-- Kontainment: @> mengandung, <@ terkandung di dalam
SELECT circle '<(0,0),1>' @> point '(0.5,0.5)' AS lingkaran_mengandung_titik;

SELECT point '(2,2)' <@ box '((1,1),(3,3))' AS titik_dalam_persegi;

-- Tumpang tindih && (box/box, circle/circle, polygon/polygon)
SELECT box '((1,1),(3,3))' && box '((2,2),(5,5))' AS persegi_bertumpang;

SELECT circle '<(0,0),1>' && circle '<(1.5,0),2>' AS lingkaran_bertumpang;

-- Perpotongan ?# : lseg yang menembus persegi
SELECT lseg '[(2,0),(2,5)]' ?# box '((1,1),(3,3))' AS ruas_memotong_persegi;

-- Titik terdekat ## dan kesamaan persis ~=
SELECT point '(0,3)' ## lseg '[(1,1),(3,1)]' AS titik_terdekat;

SELECT point '(1,2)' ~= point '(1,2)' AS sama_persis;

tabel-cabang.sql

Unduh berkas
sql
CREATE TABLE cabang_toko (
    id     integer PRIMARY KEY,
    nama   text NOT NULL,
    lokasi point NOT NULL
);

-- Koordinat (lintang, bujur) lima cabang di wilayah Jabodetabek
INSERT INTO cabang_toko (id, nama, lokasi) VALUES
    (1, 'Maju Jaya Menteng',   point '(-6.196, 106.823)'),
    (2, 'Maju Jaya Bekasi',    point '(-6.238, 106.976)'),
    (3, 'Maju Jaya Depok',     point '(-6.402, 106.817)'),
    (4, 'Maju Jaya Tangerang', point '(-6.178, 106.640)'),
    (5, 'Maju Jaya Bogor',     point '(-6.595, 106.817)');

-- Tiga cabang terdekat dari gudang di (-6.2, 106.8)
SELECT id, nama, lokasi,
       lokasi <-> point '(-6.2, 106.8)' AS jarak
FROM cabang_toko
ORDER BY lokasi <-> point '(-6.2, 106.8)', id
LIMIT 3;

-- Cabang mana saja yang berada di dalam kotak area Jakarta Pusat?
SELECT id, nama
FROM cabang_toko
WHERE lokasi <@ box '((-6.25, 106.75), (-6.15, 106.9))'
ORDER BY id;

tipe-dasar.sql

Unduh berkas
sql
-- Titik: sepasang koordinat (x, y)
SELECT point '(1,2)' AS titik;

-- Ruas garis: dua titik ujung
SELECT lseg '[(1,1),(3,3)]' AS ruas_garis;

-- Garis tak terbatas: ditulis lewat dua titik yang dilaluinya
SELECT line '[(1,1),(2,2)]' AS garis_tak_terbatas;

-- Persegi panjang: dua pojok berseberangan
SELECT box '((1,1),(3,3))' AS persegi_panjang;

-- Path terbuka memakai kurung siku
SELECT path '[(1,1),(3,1),(3,3)]' AS jalur_terbuka;

-- Path tertutup memakai kurung biasa
SELECT path '((1,1),(3,1),(3,3))' AS jalur_tertutup;

-- Poligon: titik-titik sudut, sisi terakhir menutup otomatis
SELECT polygon '((1,1),(3,1),(3,3),(1,3))' AS poligon;

-- Lingkaran: pusat dan jari-jari
SELECT circle '<(1,2),5>' AS lingkaran;