Semua constraint yang kita pelajari di section ini — primary key, foreign key, unique, check, exclusion — mudah dideklarasikan saat tabel baru diciptakan. Kebutuhan nyata jarang sesederhana itu: tabel sudah berjalan berisi jutaan baris ketika tim baru menyadari bahwa email seharusnya unik, atau kolom kota_id seharusnya merujuk tabel rujukan. Chapter Mengubah Struktur Tabel di Section Dasar SQL sudah membahas perubahan level kolom — menambah kolom, mengganti tipe, mengatur ulang default, mengganti nama. Chapter ini melengkapi sisi yang tersisa: daur hidup constraint pada tabel yang sudah terlanjur berisi data. Inti persoalannya satu: memasang aturan baru tanpa merusak data lama, tanpa menghentikan operasi lebih lama dari yang perlu.

Skenario: Tabel Warisan yang Belum Bersih #

Kita mulai dari keadaan yang meniru sistem lama: tabel kota yang rapi, dan tabel pelanggan_lama yang datanya warisan impor — termasuk satu baris kotor berisi kota_id 99 yang tidak ada di tabel kota:

sql buat-tabel.sql
-- Skenario: tabel rujukan + tabel pelanggan warisan yang belum bersih
CREATE TABLE kota (
    kota_id integer PRIMARY KEY,
    nama     text NOT NULL
);

CREATE TABLE pelanggan_lama (
    id      integer PRIMARY KEY,
    nama    text NOT NULL,
    email   text,
    kota_id integer
);

INSERT INTO kota VALUES
    (1, 'Jakarta'),
    (2, 'Bandung'),
    (3, 'Surabaya');

-- kota_id 99 tidak ada di tabel kota: data warisan yang kotor
INSERT INTO pelanggan_lama VALUES
    (1, 'Budi', '[email protected]', 1),
    (2, 'Siti', '[email protected]', 2),
    (3, 'Tono', '[email protected]', 99);

SELECT id, nama, email, kota_id
FROM pelanggan_lama
ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 3
 id | nama |      email       | kota_id 
----+------+------------------+---------
  1 | Budi | [email protected] |       1
  2 | Siti | [email protected] |       2
  3 | Tono | [email protected] |      99
(3 rows)

Baris Tono (id 3) menjadi bahan uji sepanjang chapter: aturan foreign key yang akan kita pasang seharusnya menolak nilai 99 itu, dan kita akan melihat kapan penolakan itu terjadi. Email semua baris sengaja dibuat unik agar aturan kedua — keunikan email — bisa dipasang tanpa hambatan.

Menambah Constraint pada Tabel Berisi Data #

Klausa ADD CONSTRAINT memasang aturan pada tabel yang sudah ada, dengan satu syarat penting: seluruh isi tabel yang sudah tersimpan harus lolos pemeriksaannya, karena server memindai setiap baris saat constraint dipasang. Dua penambahan berikut memperlihatkan dua perilaku yang berbeda:

-- UNIQUE langsung dipasang: seluruh isi tabel diperiksa saat itu juga
ALTER TABLE pelanggan_lama
    ADD CONSTRAINT pelanggan_email_unik UNIQUE (email);

-- FK dengan NOT VALID: baris lama tidak diperiksa, tulisan baru tetap dijaga
ALTER TABLE pelanggan_lama
    ADD CONSTRAINT pelanggan_kota_fk
    FOREIGN KEY (kota_id) REFERENCES kota (kota_id) NOT VALID;

\d pelanggan_lama

Output yang Dihasilkan:

text
ALTER TABLE
ALTER TABLE
           Table "public.pelanggan_lama"
 Column  |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
 id      | integer |           | not null | 
 nama    | text    |           | not null | 
 email   | text    |           |          | 
 kota_id | integer |           |          | 
Indexes:
    "pelanggan_lama_pkey" PRIMARY KEY, btree (id)
    "pelanggan_email_unik" UNIQUE CONSTRAINT, btree (email)
Foreign-key constraints:
    "pelanggan_kota_fk" FOREIGN KEY (kota_id) REFERENCES kota(kota_id) NOT VALID

Mari kita bedah hasilnya:

  1. ADD CONSTRAINT pelanggan_email_unik UNIQUE (email): pemasangan unique constraint memicu pemindaian seluruh tabel untuk memastikan tidak ada duplikat. Data email kita bersih, jadi perintah berhasil — hasil \d menampilkan index pelanggan_email_unik yang dibuat otomatis. Bila ada dua email sama, perintah ini gagal sebelum aturan terpasang.
  2. ADD CONSTRAINT pelanggan_kota_fk FOREIGN KEY ... NOT VALID: tanpa NOT VALID, pemasangan foreign key ini pasti gagal — baris Tono dengan kota_id 99 melanggar. Klausa NOT VALID memasang aturan tanpa memeriksa data lama.
  3. Penanda NOT VALID pada \d: constraint tampil di bagian Foreign-key constraints dengan akhiran NOT VALID — penanda administratif bahwa pemeriksaan data lama masih menggantung.

