Saat kita mengetik \d pesanan di psql, layar menampilkan daftar kolom, index, dan constraint seolah server punya juru keterangan khusus. Kenyataannya lebih elegan: seluruh pengetahuan itu tersimpan sebagai data biasa di sekumpulan table sistem yang bisa kita SELECT seperti table lain. Keluarga table itu — system catalog di schema pg_catalog — adalah sumber kebenaran tunggal bagi psql, bagi tools administrasi seperti pgAdmin, dan seharusnya juga bagi script monitoring yang kita tulis sendiri. Chapter ini memetakan katalog-katalog terpenting, lalu memakainya untuk hal-hal nyata: mendaftar objek, membaca definisi kolom, dan mengukur ukuran tabel.

pg_catalog: Skema yang Selalu Ada #

Setiap database PostgreSQL lahir dengan schema pg_catalog berisi ratusan table, view, dan function sistem. Skema ini tidak muncul di SHOW search_path:

sql
SHOW search_path;
text
 "$user", public
(1 row)

…tetapi PostgreSQL selalu mencarinya lebih dulu sebelum schema apa pun di path — ibarat entry implicit di urutan pertama. Itulah sebabnya SELECT count(*) FROM pg_class berhasil tanpa prefiks pg_catalog., dan sebabnya Anda sebaiknya tidak menamai tabel sendiri pg_class. Isinya pun bukan barang mewah: katalog adalah table biasa yang kebetulan ditulis oleh server sendiri setiap kali kita menjalankan CREATE TABLE atau perintah DDL lain.

Katalog Utama #

CatalogIsiContoh pemakaian
pg_classSatu baris untuk semua relasi: tabel, view, materialized view, index, sequence, toastDaftar objek, ukuran via relpages
pg_attributeSatu baris untuk setiap kolom setiap relasiDefinisi kolom, urutan attnum
pg_namespaceDaftar schemaTerjemahan oid schema ke nama
pg_typeSemua tipe data: bawaan, composite, enum, domainTipe kolom dan argumen function
pg_procSemua function dan procedureInventaris function di schema kita
pg_indexEntri index beserta definition pg_get_indexdefUraian kolom yang diindex
pg_constraintConstraint PK, UNIQUE, FK, CHECKVerifikasi desain tabel
pg_authid / pg_rolesDaftar role; pg_roles adalah view tanpa kolom passwordAudit siapa ada di server
pg_databaseDaftar database di clusterInventaris database
pg_tablespaceDaftar tablespaceLokasi penyimpanan fisik
pg_dependDependensi antar objek (view → tabel, kolom → sequence)Apa yang ikut terhapus saat DROP

Daftar ini tidak perlu dihafal — cukup ingat polanya: setiap jenis objek punya satu katalog. Butuh daftar tabel? pg_class. Daftar user? pg_roles. Daftar database? pg_database.

pg_class: Semua Relasi #

Kolom relkind membedakan jenis relasi di pg_class — dan “relasi” di sini berarti lebih dari tabel. File pertama berikut menyiapkan objek-objek contoh beraneka jenis, file kedua lalu membacanya dari catalog:

sql buat-objek.sql
-- Skema kecil untuk dijelajahi dari catalog
CREATE TABLE pelanggan (
  id serial PRIMARY KEY,
  nama varchar(50) NOT NULL,
  email varchar(100) UNIQUE
);

CREATE TABLE pesanan (
  id serial PRIMARY KEY,
  pelanggan_id integer NOT NULL REFERENCES pelanggan(id),
  total numeric(12,2) CHECK (total >= 0),
  dibuat_pada date NOT NULL DEFAULT DATE '2026-01-15'
);

CREATE INDEX pesanan_pelanggan_id_idx ON pesanan (pelanggan_id);

CREATE VIEW pesanan_ringkas AS
  SELECT id, pelanggan_id, total FROM pesanan;

