Lewati ke isi

03 β€” Desain Database Baru (PostgreSQL)

Dibaca oleh: Database Engineer & Backend Engineer. Tujuan: skema PostgreSQL yang rapi, bernama jelas, dan tetap mempertahankan model metadata-driven + trigger-first yang sudah terbukti.

Prinsip utama: kita merapikan, bukan merancang ulang bisnis. FIFO tetap FIFO, jurnal tetap double-entry, jenis_transaksi (eks datakode) tetap jadi mesin konfigurasi. Yang berubah: nama, tipe data, integritas, dan kerapian.


1. Prinsip Desain

  1. Nama lengkap & jelas, Bahasa Indonesia konsisten. barang, bukan brg. transaksi_detail, bukan d. Tabel/kolom harus terbaca tanpa kamus.
  2. snake_case, huruf kecil semua. Tanpa tanda kutip identifier.
  3. ID otomatis & aman: BIGINT GENERATED ALWAYS AS IDENTITY. Tidak ada lagi max(id)+1 manual.
  4. Integritas ditegakkan DB: FOREIGN KEY, NOT NULL, CHECK, UNIQUE di mana relevan. DB menolak data salah, bukan berharap aplikasi sopan.
  5. Tipe data benar: uang numeric(18,2), boolean boolean, waktu timestamptz, teks text, kode akun int (bukan decimal; nilai kode Γ— 100: 110.01 β†’ 11001).
  6. Audit seragam: setiap tabel inti punya created_at, updated_at, created_by, updated_by.
  7. Schema per tenant di database erp: data operasional berada di tenant_<kode>, data lintas-tenant di pusat, logika bersama di logika, dan audit di logs.
  8. Trigger-first dipertahankan, tetapi setiap trigger/fungsi wajib punya COMMENT ON + entri di Dokumen 05.
  9. Tanpa SQL-sebagai-data. Tidak ada lagi sql_biaya/master/detail string. Logika jadi fungsi PL/pgSQL bernama.
  10. **JSONB hanya untuk atribut fleksibel/jarang/snapshot β€” bukan data ** Tulang punggung akuntansi & stok tetap relasional ketat. Lihat Β§7.

2. Konvensi Penamaan (ringkas β€” detail di Dokumen 07)

Objek Pola Contoh
Tabel kata benda tunggal, snake_case barang, transaksi_detail
Kolom PK id id
Kolom FK <entitas>_id kontak_id, lokasi_id
Kolom boolean is_ / kata sifat jelas is_aktif, pengaruh_stok
Kolom uang tanpa awalan, numeric(18,2) total, hpp, dibayar
Kolom waktu _at timestamptz created_at, tanggal
Index ix_<tabel>_<kolom> ix_transaksi_tanggal
FK constraint fk_<tabel>_<entitas> fk_transaksi_detail_barang
Trigger trg_<tabel>_<aksi> trg_transaksi_detail_ai
Fungsi kata kerja _ hitung_stok, ambil_hpp_fifo

3. Pemetaan Tipe Data MySQL/MariaDB β†’ PostgreSQL

Lama (MariaDB) Baru (PostgreSQL) Catatan
bigint(20) bigint lebar angka tidak relevan di PG
int(11) integer
tinyint(1) boolean nilai 0/1 β†’ false/true (transformasi ETL)
decimal(12,2) (uang) numeric(18,2) dilebarkan, hindari overflow
double double precision hanya non-uang; uang TIDAK pakai double
varchar(n) varchar(n) atau text jika n besar/tak jelas β†’ text
longtext text
char(3) (kode) varchar(3)
decimal(12,2) (kode akun) int kode Γ— 100: 110.01 β†’ 11001
timestamp ... ON UPDATE timestamptz + trigger set_updated_at PG tak punya ON UPDATE bawaan
timestamp DEFAULT current_timestamp() timestamptz DEFAULT now()
date date
PK tanpa AUTO_INCREMENT bigint GENERATED ALWAYS AS IDENTITY hilangkan getidtrans() manual
ENGINE=InnoDB latin1 (default PG, UTF8) charset bersih
ON DUPLICATE KEY UPDATE INSERT ... ON CONFLICT ... DO UPDATE dipakai di port prosedur
IF(c,a,b) CASE WHEN c THEN a ELSE b END di port PL/pgSQL
SIGNAL SQLSTATE RAISE EXCEPTION error bisnis
variabel sesi @x variabel lokal DECLARE aman untuk connection pool

