Chapter sebelumnya menutup dengan misteri: mengapa double precision menjumlahkan rupiah menghasilkan 600000.4500000001 alih-alih 600000.45? Jawabannya bukan bug PostgreSQL — ia sifat dasar floating point, format bilangan yang dipakai semua komputer modern untuk pecahan. Format ini cepat dan ringkas, sehingga tetap jadi tulang punggung komputasi ilmiah, grafik, dan sensor. Tetapi cara kerjanya menyimpan angka dalam biner membuat sebagian desimal — termasuk 0.1 — mustahil disimpan persis, seperti mustahilnya menulis sepertiga dalam desimal tanpa pengulangan. Chapter ini membedah mekanisme itu dengan bukti-bukti langsung dari server, membedakan real dari double precision, dan menutup dengan panduan kapan float aman dipakai.

Dua Ukuran Floating Point #

PostgreSQL mengikuti standar IEEE 754 dengan dua ukuran tipe floating point:

TipePenyimpananPresisi desimalRentang nilai
real4 byte±6 digitsekitar 1.18e-38 sampai 3.40e+38
double precision8 byte±15 digitsekitar 2.23e-308 sampai 1.80e+308

Angka “presisi ±6 digit” berarti real hanya dapat diandalkan sampai enam digit signifikan — angka ketujuh dan seterusnya sudah bisa melenceng. Rentangnya tampak raksasa (hingga 10 pangkat 38), tetapi presisi yang sempit itulah harga yang dibayar: hanya 32 bit tersedia, dibagi antara tanda, pangkat, dan mantisa.

Mengapa 0.1 Tidak Pernah Tepat #

Floating point menyimpan pecahan sebagai penjumlahan pangkat dua: setengah, seperempat, seperdelapan, dan seterusnya. Desimal 0.1 sama dengan biner 0.0001100110011... yang berulang tanpa henti, sehingga harus dipotong pada suatu titik — dan hasil potongannya selalu sedikit lebih besar atau sedikit lebih kecil dari 0.1 sejati. Kita buktikan:

SELECT 0.1::float8 + 0.2::float8 AS jumlah;

SELECT 0.1::float8 + 0.2::float8 - 0.3::float8 AS selisih;

SELECT 0.1::float8 + 0.2::float8 = 0.3::float8 AS sama_dengan_03;

Output yang Dihasilkan:

text
       jumlah        
---------------------
 0.30000000000000004
(1 row)

        selisih        
-----------------------
 5.551115123125783e-17
(1 row)

 sama_dengan_03 
----------------
 f
(1 row)

Tiga hasil itu menceritakan satu kisah. Query pertama: 0.1 dan 0.2 masing-masing sudah disimpan sebagai pendekatan biner, dan jumlah keduanya tampil sebagai 0.30000000000000004 — bukan 0.3. Query kedua mengukur besarnya kekeliruan itu: selisih terhadap 0.3 sejati adalah 5.55e-17, satu satuan di urutan ke-17. Query ketiga menegaskan konsekuensinya: perbandingan = mengembalikan f, karena nilai yang tersimpan memang berbeda bit demi bit.

Perhatikan juga bahwa psql tidak menampilkan seluruh digit biner yang tersimpan — 0.1::float8 tampil rapi sebagai 0.1 karena tampilan default PostgreSQL memakai representasi desimal terpendek yang masih menghasilkan nilai biner yang sama ketika dibaca ulang. Kedalaman galat baru terlihat begitu nilainya dijumlahkan atau dibandingkan, seperti pada dua query pertama.

Presisi real Hanya ±6 Digit #

Kalau double precision masih sanggup menyimpan 123456789 utuh, real sudah tidak sanggup. Karena hanya punya ±6 digit presisi, real membulatkan lebih agresif — dan pelebaran cast ke double precision (yang dilakukan tanpa kehilangan apa pun) membuat pembulatan itu terlihat telanjang:

SELECT 36.6::real AS suhu_real,
       36.6::real::float8 AS nilai_yang_disimpan;