CREATE MATERIALIZED VIEW ringkasan_pelanggan AS
  SELECT count(*) AS jumlah_pelanggan FROM pelanggan;

INSERT INTO pelanggan (nama, email) VALUES
  ('Budi', '[email protected]'),
  ('Siti', '[email protected]'),
  ('Toko Maju Jaya', '[email protected]');

INSERT INTO pesanan (pelanggan_id, total) VALUES
  (1, 150000.00),
  (1, 275000.00),
  (2, 99000.00);

-- ANALYZE mengisi statistik ukuran yang dibaca dari pg_class
ANALYZE pelanggan;
ANALYZE pesanan;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
CREATE INDEX
CREATE VIEW
SELECT 1
INSERT 0 3
INSERT 0 3
ANALYZE
ANALYZE
-- pg_class memuat SEMUA relasi, bukan hanya tabel
SELECT c.relname,
       CASE c.relkind
         WHEN 'r' THEN 'tabel'
         WHEN 'v' THEN 'view'
         WHEN 'm' THEN 'materialized view'
         WHEN 'i' THEN 'index'
         WHEN 'S' THEN 'sequence'
       END AS jenis,
       n.nspname AS skema
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
ORDER BY c.relname;

-- pg_catalog sendiri adalah kumpulan ratusan relasi
SELECT count(*) AS objek_di_pg_catalog
FROM pg_class
WHERE relnamespace = 'pg_catalog'::regnamespace;

Output yang Dihasilkan:

text
         relname          |       jenis       | skema  
--------------------------+-------------------+--------
 pelanggan                | tabel             | public
 pelanggan_email_key      | index             | public
 pelanggan_id_seq         | sequence          | public
 pelanggan_pkey           | index             | public
 pesanan                  | tabel             | public
 pesanan_id_seq           | sequence          | public
 pesanan_pelanggan_id_idx | index             | public
 pesanan_pkey             | index             | public
 pesanan_ringkas          | view              | public
 ringkasan_pelanggan      | materialized view | public
(10 rows)

 objek_di_pg_catalog 
---------------------
                 266
(1 row)

Sepuluh baris pertama berasal dari lima perintah DDL kita — masing-masing menyumbang lebih dari satu baris, karena serial menciptakan sequence, PRIMARY KEY dan UNIQUE menciptakan index, dan constraint tidak pernah menjadi baris pg_class tersendiri. Huruf di kolom relkind bermakna:

relkindJenis relasi
rTable biasa (row)
vView
mMaterialized view
iIndex
SSequence (huruf besar)

Perhatikan juga join di query itu: n.oid = c.relnamespace. Kolom oid adalah kolom identitas yang dimiliki hampir semua catalog — setiap baris pg_class punya oid, dan kolom relnamespace di pg_class berisi oid schema pemiliknya. Hubungan antar katalog dirangkai lewat oid seperti foreign key, hanya saja tidak dideklarasikan sebagai constraint. Hasil kedua menegaskan skala katalognya: 266 relasi hanya di pg_catalog — angka pastinya berbeda antar versi PostgreSQL dan bertambah bila ada extension yang memasang objeknya di sana.

pg_attribute: Daftar Kolom #

Setiap kolom setiap relasi — termasuk yang tidak pernah kita deklarasikan — tercatat di pg_attribute dengan nomor urut attnum:

sql kolom-tabel.sql
-- Kolom pelanggan menurut pg_attribute
SELECT a.attname AS kolom,
       format_type(a.atttypid, a.atttypmod) AS tipe,
       a.attnotnull AS wajib_diisi
FROM pg_attribute a
WHERE a.attrelid = 'pelanggan'::regclass
  AND a.attnum > 0
ORDER BY a.attnum;

-- Kolom sistem tersembunyi bernomor negatif
SELECT a.attname AS kolom_sistem, a.attnum
FROM pg_attribute a
WHERE a.attrelid = 'pelanggan'::regclass
  AND a.attnum < 0
ORDER BY a.attnum;

