Pärast avaldamist PostgreSQL tüpiseerimise omaduste kohta, esimene kommentaar käsitles keerukusi ujuvpunktide numbritega töötamisel. Otsustasin kiiresti üle vaadata saadaval olevad SQL-päringud, et näha, kui sageli kasutatakse tüüpi REAL. Osutus, et seda kasutatakse üsna sageli, ja arendajad ei mõista alati sellega seotud ohte. See on üllatav, arvestades, et Internetis ja Habras on piisavalt palju häid artikleid ujuvpunktide numbrite arvutisimulatsioonide ja nende töötlemise omaduste kohta. Seetõttu püüan selles artiklis rakendada neid omadusi PostgreSQL-is ja selgitada «kätega» sellega seotud ebameeldivusi, et SQL-päringute arendajatel oleks kergem neid vältida.
PostgreSQLi dokumentatsioon sisaldab lühikest lauset: „Sarnaste vigade haldamine ja nende levitamine arvutuste käigus on matemaatika ja arvutiteaduse eraldi valdkonna uurimistöö objekt, ja siinkohal ei käsitleta seda“ (samuti suunates mõistlikult lugejat IEEE 754 standardi poole). Millised vead siin silmas peetakse? Arutame neid järjestikku ja varsti on selge, miks ma taas kirjutamise juurde tagasi pöördusin.
Võtame näiteks lihtsa päringu:
********* PÄRING *********
SELECT 0.1::REAL;
**************************
float4
--------
0.1
(1 rida)
Tulemus ei näita midagi erilist – saame oodatud 0.1. Aga nüüd võrdleme seda 0.1-ga:
********* PÄRING *********
SELECT 0.1::REAL = 0.1;
**************************
?column?
----------
f
(1 rida)
Ei ole võrdsed! Millised imed! Kuid see pole veel kõik. Keegi ütleb, et ta teab, et REAL käitub halvasti murdudega, no ma panen sinna täisarve, nendega on kindlasti kõik korras. Okei, proovime arvu 123 456 789 tüübiks REAL muuta:
********* PÄRING *********
SELECT 123456789::REAL::INT;
**************************
int4
-----------
123456792
(1 rida)
Ja see tuli kolmandiku võrra suurem! Kõik, andmebaas on täielikult unustanud, kuidas arvestada! Või me ei saa midagi aru? Kutsume seda lõplikult selgitama.
Korraks meenutame teooriaid. Nagu teada, võib iga kümnendlik number jagada kümne astmete järgi. Näiteks number 123.456 on võrdne 1*10² + 2*10¹ + 3*10⁰ + 4*10⁻¹ + 5*10⁻² + 6*10⁻³. Kuid arvuti töötab arvudega binaarses vormis, seega tuleb neid esitada kahekümne astmete lahtimonteerimisel. Seega number 5.625 binaarses vormis esindatakse kui 101.101 ja see on võrdne 1*2² + 0*2¹ + 1*2⁰ + 1*2⁻¹ + 0*2⁻² + 1*2⁻³. Ja kui positiivsed kahekümnendad astmed annavad alati täisarvulisi kümnendlikke numbreid (1, 2, 4, 8, 16 jne), siis negatiivsest on asi keerulisem (0.5, 0.25, 0.125, 0.0625 jne). Probleem on selles, et kõiki kümnendlikke murde ei saa esitada lõplike binaarsete murdena.. Näiteks meie kuulus 0.1 binaarses murdes näib perioodilise väärtusena 0.0(0011). Seega on selle numbri lõplik väärtus masina mälus sõltuv suuruse järgi.
Nüüd on õige aeg meenutada, kuidas salvestatakse reaalarve arvutite mälu. Üldiselt koosneb reaalnumber kolmest põhiosast – märgist, mantissist ja eksponentsist. Märk võib olla kas pluss või miinus, seega kulutab see ühe biidi. Mantissi ja eksponentsi bitid määrab reaalne tüüp. Näiteks tüübi REAL puhul on mantissi pikkus 23 bitti (üks bit, mis on võrdne 1, lisatakse mantissi ette automaatselt, seega kokku 24), ja eksponentsi pikkus on 8 bitti. Seega kokku 32 bitti ehk 4 baiti. Tüübi DOUBLE PRECISION puhul on mantissi pikkus juba 52 bitti, ja eksponentsi pikkus 11 bitti, mis kokku teeb 64 bitti ehk 8 baiti. PostgreSQL ei toeta suuremat täpsust ujuva komaga numbrite puhul.
Pakume meie 0.1 numbrit kümnendikuna välja mõlemas tüübis – REAL ja DOUBLE PRECISION. Kuna meie eksponenti märk ja väärtus langevad kokku, keskendume mantissile (alati teadlikult jätan välja vähem ilmseid eksponendi väärtuste ja nullide salvestamise eripära, kuna need raskendavad mõistmist ja segavad probleemist arusaamist; kui see huvitab, vaadake IEEE 754 standardit). Mida me saame? Ülemises reas annan "mantissi" tüübi REAL jaoks (arvestades viimase bitti ümardamist 1-le lähima esindatava numbri, vastasel juhul on see 0.099999...) ja alumises – tüübi DOUBLE PRECISION jaoks:
0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001
Koheselt on ilmne, et need on kaks täiesti erinevat numbrit! Seetõttu ei toimu esimese numbri võrdlemisel täitmist nullidega ja seega on see suurem kui teine (arvestades ümardamist – rasvases kirjas ühte). Sellega selgitatakse meie näidete mitmeti tõlgendamist. Teises näites antud number 0.1 muudetakse tüübiks DOUBLE PRECISION, mille järel võrreldakse seda tüübi REAL numbriga. Mõlemad muudetakse ühte tüüpi ja meil on täpselt see, mida näeme ülal. Muudame päringut, et kõik paika loksuks:
********* PÄRING *********
SELECT 0.1::REAL > 0.1::DOUBLE PRECISION;
**************************
?veerg?
----------
t
(1 rida)
Ja tõepoolest, kui me viime kahekordse konversiooni numbrist 0.1 REAL-iks ja DOUBLE PRECISION-iks, saame vastuse mõistatusse:
********* PÄRING *********
SELECT 0.1::REAL::DOUBLE PRECISION;
**************************
float8
-------------------
0.100000001490116
(1 rida)
Sama kehtib ka kolmanda näite kohta, mis on eelpool toodud. Number 123 456 789 on lihtsalt võimatu mahutada 24 bitti mantississe (23 eksplicit + 1 vaikimisi). Kõrgeim terve number, mille saab 24 bitis mahutada, on 224-1 = 16 777 215. Seetõttu ümardatakse meie number 123 456 789 lähima esindatava numbrini 123 456 792. Kui me muudame tüübi DOUBLE PRECISION-iks, ei näe me enam sellist stsenaariumi:
********* PÄRING *********
SELECT 123456789::DOUBLE PRECISION::INT;
**************************
int4
-----------
123456789
(1 rida)
Nii et kõik. Selgub, et mingeid imesid ei ole. Kuid kõik, mis on kirjeldatud, on hea põhjus mõelda, kui väga on teil tõesti vaja tüüpi REAL. Tõenäoliselt on selle kasutamise peamine eelis – arvutuste kiirus, kus on eelnevalt teada kaotus täpsuses. Kuid kas see on universaalne stsenaarium, mis õigustab selle tüübi nii sagedast kasutamist? Ei usu.
Allikas: habr.com