4. Skema Inti β€” DDL Acuan

Ini acuan desain, bukan file final. File final = skrip migrasi bernomor (lihat Dokumen 07). DDL di sini sengaja disederhanakan agar mudah dibaca tim; kolom jarang-pakai dari t/d dimasukkan bertahap setelah inventaris kolom selesai.

4.1 jenis_transaksi (eks datakode) β€” mesin konfigurasi

CREATE TABLE jenis_transaksi (
  kode               varchar(3)  PRIMARY KEY,            -- 'PJ','PB','RJ',...
  nama               varchar(50) NOT NULL,
  deskripsi          varchar(150),
  kategori           varchar(15),                        -- TRANSAKSI/KEUANGAN/OPNAME/...
  pengaruh_stok      boolean NOT NULL DEFAULT false,     -- eks hitungstok
  barang_di_debit    boolean NOT NULL DEFAULT false,     -- eks barangdebit (arah mutasi)
  kas_di_debit       boolean NOT NULL DEFAULT false,     -- eks kasdebit
  hitung_hpp         boolean NOT NULL DEFAULT false,     -- eks hpp
  hpp_dari_beli      boolean NOT NULL DEFAULT false,     -- eks hppbeli
  akun_utama_kode    int REFERENCES akun(kode),
  akun_kredit_kode   int REFERENCES akun(kode),
  akun_diskon_kode   int REFERENCES akun(kode),
  akun_biaya_kode    int REFERENCES akun(kode),
  akun_hpp_kode      int REFERENCES akun(kode),
  akun_pembulatan_kode int REFERENCES akun(kode),
  urut               integer,
  is_sistem          boolean NOT NULL DEFAULT false,
  created_at         timestamptz NOT NULL DEFAULT now(),
  updated_at         timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE jenis_transaksi IS
  'Master & konfigurasi perilaku akuntansi/stok per jenis transaksi (eks datakode).';

4.2 akun (eks rekening) β€” Bagan Akun

Hierarki tiga level: akun_klas (1–9) β†’ akun_subklas (110–800) β†’ akun (11001, 12000, …). Sumber: tabel legacy klas, subklas, rekening. Normalisasi kode: rekening.kode decimal(12,2) Γ— 100 β†’ int.

CREATE TABLE akun_klas (
  id    int PRIMARY KEY,       -- 1..9 (= noklasifikasi legacy)
  nama  varchar(80) NOT NULL
);

CREATE TABLE akun_subklas (
  id           int PRIMARY KEY,    -- 110..800 (= nosubklasifikasi legacy)
  klas_id      int NOT NULL REFERENCES akun_klas(id),
  nama         varchar(80) NOT NULL,
  saldo_normal char(1) NOT NULL CHECK (saldo_normal IN ('D','K'))  -- dari klas.dk
);

CREATE TABLE akun (
  kode       int PRIMARY KEY,      -- kode legacy Γ— 100: 110.01 β†’ 11001
  subklas_id int REFERENCES akun_subklas(id),
  nama       varchar(80) NOT NULL, -- eks rekening.akun
  is_aktif   boolean NOT NULL DEFAULT true,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

4.3 barang (eks brg)

CREATE TABLE barang (
  id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  kode            varchar(20) NOT NULL UNIQUE,
  nama            varchar(100) NOT NULL,
  barcode         varchar(30),
  golongan_id     bigint NOT NULL REFERENCES barang_golongan(id),
  kategori_id     bigint REFERENCES barang_kategori(id),
  jenis_id        bigint REFERENCES barang_jenis(id),
  merk_id         bigint REFERENCES barang_merk(id),
  satuan_default_id bigint REFERENCES satuan(id),
  tanpa_stok      boolean NOT NULL DEFAULT false,   -- eks nostok (jasa/non-stok)
  is_aktif        boolean NOT NULL DEFAULT true,
  created_at      timestamptz NOT NULL DEFAULT now(),
  updated_at      timestamptz NOT NULL DEFAULT now()
);

4.4 kontak (eks ktk)

CREATE TABLE kontak (
  id            bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  kode          varchar(20) NOT NULL UNIQUE,
  nama          varchar(100) NOT NULL,
  is_pelanggan  boolean NOT NULL DEFAULT false,   -- eks flag c
  is_supplier   boolean NOT NULL DEFAULT false,   -- eks flag s
  is_pegawai    boolean NOT NULL DEFAULT false,   -- eks flag p
  perusahaan    varchar(80),
  is_aktif      boolean NOT NULL DEFAULT true,    -- eks aktif=1 (semua flag lama jadi boolean)
  created_at    timestamptz NOT NULL DEFAULT now(),
  updated_at    timestamptz NOT NULL DEFAULT now()
);

Peran kontak NON-eksklusif (penting). Di sistem lama: ktk.c=customer, ktk.s=supplier, ktk.p=pegawai. Satu kontak bisa punya lebih dari satu peran sekaligus (mis. seorang pegawai yang juga supplier). Karena itu peran dimodelkan sebagai tiga boolean terpisah (is_pelanggan/is_supplier/ is_pegawai), bukan satu kolom "tipe" tunggal. Semua flag angka lama (aktif, c, s, p, dst.) β†’ boolean eksplisit (utang S4/K6 Dokumen 01).

4.5 transaksi (eks t) β€” header universal

CREATE TABLE transaksi (
  id                bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  nomor_transaksi   varchar(40) NOT NULL UNIQUE,           -- jenis.nomor_bukti (TANPA kdpc)
  nomor_transaksi_lama varchar(40),                        -- notrans lama (kdpc.*) utk telusur histori
  jenis_kode        varchar(3)  NOT NULL REFERENCES jenis_transaksi(kode),
  nomor_bukti       bigint      NOT NULL,
  sumber_perangkat  varchar(10),                           -- METADATA: desktop/mobile/web (eks kdpc, BUKAN kunci)
  tanggal           timestamptz NOT NULL DEFAULT now(),
  kontak_id         bigint REFERENCES kontak(id),
  pegawai_id        bigint REFERENCES kontak(id),
  lokasi_id         bigint REFERENCES lokasi(id),
  lokasi_tujuan_id  bigint REFERENCES lokasi(id),     -- utk pemindahan (PL)
  cabang_id         bigint REFERENCES cabang(id),     -- eks divisi_id (devisi→cabang, Dokumen 08 §5)
  subtotal          numeric(18,2) NOT NULL DEFAULT 0,
  diskon            numeric(18,2) NOT NULL DEFAULT 0,
  biaya             numeric(18,2) NOT NULL DEFAULT 0,
  pembulatan        numeric(18,2) NOT NULL DEFAULT 0,
  total             numeric(18,2) NOT NULL DEFAULT 0,
  dibayar           numeric(18,2) NOT NULL DEFAULT 0,
  sisa_kredit       numeric(18,2) NOT NULL DEFAULT 0,
  total_pelunasan   numeric(18,2) NOT NULL DEFAULT 0,
  hpp_total         numeric(18,2) NOT NULL DEFAULT 0,
  qty_total         numeric(18,2) NOT NULL DEFAULT 0,
  jatuh_tempo       date,
  termin_kode       varchar(20) REFERENCES termin(kode),
  akun_kode         int REFERENCES akun(kode),
  akun_kas_kode     int REFERENCES akun(kode),
  akun_kredit_kode  int REFERENCES akun(kode),
  harga_tingkat_kode varchar(20),
  status            smallint NOT NULL DEFAULT 0,           -- status eksplisit
  keterangan        varchar(150),
  catatan           text,
  dibuat_oleh       varchar(40) NOT NULL,
  created_at        timestamptz NOT NULL DEFAULT now(),
  updated_at        timestamptz NOT NULL DEFAULT now(),
  UNIQUE (jenis_kode, nomor_bukti)                         -- tenant = 1 DB, jadi tak perlu kdpc
);
CREATE INDEX ix_transaksi_tanggal      ON transaksi (tanggal);
CREATE INDEX ix_transaksi_jenis        ON transaksi (jenis_kode);
CREATE INDEX ix_transaksi_kontak       ON transaksi (kontak_id);

Catatan: kolom cek (sinyal kontrol trigger lama) dihapus. Perilaku khusus yang dulu diatur lewat cek=56/66/... diganti dengan kolom status/asal_proses eksplisit + parameter fungsi (lihat Dokumen 05 Β§kontrol alur).

4.6 transaksi_detail (eks d)

CREATE TABLE transaksi_detail (
  id                bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  transaksi_id      bigint NOT NULL REFERENCES transaksi(id) ON DELETE CASCADE,
  urut              integer NOT NULL,
  barang_id         bigint NOT NULL REFERENCES barang(id),
  barang_kode       varchar(20),                        -- snapshot saat transaksi
  qty_nota          numeric(14,2) NOT NULL,
  satuan_id         bigint REFERENCES satuan(id),
  isi_konversi      numeric(14,2) NOT NULL DEFAULT 1,
  qty               numeric(14,2) NOT NULL,             -- qty_nota * isi_konversi
  mutasi_stok       numeric(14,2) NOT NULL DEFAULT 0,   -- Β± qty (eks 'mutasi')
  harga             numeric(18,2) NOT NULL DEFAULT 0,
  hpp               numeric(18,2) NOT NULL DEFAULT 0,
  diskon            numeric(18,2) NOT NULL DEFAULT 0,
  diskon_nota       numeric(18,2) NOT NULL DEFAULT 0,
  jumlah            numeric(18,2) NOT NULL DEFAULT 0,    -- harga * qty_nota
  total             numeric(18,2) NOT NULL DEFAULT 0,    -- jumlah - diskon
  lokasi_id         bigint REFERENCES lokasi(id),
  lokasi_tujuan_id  bigint REFERENCES lokasi(id),
  ref_detail_id     bigint REFERENCES transaksi_detail(id), -- retur/pelunasan
  ref_transaksi_id  bigint REFERENCES transaksi(id),
  poin              numeric(14,2) NOT NULL DEFAULT 0,
  keterangan        varchar(150),
  created_at        timestamptz NOT NULL DEFAULT now(),
  updated_at        timestamptz NOT NULL DEFAULT now(),
  UNIQUE (transaksi_id, urut)
);
CREATE INDEX ix_trxd_transaksi ON transaksi_detail (transaksi_id);
CREATE INDEX ix_trxd_barang    ON transaksi_detail (barang_id);

4.6A transaksi_draft β€” area kerja sebelum dokumen final

Fitur baru pasca migrasi legacy memakai pola:

transaksi_draft -> transaksi
transaksi_draft_detail -> transaksi_detail

Tujuannya agar dokumen yang masih berubah-ubah tidak langsung menyentuh transaksi_detail, karena transaksi_detail sudah menjadi sumber trigger stok, FIFO, HPP, dan jurnal.

Aturan desain:

  • transaksi_draft dan transaksi_draft_detail adalah tenant-local.
  • Tidak ada trigger stok/FIFO/jurnal pada transaksi_draft_detail.
  • Semua jenis draft tetap memakai jenis_kode dari jenis_transaksi.
  • Draft bisa mewakili order penjualan, order pembelian, permintaan mutasi, perintah produksi, bahan produksi, dan hasil produksi.
  • Realisasi draft ke transaksi final dicatat di transaksi_draft_relasi dan transaksi_draft_detail_relasi.
  • Relasi antar transaksi final tetap memakai kolom referensi existing seperti ref_transaksi_id dan ref_detail_id, bukan tabel relasi baru.

Migrasi fitur ini tidak mengambil data dari legacy dan direncanakan sebagai seri akhir:

V900__tracking_draft_transaksi.sql
V901__tracking_relasi_draft_transaksi.sql
V902__tracking_produksi_mvp.sql
V903__tracking_jenis_transaksi_seed.sql
V904__tracking_view_operasional.sql

DDL rinci dan checklist review ada di:

4.7 Stok realtime stok (eks s) β€” cache cepat + jangkar rekonsiliasi

Pola pemilik (disengaja, dipertahankan): stok per lokasi = tampilan cepat & realtime (dijaga trigger); sumber kebenaran tetap SUM(mutasi_stok); selisih antara keduanya = alat audit untuk menemukan transaksi/logika yang salah.

CREATE TABLE stok (                      -- eks `s` (per lokasi)
  barang_id   bigint NOT NULL REFERENCES barang(id),
  lokasi_id   bigint NOT NULL REFERENCES lokasi(id),
  qty         numeric(18,2) NOT NULL DEFAULT 0,
  updated_at  timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (barang_id, lokasi_id)
);
COMMENT ON TABLE stok IS
  'Cache stok per lokasi = SUM(transaksi_detail.mutasi_stok). Sumber kebenaran tetap detail.';

-- Total stok per barang (eks brg.stok) diekspos lewat v_barang.stok_total.
-- View = selalu benar, tak bisa melenceng.
CREATE VIEW v_barang AS
  SELECT b.id, b.kode, b.nama, COALESCE(s.qty_total, 0) AS stok_total
  FROM barang b
  LEFT JOIN (SELECT barang_id, SUM(qty) AS qty_total
               FROM stok GROUP BY barang_id) s ON s.barang_id = b.id;

-- ALAT AUDIT RESMI: bandingkan cache vs hitung-ulang dari sumber.
-- Baris yang muncul di sini = ada yang salah, langsung ditelusuri.
CREATE VIEW v_stok_selisih AS
  SELECT s.barang_id, s.lokasi_id,
         s.qty                              AS qty_cache,
         COALESCE(h.qty_benar, 0)           AS qty_seharusnya,
         s.qty - COALESCE(h.qty_benar, 0)   AS selisih
  FROM stok s
  LEFT JOIN LATERAL (
     SELECT SUM(d.mutasi_stok) AS qty_benar
     FROM transaksi_detail d
     JOIN transaksi t ON t.id = d.transaksi_id
     WHERE d.barang_id = s.barang_id
       AND (t.lokasi_id = s.lokasi_id OR t.lokasi_tujuan_id = s.lokasi_id)
       AND t.jenis_kode <> 'GP'
  ) h ON true
  WHERE s.qty <> COALESCE(h.qty_benar, 0);

brg.stok lama (total keseluruhan) diganti view tenant v_barang.stok_total β€” tidak bisa "melenceng" karena dihitung saat dibaca. Jika butuh kecepatan ekstra, boleh jadi kolom cache barang.stok_total yang dijaga trigger (pola lama), dengan konsekuensi harus ikut direkonsiliasi. Default: pakai view.

4.8 FIFO untuk HPP

CREATE TABLE fifo_lapisan (             -- eks fifo
  id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  detail_masuk_id bigint NOT NULL REFERENCES transaksi_detail(id),
  transaksi_id bigint NOT NULL REFERENCES transaksi(id),
  barang_id    bigint NOT NULL REFERENCES barang(id),
  qty_masuk    numeric(18,2) NOT NULL,
  qty_keluar   numeric(18,2) NOT NULL DEFAULT 0,
  qty_sisa     numeric(18,2) NOT NULL,
  harga        numeric(18,2) NOT NULL,
  created_at   timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ix_fifo_barang_sisa ON fifo_lapisan (barang_id) WHERE qty_sisa > 0;

CREATE TABLE fifo_konsumsi (       -- eks fifokeluar
  id            bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  lapisan_id    bigint NOT NULL REFERENCES fifo_lapisan(id),
  detail_keluar_id bigint NOT NULL REFERENCES transaksi_detail(id),
  transaksi_id  bigint NOT NULL REFERENCES transaksi(id),
  barang_id     bigint NOT NULL REFERENCES barang(id),
  qty           numeric(18,2) NOT NULL
);

4.9 Jurnal jurnal (eks j) β€” sekarang PUNYA ID & integritas

CREATE TABLE jurnal (
  id            bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  transaksi_id  bigint NOT NULL REFERENCES transaksi(id) ON DELETE CASCADE,
  nomor_transaksi varchar(40) NOT NULL,
  sumber        varchar(20) NOT NULL,                 -- eks 'data' (TR/TRKREDIT/...)
  akun_kode     int         NOT NULL REFERENCES akun(kode),
  uraian        varchar(150),
  debit         numeric(18,2) NOT NULL DEFAULT 0,
  kredit        numeric(18,2) NOT NULL DEFAULT 0,
  created_at    timestamptz NOT NULL DEFAULT now(),
  CHECK (debit >= 0 AND kredit >= 0),
  CHECK (NOT (debit > 0 AND kredit > 0))              -- satu baris satu sisi
);
CREATE INDEX ix_jurnal_transaksi ON jurnal (transaksi_id);
CREATE INDEX ix_jurnal_akun      ON jurnal (akun_kode);

Tambahan integritas yang dulu tidak ada: uji keseimbangan. Sediakan view v_jurnal_tidak_imbang (mirip cek_jurnal_balance lama) + uji otomatis di migrasi: setiap transaksi harus SUM(debit) = SUM(kredit).


5. Yang Berubah dari Sistem Lama (ringkas, untuk tim)

Aspek Lama Baru
Penamaan t,d,brg,ktk transaksi,transaksi_detail,barang,kontak
ID max(id)+1 manual GENERATED ALWAYS AS IDENTITY
PK detail string idtrans.idbarang bigint identity + UNIQUE(transaksi_id, urut)
Kode akun decimal(12,2) int (kode Γ— 100: 110.01 β†’ 11001)
Boolean tinyint(1) campur boolean tegas
Sinyal cek angka ajaib 56/66/7 kolom status eksplisit
SQL-as-data sql_biaya,master,detail fungsi PL/pgSQL bernama
Charset latin1 UTF8
Jurnal tanpa PK PK + FK + CHECK keseimbangan
Penomoran kdpc (kode perangkat) di nomor sequence server; kdpc→metadata sumber_perangkat (Dok 08 §1)
Bagan Akun per tenant, kode beda-beda master standar di pusat, salinan lokal (Dok 08 Β§2)
devisi multi-perusahaan via iddevisi (hardcode) cabang (sub-entitas dalam tenant) (Dok 08 Β§5)
Stok total kolom cache brg.stok view tenant v_stok + v_stok_selisih (audit)
Login 2 pintu (opr desktop, inet mobile) 1 identitas pusat.pengguna, 1 API (Dok 08 Β§3)
Organisasi 120 tabel + 63 view tercampur tanpa pola fisik yang jelas schema pusat/logika/logs + tenant_<kode>

6. Yang TIDAK Berubah (jaga jangan rusak)

  • Model 1 tabel transaksi untuk semua jenis (transaksi/transaksi_detail).
  • jenis_transaksi (datakode) sebagai mesin pengatur akuntansi & stok.
  • Trigger-first: stok, HPP FIFO, jurnal tetap dihitung otomatis oleh DB.
  • Logika bisnis FIFO & double-entry β€” dipindah apa adanya (Dokumen 05).

7. Kapan Pakai JSONB (dan Kapan Jangan)

Pemilik tertarik pada konsep "menyimpan JSON di field" tapi belum yakin untuk apa. Ini jawabannya. PostgreSQL punya tipe jsonb (JSON ter-indeks, bisa di-query). Berguna bila dipakai di tempat yang tepat, berbahaya bila salah.

βœ… Pakai JSONB untuk: atribut fleksibel / jarang / snapshot

Kasus Contoh kolom
Spesifikasi yang beda per jenis barang (oli: viskositas; ban: ukuran; jasa: kosong) barang.atribut jsonb (pengganti brgspek yang kaku)
Pengaturan/konfigurasi per tenant/cabang/pengguna cabang.pengaturan jsonb, atau tabel konfigurasi tenant bila diperlukan
Snapshot/jejak audit (rekam kondisi data saat berubah) logs.audit.data_lama jsonb, data_baru jsonb
Payload mentah integrasi luar (respons API, webhook) sebelum diproses tabel integrasi khusus dengan kolom payload jsonb
Metadata lepas yang tak pernah jadi syarat JOIN/SUM akuntansi *.meta jsonb

JSONB di PostgreSQL bisa di-index (GIN) β†’ pencarian atribut tetap cepat. Cocok untuk data yang bentuknya tak seragam dan tak ingin bikin puluhan kolom NULL.

❌ JANGAN pakai JSONB untuk: data inti yang dihitung/direlasi/diaudit

  • Uang, qty, hpp, saldo, debit/kredit, jurnal β†’ wajib kolom numeric dengan FK/CHECK. JANGAN simpan total/HPP di dalam JSON.
  • Apa pun yang menjadi FOREIGN KEY atau sering di-WHERE/JOIN/SUM/ GROUP BY (akun, barang_id, kontak_id, tanggal, jenis_kode).
  • Pengganti normalisasi karena malas bikin tabel. JSON bukan alasan menghindari desain relasional. Kalau datanya berstruktur & dipakai relasi β†’ tabel, bukan JSON.

Aturan praktis untuk ERP ini

Tulang punggung akuntansi & stok = relasional ketat (FK + numeric + CHECK). JSONB = lemari serbaguna untuk atribut variatif/jarang/snapshot. JSONB melengkapi skema, bukan menggantikannya.

Tegas: transaksi, transaksi_detail, jurnal, fifo_lapisan, fifo_konsumsi, stok, akun tetap kolom relasional penuh β€” tanpa JSON. JSONB boleh muncul di barang.atribut, *.pengaturan, *.meta, dan tabel audit/integrasi.


Langkah berikut: Dokumen 04 (multi-tenant) lalu Dokumen 05 (port trigger). DDL di sini difinalkan menjadi skrip migrasi V001__template_tenant.sql dst. (Dokumen 07).