Bentuk ADD CONSTRAINT yang sama berlaku untuk jenis lain: ADD PRIMARY KEY (kolom), ADD UNIQUE (kolom), hingga ADD CHECK (syarat) — semuanya menempatkan aturan pada tabel yang sudah hidup, dan semuanya (kecuali dengan NOT VALID) memeriksa data yang sudah ada.

NOT VALID: Penjagaan Sekarang, Pemeriksaan Nanti #

Kesalahpahaman paling umum soal NOT VALID adalah menganggapnya “constraint sementara nonaktif”. Itu keliru: sejak terpasang, constraint itu aktif penuh untuk semua tulisan baru — INSERT maupun UPDATE. Yang dilewati hanyalah pemeriksaan ulang baris-baris lama:

-- NOT VALID bukan berarti tanpa penjagaan: tulisan BARU tetap diperiksa
INSERT INTO pelanggan_lama VALUES (4, 'Dewi', '[email protected]', 98);

Output yang Dihasilkan:

text
psql:insert-ditolak.sql:2: ERROR:  insert or update on table "pelanggan_lama" violates foreign key constraint "pelanggan_kota_fk"
DETAIL:  Key (kota_id)=(98) is not present in table "kota".

INSERT untuk Dewi ditolak ketat: kota_id 98 tidak ada di tabel kota, dan foreign key yang barusan terpasang menangkapnya saat itu juga. Jadi sejak momen pemasangan, tidak ada satu baris kotor baru pun yang bisa masuk — kerusakan tidak bertambah selagi kita membersihkan yang lama.

VALIDATE: Menuntaskan Pemeriksaan Data Lama #

Pemeriksaan yang tertunda itu dituntaskan dengan VALIDATE CONSTRAINT. Perintah ini memindai ulang seluruh isi tabel terhadap constraint-nya; bila masih ada pelanggar, perintah gagal dan constraint tetap berstatus NOT VALID:

-- VALIDATE memeriksa ulang seluruh isi tabel: data warisan yang kotor ketahuan
ALTER TABLE pelanggan_lama VALIDATE CONSTRAINT pelanggan_kota_fk;

Output yang Dihasilkan:

text
psql:validasi-gagal.sql:2: ERROR:  insert or update on table "pelanggan_lama" violates foreign key constraint "pelanggan_kota_fk"
DETAIL:  Key (kota_id)=(99) is not present in table "kota".

Baris Tono ketahuan: kota_id 99 tidak pernah terdaftar di tabel kota. Validasi gagal tanpa efek samping — aturan tetap terpasang, data tidak berubah, dan tabel tetap bisa menerima tulisan baru yang sah. Setelah datanya diperbaiki, validasi tinggal dijalankan ulang:

-- Perbaiki data warisan, lalu jalankan validasi ulang
UPDATE pelanggan_lama SET kota_id = 3 WHERE id = 3;

ALTER TABLE pelanggan_lama VALIDATE CONSTRAINT pelanggan_kota_fk;

\d pelanggan_lama

Output yang Dihasilkan:

text
UPDATE 1
ALTER TABLE
           Table "public.pelanggan_lama"
 Column  |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
 id      | integer |           | not null | 
 nama    | text    |           | not null | 
 email   | text    |           |          | 
 kota_id | integer |           |          | 
Indexes:
    "pelanggan_lama_pkey" PRIMARY KEY, btree (id)
    "pelanggan_email_unik" UNIQUE CONSTRAINT, btree (email)
Foreign-key constraints:
    "pelanggan_kota_fk" FOREIGN KEY (kota_id) REFERENCES kota(kota_id)

UPDATE mengarahkan Tono ke kota Surabaya (kota_id 3), dan VALIDATE CONSTRAINT kini berhasil — penanda NOT VALID hilang dari hasil \d, menandakan constraint tersebut sudah sepenuhnya tuntas: seluruh baris, lama maupun baru, kini terjamin konsisten dengan tabel kota.

Mengganti Nama dan Melepas Constraint #

Constraint yang sudah terpasang punya nama, dan nama itu layak dirawat: error di log produksi jauh lebih mudah dibaca bila namanya deskriptif. RENAME CONSTRAINT mengubah nama tanpa menyentuh aturannya; DROP CONSTRAINT melepas aturan itu sama sekali:

sql kelola-nama.sql
-- RENAME CONSTRAINT mengubah nama tanpa mengubah aturannya
ALTER TABLE pelanggan_lama
    RENAME CONSTRAINT pelanggan_kota_fk TO pelanggan_kota_fkey;

-- DROP CONSTRAINT melepas aturan beserta index pendukungnya
ALTER TABLE pelanggan_lama
    DROP CONSTRAINT pelanggan_email_unik;

\d pelanggan_lama

Output yang Dihasilkan:

text
ALTER TABLE
ALTER TABLE
           Table "public.pelanggan_lama"
 Column  |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
 id      | integer |           | not null | 
 nama    | text    |           | not null | 
 email   | text    |           |          | 
 kota_id | integer |           |          | 
Indexes:
    "pelanggan_lama_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "pelanggan_kota_fkey" FOREIGN KEY (kota_id) REFERENCES kota(kota_id)

