Kalau integer sempurna untuk rupiah utuh, kehidupan finansial aplikasi jarang sebersih itu: kurs rupiah-harian ditulis sampai dua desimal, bunga pinjaman 3,75 persen, diskon dihitung dari pembagian yang tidak habis. Menghitung nilai seperti ini dengan tipe floating point membuat rupiah bocor sedikit demi sedikit — pembulatan biner yang tidak terlihat di satu baris menumpuk jadi selisih nyata di laporan akhir bulan. PostgreSQL menjawabnya dengan tipe numeric: bilangan desimal dengan presisi bebas yang dihitung persis, digit demi digit, seperti yang kita lakukan dengan kalkulator. Chapter ini membahas cara mendeklarasikannya, arti presisi dan skala, serta membuktikan ketepatannya berhadapan dengan floating point.

Tipe numeric untuk Angka Persis #

numeric menyimpan nilai sebagai angka desimal sejati — bukan pendekatan biner — sehingga hasil penjumlahan, pengurangan, dan perkalian selalu persis. Standar SQL mendefinisikan dua nama untuk tipe ini, numeric dan decimal, dan di PostgreSQL keduanya identik sepenuhnya; pilih salah satu dan konsisten saja. Nama numeric yang kita pakai di seluruh course ini.

Sebagai gambaran kasar: integer dan bigint cepat karena berupa bilangan biner 4 dan 8 byte, tetapi keduanya tidak mengenal pecahan. real dan double precision mengenal pecahan tetapi menyimpannya sebagai pendekatan biner. numeric menempati posisi ketiga: pecahan persis dengan jumlah digit yang bisa mencapai ribuan, dibayar dengan perhitungan yang lebih lambat dan penyimpanan yang membesar mengikuti jumlah digitnya. Untuk kolom uang, ketepatan menang atas kecepatan.

Presisi dan Skala: numeric(p, s) #

Deklarasi lengkap tipe ini berbentuk numeric(p, s) dengan dua angka yang maknanya sering tertukar:

BagianNamaArtiContoh pada numeric(12, 2)
pPresisiTotal digit signifikan, sebelum dan sesudah komaMaksimal 12 digit
sSkalaJumlah digit setelah titik desimalSelalu 2 digit pecahan

Dengan demikian numeric(12, 2) menampung nilai terbesar 9.999.999.999,99 — sembilan digit untuk rupiah dan dua digit untuk pecahannya — pola yang pas untuk tagihan hampir Rp10 miliar. Skala 0 berarti bilangan bulat, misalnya numeric(5, 0); dan keduanya boleh dihilangkan: numeric polos berarti tanpa batas presisi maupun skala.

SELECT 123.456::numeric(5, 2) AS dibulatkan;

SELECT 1::numeric / 3 AS sepertiga;

Output yang Dihasilkan:

text
 dibulatkan 
------------
     123.46
(1 row)

       sepertiga        
------------------------
 0.33333333333333333333
(1 row)

Query pertama memperlihatkan perilaku cast ke numeric(5, 2): nilai 123.456 hanya punya izin dua digit pecahan, sehingga server membulatkannya menjadi 123.46 — dibulatkan, bukan dipotong menjadi 123.45. Query kedua memperlihatkan numeric polos: pembagian 1 / 3 dikerjakan dengan 20 digit pecahan (pembulatan default PostgreSQL untuk pembagian numeric), jauh lebih kaya dari 2 digit pecahan numeric(5, 2).

Membuat Tabel untuk Uang #

Tabel tagihan berikut adalah pola standar kolom uang di aplikasi nyata: numeric(12, 2) dengan NOT NULL. Nilainya kita isi dengan rupiah berpecahan:

sql buat-tabel.sql
CREATE TABLE tagihan (
    id        integer PRIMARY KEY,
    pelanggan text NOT NULL,
    jumlah    numeric(12, 2) NOT NULL
);

INSERT INTO tagihan (id, pelanggan, jumlah) VALUES
    (1, 'Budi', 150000.50),
    (2, 'Siti', 87500.25),
    (3, 'Toko Maju Jaya', 4200000.75);

SELECT id, pelanggan, jumlah
FROM tagihan
ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 3
 id |   pelanggan    |   jumlah   
----+----------------+------------
  1 | Budi           |  150000.50
  2 | Siti           |   87500.25
  3 | Toko Maju Jaya | 4200000.75
(3 rows)

Perhatikan outputnya: nilai yang kita tulis 150000.50 ditampilkan server sebagai 150000.50 dengan tepat dua digit pecahan — bukan 150000.499999... dan bukan 150000.5000001. Dua digit pecahan itu melekat pada nilainya karena skala 2 adalah bagian dari tipe; numeric(12,2) mengingat bahwa dirinya selalu punya dua tempat desimal, termasuk saat menampung angka bulat seperti 4500.00.

Pembulatan Otomatis Saat INSERT #

Apa yang terjadi kalau data mentah datang dengan tiga digit pecahan padahal skala hanya dua? Server tidak menolak — ia membulatkan ke skala yang dideklarasikan:

INSERT INTO tagihan (id, pelanggan, jumlah)
VALUES (4, 'Rina', 99999.999);

SELECT id, pelanggan, jumlah
FROM tagihan
WHERE id = 4;

Output yang Dihasilkan:

text
INSERT 0 1
 id | pelanggan |  jumlah   
----+-----------+-----------
  4 | Rina      | 100000.00
(1 row)

Baris Rina tersimpan sebagai 100000.00: nilai 99999.999 dibulatkan setengah ke atas menjadi 100000.00 sebelum masuk kolom. INSERT berhasil tanpa keluhan apa pun.

