# Integer vs Decimal untuk Uang di Postgres: Dua ERP, Dua Jawaban

> Saya memakai integer rupiah di satu produk dan Decimal(15,2) di produk lain. Ini cara rounding, persentase, JSON dan mata uang menentukan pemenangnya.

URL: https://www.matthewswong.com/id/blog/integer-rupiah-vs-numeric-decimal-postgres-erp/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: Backend

> **Key takeaway:** Simpan uang sebagai integer minor unit bila mata uangnya tidak punya subunit yang dipakai sungguhan dan tiap nilai tetap di bawah sekitar 2,1 miliar, seperti rupiah di point of sale. Pakai NUMERIC atau Prisma Decimal bila butuh angka desimal, banyak mata uang atau nilai lebih besar. Apa pun pilihannya, lakukan rounding sekali per komponen dan jauhkan float dari perhitungan.

Ada dua produk berbasis Postgres di meja saya yang berbeda pendapat soal apa itu satu rupiah. POS carwash menyimpan semua kolom uang sebagai Int biasa. ERP klien yang saya bangun untuk accounts receivable dan payable menyimpan kolom serupa sebagai Decimal dengan precision 15 dan scale 2. Keduanya berjalan di production, keduanya benar, dan perbedaannya bukan soal selera.

Tulisan ini membahas keputusannya, bukan alasan menolak float, yang sudah saya bahas di artikel terpisah. Saya membandingkan kedua desain dari sisi rounding, hitungan persentase, serialization dan multi-currency, memakai schema dan helper dari kedua codebase yang ditulis ulang sebagai contoh generik. Bila sebuah klaim adalah pilihan desain dan bukan hasil pengukuran, saya sebutkan. Saya tidak melakukan benchmark pada kedua desain.

## Dua produk, dua keputusan penyimpanan

POS carwash menyimpan harga item order, pembayaran, kasbon dan angka payroll sebagai Int. Rupiah secara teori punya subunit, tetapi tidak ada yang membayar dengan itu, jadi minor unit dan major unit adalah unit yang sama dan tidak perlu scaling. ERP berbeda dalam dua hal: invoice-nya membawa kode mata uang, dan kolom uangnya memakai Decimal dengan precision 15 dan scale 2, sehingga ada ruang untuk nilai pecahan.

Alasan memilih integer ketimbang float sudah ditulis di 

kenapa kami menyimpan uang sebagai integer rupiah

. Pertanyaan di sini lebih sempit dan lebih berguna: setelah float dicoret, tipe yang tepat itu hitungan integer dari minor unit, atau NUMERIC fixed-point? Jawabannya bergantung pada empat hal yang bisa Anda cek sebelum menulis migration.

- Apakah mata uangnya punya subunit yang benar-benar dipakai?
- Seberapa besar satu baris dan satu total bisa tumbuh?
- Berapa mata uang yang berbagi kolom yang sama?
- Siapa yang memakai angka itu: kode Anda sendiri, atau client milik orang lain?

## Apa wujud tiap pilihan di Postgres dan Prisma

Dua schema di bawah bersifat generik, tetapi bentuknya sama dengan yang asli. Versi integer menyimpan hitungan. Versi Decimal menyimpan angka fixed-point dengan scale yang dideklarasikan, dan membawa rate yang menghasilkan pajaknya supaya perubahan master data di kemudian hari tidak menulis ulang sejarah.

Angkanya penting. Dokumentasi PostgreSQL menyebut integer berukuran 4 byte dengan rentang minus 2147483648 sampai plus 2147483647, dan bigint berukuran 8 byte. NUMERIC dengan precision 15 dan scale 2 menampung 13 digit sebelum titik desimal dan 2 sesudahnya. Prisma memetakan Int ke integer dan mengembalikan number biasa, memetakan Decimal ke objek decimal.js, dan memetakan BigInt ke BigInt JavaScript. Kalau Decimal tidak diberi native type, Prisma memakai default decimal(65,30), yang hampir tidak pernah Anda inginkan untuk uang.