Hasil \d akhir memperlihatkan dua perubahan: foreign key kini bernama pelanggan_kota_fkey (nama yang mengikuti konvensi bawaan PostgreSQL tabel_kolom_fkey), dan baris index pelanggan_email_unik hilang — melepas unique constraint sekaligus membongkar index pendukungnya. Menyebut nama constraint secara eksplisit seperti pada contoh adalah satu-satunya cara untuk RENAME; untuk DROP, bila kita tidak hafal namanya, perintah \d nama_tabel selalu menampilkannya. Lepaskan constraint hanya setelah yakin tidak ada data yang bergantung padanya — melepas foreign key berarti kembali mengandalkan disiplin aplikasi untuk konsistensi antar tabel.

Catatan soal Lock #

Setiap varian ALTER TABLE pada chapter ini membutuhkan lock ACCESS EXCLUSIVE pada tabel saat perintahnya berjalan — selama lock itu terpasang, seluruh query lain (baca maupun tulis) menunggu. Yang membedakan adalah berapa lama lock itu dipegang: pemasangan constraint yang memindai seluruh isi tabel memegangnya sepanjang pemindaian (lama di tabel raksasa), sedangkan pemasangan dengan NOT VALID hanya sekejap karena tidak memindai apa pun; VALIDATE CONSTRAINT memakai lock SHARE UPDATE EXCLUSIVE yang lebih ringan sehingga tulisan harian tetap mengalir. Di tabel kecil latihan perbedaan ini tak terasa; di tabel produksi berjuta baris, memilih urutan operasi yang tepat adalah bedanya aplikasi berhenti lima menit dan aplikasi yang tidak terasa berhenti sama sekali. Mekanisme locking dan bagaimana transaction berperilaku saling menunggu dibahas tuntas di section tentang transaction dan concurrency.

Section Table Design kini tuntas: tabel yang kita rancang tidak lagi sekadar wadah, melainkan penjaga integritasnya sendiri — dari primary key, foreign key, dan unique, sampai check, default, generated column, identity, composite key, dan exclusion constraint. Struktur yang kokoh ini siap diisi dan digunakan lewat operasional harian database, dan operasional itu dimulai dari memasukkan data: INSERT, SELECT, UPDATE, dan DELETE menanti di Section Manipulasi Data.

Referensi #

Berkas Sumber

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

buat-tabel.sql

Unduh berkas
sql
-- Skenario: tabel rujukan + tabel pelanggan warisan yang belum bersih
CREATE TABLE kota (
    kota_id integer PRIMARY KEY,
    nama     text NOT NULL
);

CREATE TABLE pelanggan_lama (
    id      integer PRIMARY KEY,
    nama    text NOT NULL,
    email   text,
    kota_id integer
);

INSERT INTO kota VALUES
    (1, 'Jakarta'),
    (2, 'Bandung'),
    (3, 'Surabaya');

-- kota_id 99 tidak ada di tabel kota: data warisan yang kotor
INSERT INTO pelanggan_lama VALUES
    (1, 'Budi', '[email protected]', 1),
    (2, 'Siti', '[email protected]', 2),
    (3, 'Tono', '[email protected]', 99);

SELECT id, nama, email, kota_id
FROM pelanggan_lama
ORDER BY id;

insert-ditolak.sql

Unduh berkas
sql
-- NOT VALID bukan berarti tanpa penjagaan: tulisan BARU tetap diperiksa
INSERT INTO pelanggan_lama VALUES (4, 'Dewi', '[email protected]', 98);

kelola-nama.sql

Unduh berkas
sql
-- RENAME CONSTRAINT mengubah nama tanpa mengubah aturannya
ALTER TABLE pelanggan_lama
    RENAME CONSTRAINT pelanggan_kota_fk TO pelanggan_kota_fkey;

-- DROP CONSTRAINT melepas aturan beserta index pendukungnya
ALTER TABLE pelanggan_lama
    DROP CONSTRAINT pelanggan_email_unik;

\d pelanggan_lama

perbaiki-validasi.sql

Unduh berkas
sql
-- Perbaiki data warisan, lalu jalankan validasi ulang
UPDATE pelanggan_lama SET kota_id = 3 WHERE id = 3;

ALTER TABLE pelanggan_lama VALIDATE CONSTRAINT pelanggan_kota_fk;

\d pelanggan_lama

tambah-constraint.sql

Unduh berkas
sql
-- UNIQUE langsung dipasang: seluruh isi tabel diperiksa saat itu juga
ALTER TABLE pelanggan_lama
    ADD CONSTRAINT pelanggan_email_unik UNIQUE (email);

-- FK dengan NOT VALID: baris lama tidak diperiksa, tulisan baru tetap dijaga
ALTER TABLE pelanggan_lama
    ADD CONSTRAINT pelanggan_kota_fk
    FOREIGN KEY (kota_id) REFERENCES kota (kota_id) NOT VALID;

\d pelanggan_lama

validasi-gagal.sql

Unduh berkas
sql
-- VALIDATE memeriksa ulang seluruh isi tabel: data warisan yang kotor ketahuan
ALTER TABLE pelanggan_lama VALIDATE CONSTRAINT pelanggan_kota_fk;