Ada satu twist pada aturan ini: pembulatan bisa membuat nilai melampaui batas presisi. Nilai 999.999 yang dibulatkan ke numeric(5, 2) menghasilkan 1000.00 — enam digit total, padahal presisinya hanya lima — sehingga server menolaknya dengan error numeric field overflow yang akan kita lihat bentuknya di bagian berikutnya.

Melebihi Presisi Ditolak #

Nilai yang jumlah digitnya (setelah pembulatan) melewati presisi tidak akan pernah tersimpan. Kita coba menagih 10.000.000.000,00 — sebelas digit rupiah, melewati batas 9.999.999.999,99 milik numeric(12, 2):

INSERT INTO tagihan (id, pelanggan, jumlah)
VALUES (5, 'Agus', 10000000000.00);

Output yang Dihasilkan:

text
psql:melebihi-presisi.sql:2: ERROR:  numeric field overflow
DETAIL:  A field with precision 12, scale 2 must round to an absolute value less than 10^10.

Error ini lebih cerewet dari error overflow pada integer: baris DETAIL menjelaskan persis aturannya — kolom dengan presisi 12 dan skala 2 harus dibulatkan ke nilai di bawah 10^10. Menaikkan plafon berarti mengubah deklarasinya, misalnya menjadi numeric(14, 2) lewat ALTER TABLE ... ALTER COLUMN jumlah TYPE numeric(14, 2) — dan tabel yang sudah terisi akan ditulis ulang seluruhnya, jadi pilih presisi yang cukup sejak awal.

numeric vs Floating Point untuk Uang #

Bukti terakhir sekaligus yang paling meyakinkan: kita isi dua kolom dengan nilai yang sama persis — satu numeric, satu double precision — lalu jumlahkan keduanya:

CREATE TABLE perbandingan (
    id             integer PRIMARY KEY,
    jumlah_numeric numeric(12, 2) NOT NULL,
    jumlah_float   double precision NOT NULL
);

INSERT INTO perbandingan (id, jumlah_numeric, jumlah_float) VALUES
    (1, 100000.10, 100000.10),
    (2, 200000.20, 200000.20),
    (3, 300000.15, 300000.15);

SELECT SUM(jumlah_numeric) AS total_numeric,
       SUM(jumlah_float) AS total_float
FROM perbandingan;

SELECT 0.1::numeric + 0.2::numeric = 0.3::numeric AS uji_numeric,
       0.1::float8 + 0.2::float8 = 0.3::float8 AS uji_float;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 3
 total_numeric |    total_float    
---------------+-------------------
     600000.45 | 600000.4500000001
(1 row)

 uji_numeric | uji_float 
-------------+-----------
 t           | f
(1 row)

Hasilnya mencolok: total_numeric berupa 600000.45 yang rapi, sedangkan total_float berupa 600000.4500000001 — sepuluh per-triliunan rupiah yang tidak kita tagihkan ke siapa pun, murni produk pembulatan biner. Query kedua meringkas akar masalahnya dalam satu baris: 0.1 + 0.2 sama dengan 0.3 menurut numeric (t) tetapi tidak menurut float (f).

Dari eksperimen terakhir timbul pertanyaan yang wajar: mengapa float bisa begitu salah padahal komputer justru dirancang untuk berhitung cepat? Jawabannya ada pada cara bilangan pecahan disimpan sebagai biner — dan memahaminya penting supaya kita tahu kapan float justru merupakan pilihan yang tepat. Itu topik chapter Floating Point.

Referensi #

Berkas Sumber

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

buat-tabel.sql

Unduh berkas
sql
CREATE TABLE tagihan (
    id        integer PRIMARY KEY,
    pelanggan text NOT NULL,
    jumlah    numeric(12, 2) NOT NULL
);

INSERT INTO tagihan (id, pelanggan, jumlah) VALUES
    (1, 'Budi', 150000.50),
    (2, 'Siti', 87500.25),
    (3, 'Toko Maju Jaya', 4200000.75);

SELECT id, pelanggan, jumlah
FROM tagihan
ORDER BY id;

melebihi-presisi.sql

Unduh berkas
sql
INSERT INTO tagihan (id, pelanggan, jumlah)
VALUES (5, 'Agus', 10000000000.00);

pembulatan-scale.sql

Unduh berkas
sql
INSERT INTO tagihan (id, pelanggan, jumlah)
VALUES (4, 'Rina', 99999.999);

SELECT id, pelanggan, jumlah
FROM tagihan
WHERE id = 4;

presisi-skala.sql

Unduh berkas
sql
SELECT 123.456::numeric(5, 2) AS dibulatkan;

SELECT 1::numeric / 3 AS sepertiga;

uang-vs-float.sql

Unduh berkas
sql
CREATE TABLE perbandingan (
    id             integer PRIMARY KEY,
    jumlah_numeric numeric(12, 2) NOT NULL,
    jumlah_float   double precision NOT NULL
);

INSERT INTO perbandingan (id, jumlah_numeric, jumlah_float) VALUES
    (1, 100000.10, 100000.10),
    (2, 200000.20, 200000.20),
    (3, 300000.15, 300000.15);

SELECT SUM(jumlah_numeric) AS total_numeric,
       SUM(jumlah_float) AS total_float
FROM perbandingan;

SELECT 0.1::numeric + 0.2::numeric = 0.3::numeric AS uji_numeric,
       0.1::float8 + 0.2::float8 = 0.3::float8 AS uji_float;