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:

sql buat-tabel.sql
-- 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:

text
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:

text
 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:

sql kutip-error.sql
-- kutip dua membuka mode "elemen terkutip": sisanya harus masuk ke dalam kutip
SELECT '{"Layar 14" inci}'::text[] AS kutip_setengah;

Output yang Dihasilkan:

text
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:

text
         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:

text
psql:array-ragged.sql:2: ERROR:  multidimensional arrays must have array expressions with matching dimensions

Baris 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:

text
 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:

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