Integer overflow bukan galat pembulatan, melainkan exception. PostgreSQL melempar error untuk nilai di luar rentang tipenya, jadi checkout yang aman bertahun-tahun bisa gagal di hari satu agregat melewati 2147483647. Batasi nilainya di layer validasi supaya kegagalannya berupa 400 yang rapi, bukan 500.

## Rounding dan hitungan persentase

Rounding adalah titik di mana kedua desain benar-benar berbeda. Dengan integer, database tidak pernah membulatkan apa pun: saat nilai sampai di kolom, kode Anda sudah memutuskan jumlah unit utuhnya. Dengan NUMERIC, database bisa membulatkan, dan PostgreSQL mendokumentasikan bahwa numeric membulatkan nilai tengah menjauhi nol sedangkan double precision membulatkan ke genap terdekat di sebagian besar mesin. Perbedaan itu saja sudah cukup alasan untuk tidak membiarkan float menyentuh langkah rounding.

Hitungan persentase adalah masalah yang sama di kedua desain: kalikan basis dengan rate, lalu bulatkan. Ada tiga aturan yang saya bawa dari kedua codebase. Simpan rate di baris yang memakainya, supaya mengubah master rate tidak menulis ulang dokumen yang sudah diposting. Hitung dan bulatkan tiap komponen sendiri-sendiri lalu jumlahkan, jangan menjumlahkan bagian yang belum dibulatkan lalu membulatkan sekali di akhir. Dan sediakan tepat satu fungsi rounding yang dipanggil semua jalur, supaya layar accounts payable dan accounts receivable tidak bisa berbeda satu unit.

Kalau total yang tersimpan murni turunan dari kolom lain, database juga bisa memilikinya; 

generated column untuk data hasil hitung

 membahas kapan itu ide yang bagus.

Kalikan dulu sebelum membagi. Menghitung basis kali rate lebih dulu dan membagi seratus paling akhir menjaga nilai antara sepresisi yang diizinkan tipenya, alih-alih membulatkan pecahan terlalu dini.

## Serialization: di mana Decimal merepotkan

Int keluar dari Prisma sebagai number dan masuk ke JSON tanpa diubah. Decimal keluar sebagai objek decimal.js. toJSON-nya mengembalikan string, jadi response yang naif mengirim uang sebagai string, dan setiap client harus tahu itu. decimal.js juga tidak mempertahankan nol di belakang, sehingga nilai yang tersimpan dengan dua desimal bisa menjadi string tanpa nol itu kecuali Anda memanggil toFixed. Tetapkan string dengan scale tetap sebagai kontrak API, atau konversi secara eksplisit di satu tempat di batas aplikasi.

BigInt punya jebakan sendiri. MDN mendokumentasikan bahwa JSON.stringify pada nilai BigInt apa pun melempar TypeError, dan dokumentasi Prisma menunjukkan kegagalan yang sama dengan replacer function sebagai jalan keluarnya. Ini penting bahkan di desain integer, karena sum pada kolom integer di PostgreSQL mengembalikan bigint, sehingga report berbasis raw query memberi Anda BigInt walau semua kolom tersimpan adalah Int. Saya mengubahnya jadi string di batas API, bukan menyimpan BigInt di setiap baris.

Decimal juga merepotkan pada perbandingan biasa. Helper audit diff saya harus menormalkan Decimal dan number ke bentuk yang sama sebelum bisa bilang apakah sebuah field berubah, kalau tidak, nominal yang tidak berubah muncul sebagai edit. Integer dibandingkan dengan tanda sama dengan tiga kali dan tidak butuh itu.

## Multi-currency mengubah jawabannya

Integer minor unit bekerja bersih untuk rupiah karena minor unit sama dengan major unit. Begitu mata uang kedua berbagi kolom, integer telanjang jadi ambigu: apakah nilai tersimpan itu hitungan unit utuh atau per seratus? Anda butuh eksponen per mata uang, disimpan atau dicari, dan setiap jalur tampilan dan konversi harus menerapkannya. ERP menghindari itu dengan menyimpan kode mata uang di samping nominal dengan scale 2 yang tetap.