Output yang Dihasilkan:

text
 kolom |          tipe          | wajib_diisi 
-------+------------------------+-------------
 id    | integer                | t
 nama  | character varying(50)  | t
 email | character varying(100) | f
(3 rows)

 kolom_sistem | attnum 
--------------+--------
 tableoid     |     -6
 cmax         |     -5
 xmax         |     -4
 cmin         |     -3
 xmin         |     -2
 ctid         |     -1
(6 rows)

Beberapa hal menarik dari hasilnya:

  1. 'pelanggan'::regclass: cast teks ke tipe regclass menyelesaikan nama menjadi oid di pg_class — cara idiomatik merujuk tabel dari query catalog.
  2. format_type(atttypid, atttypmod): function bawaan yang merangkai oid tipe dan panjangnya menjadi teks yang persis seperti sintaks DDL — character varying(50), bukan sekadar nama tipe.
  3. Kolom bernomor negatif: enam baris kedua adalah kolom sistem yang dimiliki setiap tabel tanpa dideklarasikan — xmin dan xmax (identitas transaction MVCC), cmin/cmax (urutan command dalam transaction), ctid (lokasi fisik baris), dan tableoid (tabel asal baris partitioned). Sejak PostgreSQL 12, kolom oid tidak lagi otomatis ada di tabel user, tetapi di catalog sendiri kolom oid tetap hidup dan menjadi tulang punggung join seperti n.oid = c.relnamespace.

Ukuran Lewat relpages #

Kolom relpages di pg_class mencatat jumlah halaman 8 kB yang terpakai relasi — angka yang diisi ulang oleh ANALYZE dan autovacuum, dan kebetulan menjadi dasar perhitungan biaya query planner:

-- relpages: jumlah halaman 8 kB yang tercatat untuk relasi ini
SELECT c.relname, c.relpages,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total_ukuran
FROM pg_class c
WHERE c.relname IN ('pelanggan', 'pesanan')
ORDER BY c.relname;

Output yang Dihasilkan:

text
  relname  | relpages | total_ukuran 
-----------+----------+--------------
 pelanggan |        1 | 40 kB
 pesanan   |        1 | 40 kB
(2 rows)

Kedua tabel memang mungil — satu halaman untuk isinya. Fungsi pg_total_relation_size(oid) menghitung total byte table termasuk seluruh index dan toast-nya, dan sekali lagi menerima oid dari pg_class sebagai argumen; variasi pg_relation_size menghitung heap saja, pg_indexes_size index saja. Angka relpages sendiri baru diperbarui setelah ANALYZE, sehingga pada tabel yang baru diisi massal nilainya bisa terkatrol — ini kebiasaan lama planner, sekaligus alasan ANALYZE rutin dibahas lagi di chapter maintenance.

\d Adalah Query Catalog #

Bukti terakhir bahwa catalog hanyalah table biasa: meta-command \d di psql tidak memakai kanal rahasia apa pun. Ia menjalankan query ke pg_class, pg_attribute, pg_index, pg_constraint, dan kawan-kawannya, lalu memformat hasilnya:

sql d-psql.sql
-- \d hanyalah query catalog yang dibungkus rapi oleh psql
\d pesanan

Output yang Dihasilkan:

text
                                  Table "public.pesanan"
    Column    |     Type      | Collation | Nullable |               Default               
--------------+---------------+-----------+----------+-------------------------------------
 id           | integer       |           | not null | nextval('pesanan_id_seq'::regclass)
 pelanggan_id | integer       |           | not null | 
 total        | numeric(12,2) |           |          | 
 dibuat_pada  | date          |           | not null | '2026-01-15'::date
Indexes:
    "pesanan_pkey" PRIMARY KEY, btree (id)
    "pesanan_pelanggan_id_idx" btree (pelanggan_id)
Check constraints:
    "pesanan_total_check" CHECK (total >= 0::numeric)
Foreign-key constraints:
    "pesanan_pelanggan_id_fkey" FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)