SELECT 123456789::real AS real_hanya_tujuh_digit,
       123456789::float8 AS double_presisi;

CREATE TABLE sensor (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    suhu  real NOT NULL
);

INSERT INTO sensor (id, nama, suhu) VALUES
    (1, 'Ruang Server', 36.6),
    (2, 'Gudang', 24.1);

SELECT id, nama, suhu,
       suhu::float8 AS nilai_sebenarnya
FROM sensor
ORDER BY id;

Output yang Dihasilkan:

text
 suhu_real | nilai_yang_disimpan 
-----------+---------------------
      36.6 |  36.599998474121094
(1 row)

 real_hanya_tujuh_digit | double_presisi 
------------------------+----------------
          1.2345679e+08 |      123456789
(1 row)

CREATE TABLE
INSERT 0 2
 id |     nama     | suhu |  nilai_sebenarnya  
----+--------------+------+--------------------
  1 | Ruang Server | 36.6 | 36.599998474121094
  2 | Gudang       | 24.1 | 24.100000381469727
(2 rows)

Mari kita bedah hasilnya:

  1. Query pertama: 36.6 disimpan sebagai real lalu dilebarkan ke double precision memperlihatkan nilai yang benar-benar tersimpan: 36.599998474121094. Kolom suhu_real menampilkan 36.6 karena pembulatan tampilan, tetapi isi memorinya bukan 36.6 — dan itulah yang dipakai saat berhitung.
  2. Query kedua: 123456789 (sembilan digit) melampaui kapasitas ±6 digit real, sehingga tersimpan sebagai 1.2345679e+08 — nilai persisnya 123456792, tiga digit terakhir dari bilangan asli sudah tidak dapat dipertahankan. double precision dengan ±15 digit menampungnya persis.
  3. Tabel sensor: pola nyata dari aplikasi IoT — pembacaan suhu disimpan sebagai real karena presisi seperseribu derajat pun tidak dibutuhkan alat ukur, dan 4 byte per nilai menghemat setengah penyimpanan dibanding double precision. Kolom nilai_sebenarnya kembali memperlihatkan wajah asli angka-angka itu di balik tampilan manisnya.

Nama Alias #

Floating point punya banyak nama karena warisan berbagai standar. PostgreSQL menerimanya semua, dan seluruh alias mengarah ke tipe yang sama:

AliasTipe sebenarnya
float4real
realreal
float8double precision
double precisiondouble precision

Satu kasus khusus: float tanpa nomor mengikuti standar SQL dan menjadi double precision — kecuali saat dipakai dalam CAST(x AS float(n)), yang memilih real untuk n sampai 24 dan double precision untuk n di atasnya. Kita periksa langsung lewat fungsi pg_typeof:

sql alias-tipe.sql
SELECT pg_typeof(0.5::real) AS tipe_real,
       pg_typeof(0.5::float4) AS tipe_float4,
       pg_typeof(0.5::float) AS tipe_float,
       pg_typeof(0.5::double precision) AS tipe_double;

SELECT 0.5::float4 = 0.5::real AS float4_sama_real,
       0.5::float = 0.5::double precision AS float_sama_double;

Output yang Dihasilkan:

text
 tipe_real | tipe_float4 |    tipe_float    |   tipe_double    
-----------+-------------+------------------+------------------
 real      | real        | double precision | double precision
(1 row)

 float4_sama_real | float_sama_double 
------------------+-------------------
 t                | t
(1 row)

Fungsi pg_typeof melaporkan tipe setiap ekspresi: float4 dan real dijawab real, sedangkan float dan double precision dijawab double precision. Perbandingan kesetaraan di query kedua konsisten dengan laporan itu — nilai-nilai yang bertipe sama dibandingkan tanpa konversi tambahan.

Infinity dan NaN #

Berbeda dengan numeric dan integer, floating point mengenal tiga nilai khusus dari IEEE 754: tak terhingga positif, tak terhingga negatif, dan NaN (not a number) — hasil operasi yang tidak terdefinisi seperti pembagian nol demi nol:

SELECT 'Infinity'::float8 AS tak_terhingga,
       '-Infinity'::float8 AS minus_tak_terhingga,
       'NaN'::float8 AS bukan_angka;

SELECT 'NaN'::float8 = 'NaN'::float8 AS nan_sama_nan,
       'NaN'::float8 > 1000000::float8 AS nan_paling_besar;

Output yang Dihasilkan:

text
 tak_terhingga | minus_tak_terhingga | bukan_angka 
---------------+---------------------+-------------
      Infinity |           -Infinity |         NaN
(1 row)

 nan_sama_nan | nan_paling_besar 
--------------+------------------
 t            | t
(1 row)

Perilaku NaN di PostgreSQL mengejutkan mereka yang hafal standar IEEE: menurut IEEE, NaN tidak pernah sama dengan NaN, tetapi perbandingan pertama di atas menghasilkan t. PostgreSQL sengaja memperlakukan NaN sebagai nilai terbesar dari semua angka — perbandingan NaN > 1000000 pun menghasilkan t — supaya ORDER BY dan index selalu punya urutan total. Nilai khusus ini berguna sebagai penanda pembacaan sensor yang gagal, tetapi jangan biarkan NaN diam-diam ikut terjumlah dalam laporan.

Kapan Floating Point Aman Dipakai #

Setelah melihat semua jebakannya, posisi float menjadi jelas:

  • Aman: pengukuran fisik (suhu, jarak, berat) yang memang sudah punya galat instrumen; komputasi ilmiah dan statistik yang menerima pembulatan; grafik dan game yang hanya butuh tampilan mulus.
  • Tidak aman: uang dan pembukuan (pakai numeric), dan nilai yang harus bandingkan dengan = secara persis — dua float yang tampak sama bisa berbeda di digit ke-16.

Dengan aturan itu, float menjadi alat yang jujur: cepat, ringkas, dan dapat diprediksi selama kita tahu bahwa isinya pendekatan. Topik tipe data berikutnya berpindah dari angka ke teks — di sana PostgreSQL menawarkan tiga pilihan (char, varchar, text) yang konon berbeda kecepatannya, tapi ternyata menyimpan rahasia yang mengejutkan.

Referensi #

Berkas Sumber

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

alias-tipe.sql

Unduh berkas
sql
SELECT pg_typeof(0.5::real) AS tipe_real,
       pg_typeof(0.5::float4) AS tipe_float4,
       pg_typeof(0.5::float) AS tipe_float,
       pg_typeof(0.5::double precision) AS tipe_double;

SELECT 0.5::float4 = 0.5::real AS float4_sama_real,
       0.5::float = 0.5::double precision AS float_sama_double;

bukti-binari.sql

Unduh berkas
sql
SELECT 0.1::float8 + 0.2::float8 AS jumlah;

SELECT 0.1::float8 + 0.2::float8 - 0.3::float8 AS selisih;

SELECT 0.1::float8 + 0.2::float8 = 0.3::float8 AS sama_dengan_03;

nilai-khusus.sql

Unduh berkas
sql
SELECT 'Infinity'::float8 AS tak_terhingga,
       '-Infinity'::float8 AS minus_tak_terhingga,
       'NaN'::float8 AS bukan_angka;

SELECT 'NaN'::float8 = 'NaN'::float8 AS nan_sama_nan,
       'NaN'::float8 > 1000000::float8 AS nan_paling_besar;

real-vs-double.sql

Unduh berkas
sql
SELECT 36.6::real AS suhu_real,
       36.6::real::float8 AS nilai_yang_disimpan;

SELECT 123456789::real AS real_hanya_tujuh_digit,
       123456789::float8 AS double_presisi;

CREATE TABLE sensor (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    suhu  real NOT NULL
);

INSERT INTO sensor (id, nama, suhu) VALUES
    (1, 'Ruang Server', 36.6),
    (2, 'Gudang', 24.1);

SELECT id, nama, suhu,
       suhu::float8 AS nilai_sebenarnya
FROM sensor
ORDER BY id;