Uvirkelige funktioner af rigtige typer, eller vær forsigtig med REAL

Efter udgivelsen Artikel om funktionerne ved at skrive i PostgreSQL, handlede den allerførste kommentar om vanskelighederne ved at arbejde med rigtige tal. Jeg besluttede at tage et hurtigt kig på koden for de tilgængelige SQL-forespørgsler for at se, hvor ofte de bruger den RIGTIGE type. Det viser sig, at det bruges ret ofte, og udviklere forstår ikke altid farerne bag det. Og det på trods af, at der findes en del gode artikler på internettet og på Habré om funktionerne ved at gemme reelle tal i computerens hukommelse og om at arbejde med dem. Derfor vil jeg i denne artikel forsøge at anvende sådanne funktioner til PostgreSQL, og vil forsøge at tage et hurtigt kig på problemerne forbundet med dem, så det bliver nemmere for SQL-forespørgselsudviklere at undgå dem.

PostgreSQL-dokumentationen siger kortfattet: "Håndtering af sådanne fejl og deres udbredelse under beregning er genstand for en hel gren af ​​matematik og datalogi, og er ikke dækket her" (mens man klogt henviser læseren til IEEE 754-standarden). Hvilken slags fejl menes her? Lad os diskutere dem i rækkefølge, og det vil snart blive klart, hvorfor jeg tog pennen op igen.

Lad os for eksempel tage en simpel anmodning:

********* ЗАПРОС *********
SELECT 0.1::REAL;
**************************
float4
--------
    0.1
(1 строка)

Som et resultat vil vi ikke se noget særligt - vi får den forventede 0.1. Men lad os nu sammenligne det med 0.1:

********* ЗАПРОС *********
SELECT 0.1::REAL = 0.1;
**************************
?column?
----------
f
(1 строка)

Ikke lige! Hvilke mirakler! Men videre, mere. Nogen vil sige, jeg ved, at REAL opfører sig dårligt med brøker, så jeg vil indtaste hele tal der, og alt vil helt sikkert være fint med dem. Ok, lad os caste tallet 123 til REAL:

********* ЗАПРОС *********
SELECT 123456789::REAL::INT;
**************************
   int4   
-----------
123456792
(1 строка)

Og det blev til 3 mere! Det er det, databasen har endelig glemt, hvordan man tæller! Eller misforstår vi noget? Lad os finde ud af det.

Lad os først huske materialet. Som du ved, kan ethvert decimaltal udvides til ti potenser. Så tallet 123.456 vil være lig med 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​​​6*10-3. Men computeren opererer med tal i binær form, derfor skal de repræsenteres i form af ekspansion i to potenser. Derfor er tallet 5.625 i binært repræsenteret som 101.101 og vil være lig med 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. Og hvis positive potenser af to altid giver hele decimaltal (1, 2, 4, 8, 16 osv.), så er alt mere kompliceret med negative (0.5, 0.25, 0.125, 0,0625 osv.). Problemet er det Ikke alle decimaler kan repræsenteres som en endelig binær brøk. Således optræder vores berygtede 0.1 i form af en binær brøk som den periodiske værdi 0.0(0011). Som følge heraf vil den endelige værdi af dette tal i computerhukommelsen variere afhængigt af bitdybden.

Nu er det tid til at huske, hvordan reelle tal er gemt i computerens hukommelse. Generelt set består et reelt tal af tre hoveddele - fortegn, mantisse og eksponent. Tegnet kan være enten plus eller minus, så der er afsat en bit til det. Men antallet af bits af mantissen og eksponenten bestemmes af den reelle type. Så for den REAL type er længden af ​​mantissen 23 bit (en bit lig med 1 tilføjes implicit til begyndelsen af ​​mantissen, og resultatet er 24), og eksponenten er 8 bit. Det samlede antal er 32 bits eller 4 bytes. Og for typen DOBBELT PRECISION vil længden af ​​mantissen være 52 bit, og eksponenten vil være 11 bit, for i alt 64 bit, eller 8 bytes. PostgreSQL understøtter ikke højere præcision for flydende kommatal.

Lad os pakke vores decimaltal 0.1 ind i både REAL og DOBBELT PRÆCISION. Da eksponentens fortegn og værdi er de samme, vil vi fokusere på mantissen (jeg udelader bevidst de ikke-oplagte træk ved lagring af værdierne af eksponenten og nul reelle værdier, da de komplicerer forståelsen og distraherer fra essensen af problemet, hvis du er interesseret, se IEEE 754-standarden). Hvad får vi? I den øverste linje vil jeg give "mantissen" for den RIGTIGE type (under hensyntagen til afrundingen af ​​den sidste bit med 1 til det nærmeste repræsentative tal, ellers vil det være 0.099999...), og i den nederste linje - for typen DOBBELT PRÆCISION:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Det er åbenbart to helt forskellige tal! Derfor, når man sammenligner, vil det første tal være polstret med nuller og vil derfor være større end det andet (under hensyntagen til afrunding - det, der er markeret med fed). Dette forklarer tvetydigheden fra vores eksempler. I det andet eksempel støbes det eksplicit specificerede tal 0.1 til typen DOBBELT PRECISION og sammenlignes derefter med et tal af typen REAL. Begge er reduceret til samme type, og vi har præcis, hvad vi ser ovenfor. Lad os ændre forespørgslen, så alt falder på plads:

********* ЗАПРОС *********
SELECT 0.1::REAL > 0.1::DOUBLE PRECISION;
**************************
?column?
----------
t
(1 строка)

Og faktisk, ved at udføre en dobbelt reduktion af tallet 0.1 til RIGTIG og DOBBELT PRÆCISION, får vi svaret på gåden:

********* ЗАПРОС *********
SELECT 0.1::REAL::DOUBLE PRECISION;
**************************

      float8       
-------------------
0.100000001490116
(1 строка)

Dette forklarer også det tredje eksempel ovenfor. Tallet 123 er enkelt det er umuligt at passe mantissen ind i 24 bits (23 eksplicitte + 1 underforstået). Det maksimale heltal, der kan passe ind i 24 bit, er 224-1 = 16. Derfor afrundes vores tal 777 til nærmeste repræsentative 215. Ved at ændre typen til DOBBELT PRÆCISION ser vi ikke længere dette scenario:

********* ЗАПРОС *********
SELECT 123456789::DOUBLE PRECISION::INT;
**************************
   int4   
-----------
123456789
(1 строка)

Det er alt. Det viser sig, at der ikke er nogen mirakler. Men alt det beskrevne er en god grund til at tænke over, hvor meget du egentlig har brug for den RIGTIGE type. Måske den største fordel ved dets brug er hastigheden af ​​beregninger med et kendt tab af nøjagtighed. Men ville dette være et universelt scenarie, der ville retfærdiggøre så hyppig brug af denne type? Tænk ikke.

Kilde: www.habr.com

Tilføj en kommentar