Semua informasi di output itu — tipe kolom dari pg_type lewat pg_attribute, nilai default, dua index dari pg_index, satu CHECK dari pg_constraint, satu foreign key dengan referensinya — bisa diambil query catalog yang kita tulis sendiri, dan itulah yang dilakukan semua tool administrasi PostgreSQL di luar sana.

Standard SQL juga mendefinisikan antarmuka metadata resmi — information_schema — yang menawarkan hal yang sama dengan janji portabilitas lintas database. Perbandingan keduanya, beserta kapan memilih yang mana, menjadi pokok bahasan chapter berikutnya.

Referensi #

Berkas Sumber

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

buat-objek.sql

Unduh berkas
sql
-- Skema kecil untuk dijelajahi dari catalog
CREATE TABLE pelanggan (
  id serial PRIMARY KEY,
  nama varchar(50) NOT NULL,
  email varchar(100) UNIQUE
);

CREATE TABLE pesanan (
  id serial PRIMARY KEY,
  pelanggan_id integer NOT NULL REFERENCES pelanggan(id),
  total numeric(12,2) CHECK (total >= 0),
  dibuat_pada date NOT NULL DEFAULT DATE '2026-01-15'
);

CREATE INDEX pesanan_pelanggan_id_idx ON pesanan (pelanggan_id);

CREATE VIEW pesanan_ringkas AS
  SELECT id, pelanggan_id, total FROM pesanan;

CREATE MATERIALIZED VIEW ringkasan_pelanggan AS
  SELECT count(*) AS jumlah_pelanggan FROM pelanggan;

INSERT INTO pelanggan (nama, email) VALUES
  ('Budi', '[email protected]'),
  ('Siti', '[email protected]'),
  ('Toko Maju Jaya', '[email protected]');

INSERT INTO pesanan (pelanggan_id, total) VALUES
  (1, 150000.00),
  (1, 275000.00),
  (2, 99000.00);

-- ANALYZE mengisi statistik ukuran yang dibaca dari pg_class
ANALYZE pelanggan;
ANALYZE pesanan;

d-psql.sql

Unduh berkas
sql
-- \d hanyalah query catalog yang dibungkus rapi oleh psql
\d pesanan

jenis-relasi.sql

Unduh berkas
sql
-- pg_class memuat SEMUA relasi, bukan hanya tabel
SELECT c.relname,
       CASE c.relkind
         WHEN 'r' THEN 'tabel'
         WHEN 'v' THEN 'view'
         WHEN 'm' THEN 'materialized view'
         WHEN 'i' THEN 'index'
         WHEN 'S' THEN 'sequence'
       END AS jenis,
       n.nspname AS skema
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
ORDER BY c.relname;

-- pg_catalog sendiri adalah kumpulan ratusan relasi
SELECT count(*) AS objek_di_pg_catalog
FROM pg_class
WHERE relnamespace = 'pg_catalog'::regnamespace;

kolom-tabel.sql

Unduh berkas
sql
-- Kolom pelanggan menurut pg_attribute
SELECT a.attname AS kolom,
       format_type(a.atttypid, a.atttypmod) AS tipe,
       a.attnotnull AS wajib_diisi
FROM pg_attribute a
WHERE a.attrelid = 'pelanggan'::regclass
  AND a.attnum > 0
ORDER BY a.attnum;

-- Kolom sistem tersembunyi bernomor negatif
SELECT a.attname AS kolom_sistem, a.attnum
FROM pg_attribute a
WHERE a.attrelid = 'pelanggan'::regclass
  AND a.attnum < 0
ORDER BY a.attnum;

ukuran-tabel.sql

Unduh berkas
sql
-- relpages: jumlah halaman 8 kB yang tercatat untuk relasi ini
SELECT c.relname, c.relpages,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total_ukuran
FROM pg_class c
WHERE c.relname IN ('pelanggan', 'pesanan')
ORDER BY c.relname;