Satu produk sering butuh lebih dari satu label: alat tulis sekaligus diskon, atau elektronik sekaligus baru. Jalur normalisasi klasik membuat tabel tag terpisah lalu meng-JOIN-nya — tepat saat tag perlu diberi metadata sendiri, tetapi membengkak saat kita hanya ingin menyimpan dan membaca daftar kecil yang selalu menempel pada baris induknya. PostgreSQL menawarkan jalan tengah yang jarang dimiliki database lain: array asli — satu kolom berisi banyak nilai bertipe sama, yang tetap bisa diakses per elemen, dicari keanggotaannya, dan dibongkar menjadi baris. Chapter ini membahas deklarasi text[], akses elemen yang berindeks dari 1, operator pencarian ANY dan @>, sampai jebakan NULL yang kerap membuat baris hilang tanpa pesan error.
Deklarasi dan Pengisian Array #
Tipe array dideklarasikan dengan menempelkan [] di belakang tipe elemennya: text[] berarti array of text, integer[] array of integer, dan pola yang sama berlaku untuk semua tipe. Nilai bisa dibangun lewat constructor ARRAY[...] atau literal string '{a,b}':
-- tipe array dideklarasikan dengan [] di belakang tipe elemen
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10, 2) NOT NULL,
tag text[]
);
INSERT INTO produk (id, nama, harga, tag) VALUES
(1, 'Buku Tulis', 4500, ARRAY['alat tulis', 'diskon']),
(2, 'Laptop Kerja', 8500000, ARRAY['elektronik']),
(3, 'Kertas HVS A4', 55000, '{}'),
(4, 'Stapler Besar', 24000, NULL),
(5, 'Kamera Mirrorless', 12000000, ARRAY['elektronik', NULL]);
SELECT id, nama, harga, tag
FROM produk
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 5
id | nama | harga | tag
----+-------------------+-------------+-----------------------
1 | Buku Tulis | 4500.00 | {"alat tulis",diskon}
2 | Laptop Kerja | 8500000.00 | {elektronik}
3 | Kertas HVS A4 | 55000.00 | {}
4 | Stapler Besar | 24000.00 |
5 | Kamera Mirrorless | 12000000.00 | {elektronik,NULL}
(5 rows)Lima baris di atas sengaja memuat semua variasi yang akan kita bedah sepanjang chapter: array dua elemen (Buku Tulis), array satu elemen (Laptop Kerja), array kosong '{}' (Kertas HVS A4), kolom NULL (Stapler Besar), dan array yang salah satu elemennya NULL (Kamera Mirrorless). Perhatikan cara psql menampilkan array: elemen dengan spasi dibungkus kutip dua ("alat tulis"), elemen NULL ditulis apa adanya, dan kolom NULL tampil kosong — tiga wujud yang berbeda perilakunya di bagian akhir chapter.
Akses Elemen: Mulai dari 1 #
Elemen array diakses dengan subscript kotak siku — dan penting untuk diingat: indeks PostgreSQL dimulai dari 1, bukan 0 seperti kebiasaan banyak bahasa pemrograman:
-- indeks array PostgreSQL dimulai dari 1, bukan 0
SELECT id, nama, tag[1] AS elemen_pertama, tag[2] AS elemen_kedua
FROM produk
ORDER BY id;
-- indeks di luar jangkauan menghasilkan NULL, bukan error
SELECT id, nama, tag[0] AS elemen_nol
FROM produk
ORDER BY id;
SELECT array_dims(ARRAY[10, 20, 30]) AS dimensi;
SELECT array_lower(ARRAY[10, 20, 30], 1) AS batas_bawah,
array_upper(ARRAY[10, 20, 30], 1) AS batas_atas;
SELECT id, nama,
array_length(tag, 1) AS panjang,
cardinality(tag) AS jumlah_elemen
FROM produk
ORDER BY id;Output yang Dihasilkan:
id | nama | elemen_pertama | elemen_kedua
----+-------------------+----------------+--------------
1 | Buku Tulis | alat tulis | diskon
2 | Laptop Kerja | elektronik |
3 | Kertas HVS A4 | |
4 | Stapler Besar | |
5 | Kamera Mirrorless | elektronik |
(5 rows)
id | nama | elemen_nol
----+-------------------+------------
1 | Buku Tulis |
2 | Laptop Kerja |
3 | Kertas HVS A4 |
4 | Stapler Besar |
5 | Kamera Mirrorless |
(5 rows)
dimensi
---------
[1:3]
(1 row)
batas_bawah | batas_atas
-------------+------------
1 | 3
(1 row)
id | nama | panjang | jumlah_elemen
----+-------------------+---------+---------------
1 | Buku Tulis | 2 | 2
2 | Laptop Kerja | 1 | 1
3 | Kertas HVS A4 | | 0
4 | Stapler Besar | |
5 | Kamera Mirrorless | 2 | 2
(5 rows)Query pertama memperlihatkan tag[1] mengembalikan elemen pertama — alat tulis untuk Buku Tulis dan elektronik untuk Laptop Kerja. Query kedua adalah jebakan yang perlu diwaspadai: tag[0] tidak error, melainkan menghasilkan NULL untuk semua baris, karena subscript di luar jangkauan memang didefinisikan mengembalikan NULL. Query penutup membandingkan dua fungsi ukuran: array_length(tag, 1) mengembalikan NULL untuk array kosong, sedangkan cardinality(tag) mengembalikan 0 — gunakan cardinality saat logika Anda membedakan “kosong” dari “tidak ada array sama sekali”.
Mencari Keanggotaan: ANY dan Operator Array #
Pertanyaan paling sering terhadap kolom array: “apakah daftar ini memuat nilai X?”. Tiga operator menjawab tiga variasi pertanyaan itu:
-- = ANY(): cocok bila ADA satu elemen yang sama
SELECT id, nama
FROM produk
WHERE 'elektronik' = ANY (tag)
ORDER BY id;
-- @>: cocok bila SELURUH elemen di kanan ada di array kiri
SELECT id, nama
FROM produk
WHERE tag @> ARRAY['alat tulis', 'diskon']
ORDER BY id;
-- &&: cocok bila ada IRISAN elemen
SELECT id, nama
FROM produk
WHERE tag && ARRAY['diskon', 'formal']
ORDER BY id;Output yang Dihasilkan:
id | nama
----+-------------------
2 | Laptop Kerja
5 | Kamera Mirrorless
(2 rows)
id | nama
----+------------
1 | Buku Tulis
(1 row)
id | nama
----+------------
1 | Buku Tulis
(1 row)'elektronik' = ANY (tag) membaca “elektronik sama dengan salah satu elemen tag” — mengembalikan Laptop Kerja dan Kamera Mirrorless. Operator @> (contains) menuntut semua elemen di sisi kanan hadir: hanya Buku Tulis yang memuat alat tulis sekaligus diskon. Operator && (overlap) cukup dengan satu elemen bersama, sehingga daftar ['diskon', 'formal'] menangkap Buku Tulis lewat diskon meskipun tidak ada produk bertag formal. Ketiganya — terutama @> dan && — dapat diakselerasi index GIN, topik yang dibahas di section Indexes.
Membongkar dan Merakit: unnest dan array_agg #
Dua fungsi ini adalah gerbang dua arah antara array dan tabel. unnest() membongkar array menjadi baris; array_agg() merakit baris kembali menjadi array:
-- unnest(): satu baris baru per elemen array
SELECT unnest(ARRAY['senin', 'rabu', 'jumat']) AS hari;
-- dipakai di SELECT tabel: kolom array "melebar" menjadi baris
SELECT id, nama, unnest(tag) AS tag
FROM produk
WHERE id <= 2
ORDER BY id;
-- array_agg(): kebalikan unnest, merakit baris kembali menjadi array
SELECT array_agg(nama ORDER BY id) AS daftar_nama
FROM produk
WHERE id <= 3;Output yang Dihasilkan:
hari
-------
senin
rabu
jumat
(3 rows)
id | nama | tag
----+--------------+------------
1 | Buku Tulis | alat tulis
1 | Buku Tulis | diskon
2 | Laptop Kerja | elektronik
(3 rows)
daftar_nama
-----------------------------------------------
{"Buku Tulis","Laptop Kerja","Kertas HVS A4"}
(1 row)Query kedua memperlihatkan pola “melebar”: baris Buku Tulis dengan dua tag menjadi dua baris output, satu per elemen. Perhatikan pula array_agg(nama ORDER BY id) — klausa ORDER BY di dalam aggregate menjamin urutan elemen hasil rakitan; tanpa klausa itu, urutan elemen array tidak dijamin mengikuti apa pun.
NULL, Array Kosong, dan Elemen NULL #
Tiga “kekosongan” yang tampak mirip di layar ternyata berperilaku berbeda saat diquery — dan perbedaannya bisa membuat baris hilang tanpa error:
-- tiga wujud "kekosongan" array yang berbeda perilakunya
SELECT NULL::text[] AS array_null,
'{}'::text[] AS array_kosong,
ARRAY[NULL]::text[] AS array_satu_null;
-- array_length vs cardinality pada array kosong
SELECT array_length('{}'::text[], 1) AS panjang_kosong,
cardinality('{}'::text[]) AS jumlah_kosong;
-- = ANY() pada array NULL menghasilkan NULL,
-- pada array kosong menghasilkan false
SELECT 'diskon' = ANY(NULL::text[]) AS cocok_di_array_null,
'diskon' = ANY('{}'::text[]) AS cocok_di_array_kosong;Output yang Dihasilkan:
array_null | array_kosong | array_satu_null
------------+--------------+-----------------
| {} | {NULL}
(1 row)
panjang_kosong | jumlah_kosong
----------------+---------------
| 0
(1 row)
cocok_di_array_null | cocok_di_array_kosong
---------------------+-----------------------
| f
(1 row)Ketiga wujud itu: kolom NULL (tidak punya array sama sekali), '{}' (array ada tapi nol elemen), dan ARRAY[NULL] (array berisi satu elemen yang nilainya NULL). Perilaku = ANY() membedakan ketiganya dengan cara yang penting untuk WHERE: pada array kosong hasilnya false (jelas tidak ada kecocokan), tetapi pada array NULL hasilnya NULL — dan baris dengan kondisi NULL tersaring keluar dari hasil query tanpa pesan apa pun, sama seperti perilaku NULL pada perbandingan biasa.
Array atau Tabel Terpisah #
| Kondisi data | Pilihan yang tepat |
|---|---|
| Daftar kecil, hampir selalu dibaca bersama baris induknya (tag, nomor telepon alternatif) | Array satu kolom |
| Perlu kolom tambahan per item (tanggal mulai tag, bobot) | Tabel terpisah |
| Itemnya perlu di-query berdiri sendiri lintas induk (“semua tag yang pernah dipakai”) | Tabel terpisah |
| Nilai item harus tervalidasi lewat FK ke tabel referensi | Tabel terpisah |
Aturan ringkasnya: array menguntungkan saat hubungannya “milik eksklusif dan dibaca bersama”; normalisasi tabel menang begitu item berdiri di atas kakinya sendiri. Batas keduanya tidak tajam, dan chapter Memilih Tipe Data merangkum pertimbangan lintas-tipe di akhir section ini.
Array menyelesaikan kebutuhan “banyak nilai terstruktur dalam satu kolom” dengan tipe elemen yang tetap ketat. Topik penutup katalog tipe ini melonggarkan struktur itu sepenuhnya: json dan jsonb, dua tipe untuk dokumen yang bentuknya bebas berubah dari baris ke baris.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
akses-elemen.sql
Unduh berkas-- indeks array PostgreSQL dimulai dari 1, bukan 0
SELECT id, nama, tag[1] AS elemen_pertama, tag[2] AS elemen_kedua
FROM produk
ORDER BY id;
-- indeks di luar jangkauan menghasilkan NULL, bukan error
SELECT id, nama, tag[0] AS elemen_nol
FROM produk
ORDER BY id;
SELECT array_dims(ARRAY[10, 20, 30]) AS dimensi;
SELECT array_lower(ARRAY[10, 20, 30], 1) AS batas_bawah,
array_upper(ARRAY[10, 20, 30], 1) AS batas_atas;
SELECT id, nama,
array_length(tag, 1) AS panjang,
cardinality(tag) AS jumlah_elemen
FROM produk
ORDER BY id;
any-dan-contains.sql
Unduh berkas-- = ANY(): cocok bila ADA satu elemen yang sama
SELECT id, nama
FROM produk
WHERE 'elektronik' = ANY (tag)
ORDER BY id;
-- @>: cocok bila SELURUH elemen di kanan ada di array kiri
SELECT id, nama
FROM produk
WHERE tag @> ARRAY['alat tulis', 'diskon']
ORDER BY id;
-- &&: cocok bila ada IRISAN elemen
SELECT id, nama
FROM produk
WHERE tag && ARRAY['diskon', 'formal']
ORDER BY id;
deklarasi-array.sql
Unduh berkas-- tipe array dideklarasikan dengan [] di belakang tipe elemen
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10, 2) NOT NULL,
tag text[]
);
INSERT INTO produk (id, nama, harga, tag) VALUES
(1, 'Buku Tulis', 4500, ARRAY['alat tulis', 'diskon']),
(2, 'Laptop Kerja', 8500000, ARRAY['elektronik']),
(3, 'Kertas HVS A4', 55000, '{}'),
(4, 'Stapler Besar', 24000, NULL),
(5, 'Kamera Mirrorless', 12000000, ARRAY['elektronik', NULL]);
SELECT id, nama, harga, tag
FROM produk
ORDER BY id;
gotcha-null.sql
Unduh berkas-- tiga wujud "kekosongan" array yang berbeda perilakunya
SELECT NULL::text[] AS array_null,
'{}'::text[] AS array_kosong,
ARRAY[NULL]::text[] AS array_satu_null;
-- array_length vs cardinality pada array kosong
SELECT array_length('{}'::text[], 1) AS panjang_kosong,
cardinality('{}'::text[]) AS jumlah_kosong;
-- = ANY() pada array NULL menghasilkan NULL,
-- pada array kosong menghasilkan false
SELECT 'diskon' = ANY(NULL::text[]) AS cocok_di_array_null,
'diskon' = ANY('{}'::text[]) AS cocok_di_array_kosong;
unnest-dan-aggregate.sql
Unduh berkas-- unnest(): satu baris baru per elemen array
SELECT unnest(ARRAY['senin', 'rabu', 'jumat']) AS hari;
-- dipakai di SELECT tabel: kolom array "melebar" menjadi baris
SELECT id, nama, unnest(tag) AS tag
FROM produk
WHERE id <= 2
ORDER BY id;
-- array_agg(): kebalikan unnest, merakit baris kembali menjadi array
SELECT array_agg(nama ORDER BY id) AS daftar_nama
FROM produk
WHERE id <= 3;