Spanner menyediakan jenis NUMERIC
yang dapat menyimpan angka presisi desimal
secara akurat.
Semantik jenis NUMERIC
di Spanner bervariasi antara dua dialek SQL-nya (GoogleSQL dan PostgreSQL), terutama seputar batas skala dan presisi:
NUMERIC
dalam dialek PostgreSQL adalah jenis numerik presisi desimal arbitrer (skala atau presisi dapat berupa angka apa pun dalam rentang yang didukung) sehingga merupakan pilihan ideal untuk menyimpan data numerik presisi arbitrer.NUMERIC
di GoogleSQL adalah jenis numerik presisi tetap (presisi=38 dan skala=9) dan tidak dapat digunakan untuk menyimpan data numerik presisi arbitrer. Jika Anda perlu menyimpan angka presisi arbitrer dalam database dialek GoogleSQL, sebaiknya simpan sebagai string.
Presisi jenis numerik Spanner
Presisi adalah jumlah digit dalam angka. Skala adalah jumlah digit di sebelah kanan koma desimal dalam angka. Misalnya, angka 123.456 memiliki presisi 6 dan skala 3. Spanner memiliki tiga jenis numerik:
- Jenis bilangan bulat bertanda 64-bit yang disebut
INT64
dalam dialek GoogleSQL danINT8
dalam dialek PostgreSQL. - Jenis floating point presisi biner IEEE 64-bit (ganda) yang disebut
FLOAT64
dalam dialek GoogleSQL danFLOAT8
dalam dialek PostgreSQL. - Jenis
NUMERIC
presisi desimal.
Mari kita lihat setiap jenisnya dalam hal presisi dan skala.
INT64
/ INT8
mewakili nilai numerik yang tidak memiliki komponen pecahan. Jenis data ini memberikan presisi 18 digit, dengan skala nol.
FLOAT64
/ FLOAT8
hanya dapat mewakili perkiraan nilai numerik desimal dengan komponen pecahan dan memberikan
15 hingga 17 digit signifikan (jumlah digit dalam angka dengan semua nol di akhir
dihapus) presisi desimal. Kita mengatakan bahwa jenis ini mewakili nilai numerik desimal perkiraan karena representasi biner floating point 64-bit IEEE yang digunakan Spanner tidak dapat mewakili pecahan desimal (basis 10) secara akurat (hanya dapat mewakili pecahan basis 2 secara akurat).
Hilangnya presisi ini menyebabkan error pembulatan untuk beberapa pecahan desimal.
Misalnya, saat Anda menyimpan nilai desimal 0,2 menggunakan jenis data FLOAT64
/ FLOAT8
, representasi biner akan dikonversi kembali menjadi nilai desimal 0,20000000000000001 (hingga presisi 18 digit). Demikian pula, (1,4 * 165) dikonversi
kembali menjadi 230,999999999999971 dan (0,1 + 0,2) dikonversi kembali menjadi
0,30000000000000004. Inilah sebabnya float 64-bit dijelaskan hanya memiliki 15-17
digit presisi yang signifikan (hanya beberapa angka dengan lebih dari 15 digit
desimal yang dapat direpresentasikan sebagai float 64-bit tanpa pembulatan). Untuk mengetahui detail selengkapnya tentang cara penghitungan presisi floating point, lihat Format floating point presisi ganda.
Baik INT64
/ INT8
maupun FLOAT64
/ FLOAT8
tidak memiliki presisi yang ideal untuk
penghitungan keuangan, ilmiah, atau teknik, yang biasanya memerlukan presisi 30
digit atau lebih.
Jenis data NUMERIC
cocok untuk aplikasi tersebut, karena dapat
mewakili nilai numerik presisi desimal yang tepat dengan presisi lebih
dari 30 digit desimal.
Jenis data NUMERIC
GoogleSQL dapat mewakili angka dengan presisi desimal tetap 38 dan skala tetap 9. Rentang NUMERIC
GoogleSQL adalah -99999999999999999999999999999.999999999
hingga 99999999999999999999999999999.999999999.
Jenis NUMERIC
dialek PostgreSQL dapat mewakili angka dengan presisi desimal maksimum 147.455 dan skala maksimum 16.383.
Jika Anda perlu menyimpan angka yang lebih besar dari presisi dan skala
yang ditawarkan oleh NUMERIC
, bagian berikut menjelaskan beberapa solusi
yang direkomendasikan.
Rekomendasi: simpan angka presisi arbitrer sebagai string
Jika Anda perlu menyimpan angka presisi arbitrer dalam database
Spanner, dan Anda memerlukan presisi yang lebih tinggi dari yang disediakan NUMERIC
, sebaiknya
simpan nilai sebagai representasi desimalnya di kolom STRING
/ VARCHAR
. Misalnya, angka 123.4
disimpan sebagai string "123.4"
.
Dengan pendekatan ini, aplikasi Anda harus melakukan konversi lossless antara
representasi internal aplikasi dari angka dan nilai kolom
STRING
/ VARCHAR
untuk pembacaan dan penulisan database.
Sebagian besar library presisi arbitrer memiliki metode bawaan untuk melakukan
konversi lossless ini. Di Java, misalnya, Anda dapat menggunakan metode
BigDecimal.toPlainString()
dan konstruktor
BigDecimal(String)
.
Menyimpan angka sebagai string memiliki keunggulan bahwa nilai disimpan dengan presisi yang tepat (hingga batas panjang kolom STRING
/ VARCHAR
), dan nilai tersebut tetap dapat dibaca manusia.
Melakukan agregasi dan penghitungan yang tepat
Untuk melakukan agregasi dan penghitungan persis pada representasi string dari angka presisi arbitrer, aplikasi Anda harus melakukan penghitungan ini. Anda tidak dapat menggunakan fungsi agregat SQL.
Misalnya, untuk melakukan tindakan yang setara dengan SUM(value)
SQL di rentang
baris, aplikasi harus mengkueri nilai string untuk baris, lalu
mengonversi dan menjumlahkannya secara internal di aplikasi.
Melakukan perkiraan agregasi, pengurutan, dan penghitungan
Anda dapat menggunakan kueri SQL untuk melakukan penghitungan agregat perkiraan dengan
mentransmisikan nilai ke FLOAT64
/ FLOAT8
.
GoogleSQL
SELECT SUM(CAST(value AS FLOAT64)) FROM my_table
PostgreSQL
SELECT SUM(value::FLOAT8) FROM my_table
Demikian pula, Anda dapat mengurutkan menurut nilai numerik atau membatasi nilai menurut rentang dengan transmisi:
GoogleSQL
SELECT value FROM my_table ORDER BY CAST(value AS FLOAT64);
SELECT value FROM my_table WHERE CAST(value AS FLOAT64) > 100.0;
PostgreSQL
SELECT value FROM my_table ORDER BY value::FLOAT8;
SELECT value FROM my_table WHERE value::FLOAT8 > 100.0;
Penghitungan ini mendekati batas jenis data FLOAT64
/ FLOAT8
.
Alternatif
Ada cara lain untuk menyimpan angka presisi arbitrer di Spanner. Jika menyimpan angka presisi arbitrer sebagai string tidak berfungsi untuk aplikasi Anda, pertimbangkan alternatif berikut:
Menyimpan nilai bilangan bulat yang diskalakan aplikasi
Untuk menyimpan angka presisi arbitrer, Anda dapat melakukan penskalaan awal pada nilai sebelum
menulis, sehingga angka selalu disimpan sebagai bilangan bulat, dan menskalakan ulang nilai
setelah membaca. Aplikasi Anda menyimpan faktor skala tetap, dan presisinya
dibatasi hingga 18 digit yang disediakan oleh jenis data INT64
/ INT8
.
Misalnya, angka yang perlu disimpan dengan akurasi 5
angka di belakang koma. Aplikasi mengonversi nilai ke bilangan bulat dengan mengalikan
nilai tersebut dengan 100.000 (memindahkan titik desimal 5 tempat ke kanan), sehingga nilai
12.54321 disimpan sebagai 1254321
.
Dalam hal moneter, pendekatan ini seperti menyimpan nilai dolar sebagai kelipatan milidetik, mirip dengan menyimpan satuan waktu sebagai milidetik.
Aplikasi menentukan faktor penskalaan tetap. Jika mengubah faktor penskalaan, Anda harus mengonversi semua nilai yang sebelumnya diskalakan di database.
Pendekatan ini menyimpan nilai yang dapat dibaca manusia (dengan asumsi Anda mengetahui faktor penskalaan). Selain itu, Anda dapat menggunakan kueri SQL untuk melakukan penghitungan langsung pada nilai yang disimpan dalam database, selama hasilnya diskalakan dengan benar dan tidak meluap.
Menyimpan nilai bilangan bulat tanpa penskalaan dan skala dalam kolom terpisah
Anda juga dapat menyimpan angka presisi arbitrer di Spanner menggunakan dua elemen:
- Nilai bilangan bulat yang tidak diskalakan yang disimpan dalam array byte.
- Bilangan bulat yang menentukan faktor penskalaan.
Pertama, aplikasi Anda mengonversi desimal presisi arbitrer menjadi nilai bilangan bulat
tanpa penskalaan. Misalnya, aplikasi mengonversi 12.54321
menjadi 1254321
.
Skala untuk contoh ini adalah 5
.
Kemudian, aplikasi akan mengonversi nilai bilangan bulat yang tidak diskalakan menjadi array byte menggunakan representasi biner portabel standar (misalnya, big-endian two's complement).
Database kemudian menyimpan array byte (BYTES
/ BYTEA
) dan skala bilangan bulat (INT64
/ INT8
) dalam dua kolom terpisah, dan mengonversinya kembali saat dibaca.
Di Java, Anda dapat menggunakan BigDecimal
dan BigInteger
untuk melakukan penghitungan ini:
byte[] storedUnscaledBytes = bigDecimal.unscaledValue().toByteArray();
int storedScale = bigDecimal.scale();
Anda dapat membaca kembali ke BigDecimal
Java menggunakan kode berikut:
BigDecimal bigDecimal = new BigDecimal(
new BigInteger(storedUnscaledBytes),
storedScale);
Pendekatan ini menyimpan nilai dengan presisi arbitrer dan representasi portabel, tetapi nilai tersebut tidak dapat dibaca manusia dalam database, dan semua penghitungan harus dilakukan oleh aplikasi.
Menyimpan representasi internal aplikasi sebagai byte
Opsi lainnya adalah melakukan serialisasi nilai desimal presisi arbitrer ke array byte menggunakan representasi internal aplikasi, lalu menyimpannya langsung di database.
Nilai database yang disimpan tidak dapat dibaca manusia, dan aplikasi harus melakukan semua penghitungan.
Pendekatan ini memiliki masalah portabilitas. Jika Anda mencoba membaca nilai dengan bahasa pemrograman atau library yang berbeda dari yang awalnya menulisnya, nilai tersebut mungkin tidak berfungsi. Membaca kembali nilai mungkin tidak berfungsi karena library presisi arbitrer yang berbeda dapat memiliki representasi serialisasi yang berbeda untuk array byte.
Langkah selanjutnya
- Baca tentang jenis data lainnya yang tersedia untuk Spanner.
- Pelajari cara menyiapkan desain skema dan model data Spanner dengan benar.
- Pelajari cara mengoptimalkan desain skema untuk Spanner.