Itu pun tidak gratis: scale dua mengasumsikan setiap mata uang yang Anda terima punya dua angka desimal. Untuk sisi akuntansi konversi, tabel kurs dan jurnal selisih kurs, lihat 

akuntansi multi-currency di ERP

. Aturan saya: sistem satu mata uang dengan unit kasar layak memakai integer, dan apa pun yang mungkin menjadi multi-currency sebaiknya mulai dari NUMERIC.

## Tabel keputusan

Ini perbandingan yang akan saya berikan ke rekan satu tim. Ini perbandingan desain, bukan benchmark: dokumentasi PostgreSQL sendiri menyebut kalkulasi numeric lambat dibanding integer, tetapi saya belum mengukur selisihnya di kedua produk dan tidak akan menyebut besarnya.

Aspek

Integer minor unit (Int)

NUMERIC / Prisma Decimal

Tipe di Prisma

Int, dikembalikan sebagai number biasa

Decimal dengan native precision dan scale, dikembalikan sebagai objek decimal.js

Rentang

Sekitar 2,1 miliar per baris untuk integer 4 byte; sum butuh bigint

Sesuai precision yang dideklarasikan; 13 digit bulat pada precision 15, scale 2

Rounding

Sepenuhnya kode Anda, sebelum nilai disimpan

Kode Anda atau SQL; numeric membulatkan nilai tengah menjauhi nol

Hitungan persentase

Kali lalu bagi dan bulatkan di kode aplikasi

Aritmetika Decimal di kode atau numeric di SQL, rate disimpan di kolom sendiri

JSON dan API

Berupa number, gratis; agregat datang sebagai bigint dan perlu dikonversi ke string

Diserialisasi sebagai string; client harus mem-parse dan Anda harus menetapkan scale

Multi-currency

Butuh eksponen per mata uang di mana-mana

Satu scale tetap plus kode mata uang, bila semua mata uang berbagi scale

Perbandingan dan diff

Equality biasa

Normalkan dulu sebelum membandingkan Decimal dengan number

Paling cocok untuk

Satu mata uang tanpa subunit praktis, nilai terbatas, client milik sendiri

Pecahan, banyak mata uang, total besar, integrasi pihak ketiga

Kalau kolom Anda multi-currency sekaligus besar, tabel ini bukan pilihan yang tipis. Kalau single-currency dan kecil, integer menang di kesederhanaan, dan penghematannya terasa di setiap query, serializer dan test yang tidak perlu Anda tulis.

## Kegagalan yang ingin saya peringatkan: penyimpanan Decimal, aritmetika float

Membaca schema dan helper ERP berdampingan mengajarkan hal yang kurang enak. Kolomnya Decimal, DTO membatasi nilai maksimum sesuai kapasitas Decimal(15,2), namun helper pajaknya menghitung dengan number JavaScript biasa lalu membulatkan ke unit utuh. Kolom Decimal menjamin apa yang disimpan, bukan bagaimana nilainya dihitung.

Helper itu membawa epsilon guard kecil, dan komentarnya sendiri mencatat alasannya: hasil yang secara matematis tepat setengah ternyata muncul sebagai 5.4999999999999996, dan pembulatan biasa akan memotongnya. Guard itu bekerja, tetapi saya tetap lebih suka tidak membutuhkannya. Ini pengamatan desain dari membaca kode, bukan laporan insiden; saya tidak mengklaim itu pernah menghasilkan invoice yang salah.

Memilih NUMERIC tidak otomatis membuat perhitungan Anda desimal. Kalau nilai melewati Number sebelum dibulatkan, yang Anda punya adalah kolom fixed-point yang disuplai pipeline floating-point. Jaga aritmetika tetap di integer atau di objek Decimal dari ujung ke ujung.

## Yang akan saya ubah, dan checklist hari Senin

Batasan dulu. Kedua desain adalah keputusan dari proyek saya sendiri, bukan survei seluruh industri. Saya tidak punya angka performa, saya belum menjalankan produk integer dengan mata uang kedua, dan epsilon guard di atas adalah hal yang akan saya ganti dengan aritmetika Decimal atau integer bila saya memulai ERP dari awal.

