Chapter Arrays di section Tipe Data
sudah memperkenalkan kolom text[], operator ANY, dan jebakan NULL. Section ini membahas array jauh lebih dalam — mulai dari aturan penulisan literal yang membuat pemula sering tersandung, array multi-dimensi, sampai pembaruan isi array yang menumpuk elemen. Kita kembali memakai domain yang sama: tabel produk milik Toko Maju Jaya dengan kolom tag. Setiap contoh di chapter ini berdiri sendiri, jadi Anda bisa langsung menyalin dan menjalankan file SQL-nya dari database kosong.
Deklarasi Kolom Array #
Tipe array dideklarasikan dengan menempelkan [] pada tipe elemen: text[], integer[], atau numeric(10,2)[]. PostgreSQL juga menerima ukuran eksplisit seperti text[3], tetapi angka tersebut hanya menjadi dokumentasi — server tidak pernah membatasi jumlah elemen yang masuk. Perhatikan deklarasi dan isi tabel berikut:
-- text[3]: angka 3 hanya dokumentasi, PostgreSQL tidak membatasi jumlah elemen
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10, 2) NOT NULL,
tag text[3]
);
INSERT INTO produk (id, nama, harga, tag) VALUES
(1, 'Buku Tulis 384 Halaman', 9500, ARRAY['alat tulis', 'diskon']),
(2, 'Pensil Kayu 2B', 2500, '{"sekolah","alat tulis"}'),
(3, 'Laptop Kerja 14 inci', 8500000,
ARRAY['elektronik', 'kantor', 'premium', 'garansi']),
(4, 'Kertas HVS A4 80gsm', 55000, '{}'),
(5, 'Mouse Wireless', 185000, NULL);
SELECT id, nama, harga, tag, array_dims(tag) AS dimensi
FROM produk
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 5
id | nama | harga | tag | dimensi
----+------------------------+------------+-------------------------------------+---------
1 | Buku Tulis 384 Halaman | 9500.00 | {"alat tulis",diskon} | [1:2]
2 | Pensil Kayu 2B | 2500.00 | {sekolah,"alat tulis"} | [1:2]
3 | Laptop Kerja 14 inci | 8500000.00 | {elektronik,kantor,premium,garansi} | [1:4]
4 | Kertas HVS A4 80gsm | 55000.00 | {} |
5 | Mouse Wireless | 185000.00 | |
(5 rows)Kolom tag dideklarasikan sebagai text[3], tetapi baris Laptop Kerja 14 inci menyimpan empat elemen dan PostgreSQL menerimanya tanpa keluhan — kolom dimensi dari array_dims() melaporkan [1:4] untuk baris tersebut. Baris Pensil Kayu 2B memperlihatkan bentuk kedua pengisian array: literal string '{"sekolah","alat tulis"}'. Baris Kertas HVS A4 memakai array kosong '{}', sedangkan Mouse Wireless menyimpan NULL — dua kekosongan yang berbeda perilakunya, sudah dibahas di chapter sebelumnya dan akan terus muncul di chapter operator nanti.
Literal String dan Aturan Kutip #
Literal array ditulis sebagai satu string berformat {elemen1,elemen2}. Karena seluruh array hidup di dalam satu string SQL, ada dua lapis aturan kutip yang bekerja sekaligus — dan di sinilah kesalahan paling sering terjadi. Kita bedah kasusnya satu per satu:
-- elemen tanpa karakter pembatas bisa ditulis langsung
SELECT '{diskon, promo}'::text[] AS tanpa_spesial;
-- koma adalah pembatas elemen: string ini menjadi TIGA elemen, bukan dua
SELECT '{buku, alat tulis, umum}'::text[] AS pecah_diam_diam;
-- elemen yang memuat koma harus dibungkus kutip dua
SELECT '{"buku, alat tulis", umum}'::text[] AS kutip_benar;
-- tanda kutip satu di dalam elemen literal ditulis dua kali
SELECT '{"O''Brien","Toko''s Choice"}'::text[] AS kutip_satu;
-- ARRAY constructor memakai aturan string SQL biasa: lebih sulit salah
SELECT ARRAY['buku, alat tulis', 'umum', 'O''Brien'] AS constructor;Output yang Dihasilkan:
tanpa_spesial
----------------
{diskon,promo}
(1 row)
pecah_diam_diam
--------------------------
{buku,"alat tulis",umum}
(1 row)
kutip_benar
---------------------------
{"buku, alat tulis",umum}
(1 row)
kutip_satu
---------------------------
{O'Brien,"Toko's Choice"}
(1 row)
constructor
-----------------------------------
{"buku, alat tulis",umum,O'Brien}
(1 row)Empat baris pertama memakai literal; baris terakhir memakai constructor ARRAY[...]. Pelajarannya: koma, kurung kurawal, dan kutip dua adalah pembatas bagi parser array. Elemen yang memuat koma — misalnya buku, alat tulis — harus dibungkus kutip dua di dalam literal; tanpa kutip, elemen itu pecah menjadi dua secara diam-diam seperti pada kolom pecah_diam_diam (tiga elemen, tanpa error!). Tanda kutip satu di dalam elemen ditulis dua kali (''), karena begitulah aturan string SQL melepaskan tanda kutip satu.
Sekarang lihat kebalikannya: kutip dua yang dibuka harus menutup seluruh elemen. Query berikut memulai elemen dengan kutip dua lalu menyisakan teks di luar kutip:
-- kutip dua membuka mode "elemen terkutip": sisanya harus masuk ke dalam kutip
SELECT '{"Layar 14" inci}'::text[] AS kutip_setengah;Output yang Dihasilkan:
psql:kutip-error.sql:2: ERROR: malformed array literal: "{"Layar 14" inci}"
LINE 1: SELECT '{"Layar 14" inci}'::text[] AS kutip_setengah;
^
DETAIL: Incorrectly quoted array element.Parser membaca "Layar 14" sebagai elemen terkutip, lalu menemukan sisa inci} yang tidak lagi berada di dalam kutip — formatnya dianggap rusak dan error muncul. Konstruktor ARRAY['Layar 14 inci'] sama sekali tidak mengenal masalah ini karena tiap elemen adalah string SQL biasa.
Array Multi-Dimensi #
PostgreSQL mendukung array berdimensi banyak dengan syarat satu: bentuknya harus persegi (rectangular) — setiap dimensi memiliki panjang yang sama di semua barisnya. Contoh nyatanya laporan omzet dua pekan, dengan tiap pekan berupa array tiga hari:
-- array dua dimensi: tiap elemen adalah array satu dimensi dengan panjang sama
SELECT ARRAY[ARRAY[120, 95, 110], ARRAY[130, 105, 125]] AS omzet_harian;
CREATE TABLE laporan (
id integer PRIMARY KEY,
nama text NOT NULL,
omzet integer[][]
);
INSERT INTO laporan (id, nama, omzet) VALUES
(1, 'Pekan Biasa', ARRAY[[120, 95, 110], [130, 105, 125]]),
(2, 'Pekan Promo', ARRAY[[210, 180, 205], [220, 190, 215]]);
SELECT id, nama, omzet,
array_dims(omzet) AS dimensi,
cardinality(omzet) AS total_elemen
FROM laporan
ORDER BY id;
-- akses elemen dua dimensi: [baris][kolom]
SELECT id, nama, omzet[2][3] AS omzet_hari_ketiga
FROM laporan
ORDER BY id;Output yang Dihasilkan:
omzet_harian
------------------------------
{{120,95,110},{130,105,125}}
(1 row)
CREATE TABLE
INSERT 0 2
id | nama | omzet | dimensi | total_elemen
----+-------------+-------------------------------+------------+--------------
1 | Pekan Biasa | {{120,95,110},{130,105,125}} | [1:2][1:3] | 6
2 | Pekan Promo | {{210,180,205},{220,190,215}} | [1:2][1:3] | 6
(2 rows)
id | nama | omzet_hari_ketiga
----+-------------+-------------------
1 | Pekan Biasa | 125
2 | Pekan Promo | 215
(2 rows)array_dims() melaporkan [1:2][1:3]: dua baris, tiga kolom, dan cardinality() menghitung total enam elemen. Akses elemen memakai subscript berantai [baris][kolom], sehingga omzet[2][3] membaca hari ketiga di pekan kedua. Bentuk tidak persegi ditolak keras:
-- baris kedua lebih pendek dari baris pertama: PostgreSQL menolak
SELECT ARRAY[ARRAY[120, 95, 110], ARRAY[130, 105]] AS tidak_persegi;Output yang Dihasilkan:
psql:array-ragged.sql:2: ERROR: multidimensional arrays must have array expressions with matching dimensionsBaris kedua hanya berisi dua elemen sementara baris pertama tiga — PostgreSQL tidak mau menebak nebak dan langsung menolak dengan error. Untuk data yang panjang barisnya berbeda-beda (misalnya tag produk), gunakan array satu dimensi, bukan array dua dimensi yang dipaksa.
Mengakses Elemen dan Slice #
Indeks array PostgreSQL dimulai dari 1, dan subscript di luar jangkauan menghasilkan NULL tanpa error — perilaku yang sudah diingatkan di chapter Arrays sebelumnya. Bentuk akses yang belum kita bahas adalah slice: rentang [m:n] yang mengembalikan potongan array, bukan satu elemen:
-- indeks elemen dimulai dari 1; di luar jangkauan menghasilkan NULL, bukan error
SELECT id, nama, tag[1] AS tag_pertama, tag[2] AS tag_kedua
FROM produk
ORDER BY id;
-- slice [m:n] mengembalikan potongan array, bukan satu elemen
SELECT id, nama, tag[2:3] AS tag_tengah
FROM produk
WHERE cardinality(tag) >= 2
ORDER BY id;
-- slice yang melewati batas atas dipangkas secara diam-diam
SELECT (ARRAY['a', 'b', 'c'])[2:10] AS dipangkas;Output yang Dihasilkan:
id | nama | tag_pertama | tag_kedua
----+------------------------+-------------+------------
1 | Buku Tulis 384 Halaman | alat tulis | diskon
2 | Pensil Kayu 2B | sekolah | alat tulis
3 | Laptop Kerja 14 inci | elektronik | kantor
4 | Kertas HVS A4 80gsm | |
5 | Mouse Wireless | |
(5 rows)
id | nama | tag_tengah
----+------------------------+------------------
1 | Buku Tulis 384 Halaman | {diskon}
2 | Pensil Kayu 2B | {"alat tulis"}
3 | Laptop Kerja 14 inci | {kantor,premium}
(3 rows)
dipangkas
-----------
{b,c}
(1 row)Perbedaan hasilnya jelas: tag[2] mengembalikan satu elemen (diskon untuk Buku Tulis), sedangkan tag[2:3] mengembalikan array berisi maksimal dua elemen. Slice juga bersifat toleran terhadap batas atas yang kelewat jauh — [2:10] pada array tiga elemen dipangkas diam-diam menjadi dua elemen, tanpa error, berbeda dengan bahasa seperti Python yang akan protes.
Memperbarui Isi Array #
Kolom array diperbarui seperti kolom biasa lewat UPDATE, tetapi ada satu kebiasaan yang wajib: menempel elemen baru dengan operator || membutuhkan cast eksplisit ke tipe elemen:
-- menambah elemen di akhir: beri CAST eksplisit ke tipe elemen
UPDATE produk SET tag = tag || 'promo'::text WHERE id = 1;
-- menambah elemen di awal: elemen || array
UPDATE produk SET tag = 'unggulan'::text || tag WHERE id = 2;
-- mengganti seluruh isi array sekaligus dengan ARRAY constructor
UPDATE produk SET tag = ARRAY['murah', 'stok besar'] WHERE id = 4;
SELECT id, nama, tag, cardinality(tag) AS jumlah
FROM produk
ORDER BY id;Output yang Dihasilkan:
UPDATE 1
UPDATE 1
UPDATE 1
id | nama | tag | jumlah
----+------------------------+-------------------------------------+--------
1 | Buku Tulis 384 Halaman | {"alat tulis",diskon,promo} | 3
2 | Pensil Kayu 2B | {unggulan,sekolah,"alat tulis"} | 3
3 | Laptop Kerja 14 inci | {elektronik,kantor,premium,garansi} | 4
4 | Kertas HVS A4 80gsm | {murah,"stok besar"} | 2
5 | Mouse Wireless | |
(5 rows)tag || 'promo'::text menambahkan elemen di akhir, 'unggulan'::text || tag menambahkan di awal, dan penugasan dengan ARRAY[...] mengganti seluruh isi sekaligus. Dua jebakan menunggu di pola ini. Pertama, tanpa cast, tag || 'promo' menghasilkan error malformed array literal karena PostgreSQL menafsirkan 'promo' sebagai array dan mencoba mem-parse-nya. Kedua, baris Mouse Wireless tidak berubah sama sekali — NULL digabung apapun tetap NULL, jadi NULL || 'promo' tetap NULL. Bila niat Anda “beri tag default pada yang belum bertag”, gunakan COALESCE(tag, '{}') || 'promo'::text.
Deklarasi, pengisian, dan akses kini ada di tangan Anda. Namun menyimpan array belum berarti bisa menanyakan isinya dengan nyaman — untuk itu kita butuh rombongan operator khusus array: pembanding leksikografis, @> untuk containment, && untuk overlap, serta ANY/ALL yang akan jadi senjata utama query sehari-hari. Chapter berikutnya membahas semuanya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
akses-dan-slice.sql
Unduh berkas-- indeks elemen dimulai dari 1; di luar jangkauan menghasilkan NULL, bukan error
SELECT id, nama, tag[1] AS tag_pertama, tag[2] AS tag_kedua
FROM produk
ORDER BY id;
-- slice [m:n] mengembalikan potongan array, bukan satu elemen
SELECT id, nama, tag[2:3] AS tag_tengah
FROM produk
WHERE cardinality(tag) >= 2
ORDER BY id;
-- slice yang melewati batas atas dipangkas secara diam-diam
SELECT (ARRAY['a', 'b', 'c'])[2:10] AS dipangkas;
array-multidimensi.sql
Unduh berkas-- array dua dimensi: tiap elemen adalah array satu dimensi dengan panjang sama
SELECT ARRAY[ARRAY[120, 95, 110], ARRAY[130, 105, 125]] AS omzet_harian;
CREATE TABLE laporan (
id integer PRIMARY KEY,
nama text NOT NULL,
omzet integer[][]
);
INSERT INTO laporan (id, nama, omzet) VALUES
(1, 'Pekan Biasa', ARRAY[[120, 95, 110], [130, 105, 125]]),
(2, 'Pekan Promo', ARRAY[[210, 180, 205], [220, 190, 215]]);
SELECT id, nama, omzet,
array_dims(omzet) AS dimensi,
cardinality(omzet) AS total_elemen
FROM laporan
ORDER BY id;
-- akses elemen dua dimensi: [baris][kolom]
SELECT id, nama, omzet[2][3] AS omzet_hari_ketiga
FROM laporan
ORDER BY id;
array-ragged.sql
Unduh berkas-- baris kedua lebih pendek dari baris pertama: PostgreSQL menolak
SELECT ARRAY[ARRAY[120, 95, 110], ARRAY[130, 105]] AS tidak_persegi;
aturan-kutip.sql
Unduh berkas-- elemen tanpa karakter pembatas bisa ditulis langsung
SELECT '{diskon, promo}'::text[] AS tanpa_spesial;
-- koma adalah pembatas elemen: string ini menjadi TIGA elemen, bukan dua
SELECT '{buku, alat tulis, umum}'::text[] AS pecah_diam_diam;
-- elemen yang memuat koma harus dibungkus kutip dua
SELECT '{"buku, alat tulis", umum}'::text[] AS kutip_benar;
-- tanda kutip satu di dalam elemen literal ditulis dua kali
SELECT '{"O''Brien","Toko''s Choice"}'::text[] AS kutip_satu;
-- ARRAY constructor memakai aturan string SQL biasa: lebih sulit salah
SELECT ARRAY['buku, alat tulis', 'umum', 'O''Brien'] AS constructor;
buat-tabel.sql
Unduh berkas-- text[3]: angka 3 hanya dokumentasi, PostgreSQL tidak membatasi jumlah elemen
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10, 2) NOT NULL,
tag text[3]
);
INSERT INTO produk (id, nama, harga, tag) VALUES
(1, 'Buku Tulis 384 Halaman', 9500, ARRAY['alat tulis', 'diskon']),
(2, 'Pensil Kayu 2B', 2500, '{"sekolah","alat tulis"}'),
(3, 'Laptop Kerja 14 inci', 8500000,
ARRAY['elektronik', 'kantor', 'premium', 'garansi']),
(4, 'Kertas HVS A4 80gsm', 55000, '{}'),
(5, 'Mouse Wireless', 185000, NULL);
SELECT id, nama, harga, tag, array_dims(tag) AS dimensi
FROM produk
ORDER BY id;
kutip-error.sql
Unduh berkas-- kutip dua membuka mode "elemen terkutip": sisanya harus masuk ke dalam kutip
SELECT '{"Layar 14" inci}'::text[] AS kutip_setengah;
update-array.sql
Unduh berkas-- menambah elemen di akhir: beri CAST eksplisit ke tipe elemen
UPDATE produk SET tag = tag || 'promo'::text WHERE id = 1;
-- menambah elemen di awal: elemen || array
UPDATE produk SET tag = 'unggulan'::text || tag WHERE id = 2;
-- mengganti seluruh isi array sekaligus dengan ARRAY constructor
UPDATE produk SET tag = ARRAY['murah', 'stok besar'] WHERE id = 4;
SELECT id, nama, tag, cardinality(tag) AS jumlah
FROM produk
ORDER BY id;