Pada hari Senin, dalam urutan yang akan saya kerjakan:

- Tulis mata uangnya, nilai tunggal terbesar dan agregat terbesar yang Anda perkirakan, lalu bandingkan dengan rentang tipenya.
- Pilih satu fungsi rounding dan jadikan satu-satunya cara menghasilkan nominal tersimpan; hapus semua pemanggilan primitif rounding yang lain.
- Simpan rate di samping setiap persentase hasil hitung supaya perubahan master data tidak menulis ulang baris yang sudah diposting.
- Tentukan kontrak JSON untuk uang, number atau string, dan tulis test yang gagal bila bigint atau objek Decimal sampai ke response.
- Tambahkan batas validasi di DTO yang sama dengan limit kolom, supaya overflow menjadi 400 dan bukan 500.

Pilih representasi dari mata uang dan pemakainya, lalu tegakkan dari ujung ke ujung. Integer menang bila unitnya kasar, rentangnya terbatas dan Anda memiliki client-nya. NUMERIC menang bila ada pecahan, banyak mata uang atau integrasi milik orang lain. Apa pun pilihannya, satu fungsi rounding dan tidak ada float di pipeline lebih penting daripada tipenya.

## Sumber dan bacaan lanjutan

- Dokumentasi PostgreSQL: tipe numerik
- Dokumentasi PostgreSQL: fungsi agregat
- Dokumentasi Prisma: schema reference dan pemetaan tipe
- Dokumentasi Prisma: bekerja dengan field Decimal dan BigInt
- Dokumentasi API decimal.js

## Source links

- https://www.postgresql.org/docs/current/datatype-numeric.html
- https://www.postgresql.org/docs/current/functions-aggregate.html
- https://www.prisma.io/docs/orm/v7/reference/prisma-schema-reference
- https://www.prisma.io/docs/orm/v7/prisma-client/special-fields-and-types
- https://mikemcl.github.io/decimal.js/

## FAQ

### Sebaiknya uang disimpan sebagai integer atau decimal di Postgres?

Pakai integer minor unit bila Anda menangani satu mata uang tanpa subunit praktis, seperti rupiah, dan setiap nilai tetap di bawah rentang integer. Pakai NUMERIC bila butuh pecahan, banyak mata uang atau total yang lebih besar. Di kedua kasus, jauhkan float dari aritmetika.

### Berapa rentang kolom integer Postgres untuk uang?

PostgreSQL mendokumentasikan integer berukuran 4 byte, dari minus 2147483648 sampai plus 2147483647, dan bigint berukuran 8 byte. Nilai di luar rentang memunculkan error, bukan berputar. Ingat bahwa sum pada kolom integer mengembalikan bigint.

### Bagaimana Prisma mengembalikan field Decimal dan cara mengirimnya sebagai JSON?

Prisma mengembalikan field Decimal sebagai objek decimal.js yang toJSON-nya menghasilkan string. Artinya response biasa mengirim uang sebagai string. Pilih kontrak string dengan scale tetap atau konversi eksplisit di satu batas API, pakai toFixed bila perlu nol di belakang.

### Kenapa JSON.stringify melempar error pada BigInt dari Prisma?

JSON tidak punya tipe BigInt, jadi JSON.stringify melempar TypeError untuk nilai BigInt apa pun. Berikan replacer function yang mengubah bigint menjadi string, atau konversi di batas API. Ini muncul bahkan di desain integer karena sum pada kolom integer mengembalikan bigint.

### Apakah kolom Decimal berarti hitungan uang saya pasti presisi?

Tidak. Kolom hanya menjamin apa yang disimpan, bukan bagaimana nilainya dihitung. Kalau angka melewati Number JavaScript sebelum dibulatkan, error floating-point masih bisa masuk. Jaga aritmetika di integer atau objek Decimal, dan pakai satu fungsi rounding.

---

- HTML version: https://www.matthewswong.com/id/blog/integer-rupiah-vs-numeric-decimal-postgres-erp/
- English: https://www.matthewswong.com/en/blog/integer-rupiah-vs-numeric-decimal-postgres-erp/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
