MisstÀnkta typer

I deras yttre vĂ€cker ingenting misstankar. Mer Ă€n sĂ„, de verkar till och med bekanta för dig. Men det Ă€r bara sĂ„ lĂ€nge du inte kontrollerar dem. Just hĂ€r visar de sin illvilliga natur och fungerar inte alls som du förvĂ€ntat dig. Ibland gör de nĂ„got sĂ„ skrĂ€mmande att hĂ„ret reser sig pĂ„ huvudet – till exempel, de förlorar de hemliga uppgifter som anförtrotts dem. NĂ€r du konfronterar dem hĂ€vdar de att de inte kĂ€nner varandra, fastĂ€n de flitigt arbetar i skuggan under samma tak. Det Ă€r dags att avslöja dem. LĂ„t oss ocksĂ„ reda ut dessa misstĂ€nkta typer.

Datatyper i PostgreSQL, trots sin logik, ger ibland vÀldigt konstiga överraskningar. I denna artikel ska vi försöka klargöra nÄgra av deras egenheter, förstÄ orsaken till deras underliga beteenden och lÀra oss hur man undviker problem i vardaglig praktik. För att vara Àrlig har jag skrivit denna artikel Àven som en slags referens för mig sjÀlv, en referens som man enkelt kan konsultera vid tveksamma fall. DÀrför kommer den att uppdateras i takt med att nya överraskningar frÄn misstÀnkta typer upptÀckts. SÄ, lÄt oss börja, o orubbliga databassökare!

Dossier nummer ett. real/double precision/numeric/money

Det skulle kunna verka som att numeriska typer Ă€r de minst problematiska nĂ€r det gĂ€ller överraskningar i beteendet. Men sĂ„ Ă€r det inte. SĂ„ vi börjar med dem. SÄ 

Vi har glömt hur man rÀknar

SELECT 0.1::real = 0.1

?column?
boolean
---------
f

Vad Ă€r problemet? Jo, PostgreSQL konverterar den otypade konstanten 0.1 till typen double precision och försöker jĂ€mföra den med 0.1 av typen real. Detta Ă€r helt olika vĂ€rden! Problemet ligger i representationen av flyttal i maskinens minne. Eftersom 0.1 inte kan representeras som ett Ă€ndligt binĂ€rt brĂ„k (det blir 0.0(0011) i binĂ€r form), kommer tal med olika precision att skilja sig Ă„t, dĂ€rav resultatet att de inte Ă€r lika. Över lag Ă€r detta Ă€mnet för en separat artikel, sĂ„ jag ska inte gĂ„ in pĂ„ det mer hĂ€r.

VarifrÄn kommer felet?

SELECT double precision(1)

ERROR: syntax error at or near "("
LINE 1: SELECT double precision(1)
                               ^
********** Fel **********
ERROR: syntax error at or near "("
SQL-status: 42601
Symbol: 24

MÄnga vet att PostgreSQL tillÄter funktionell typkonvertering. Det betyder att man kan skriva inte bara 1::int, utan ocksÄ int(1), vilket Àr likvÀrdigt. Men inte för typer vars namn bestÄr av flera ord! SÄ om du vill konvertera ett numeriskt vÀrde till typen double precision i funktionell form, anvÀnd aliaset för denna typ float8, det vill sÀga SELECT float8(1).

Vad Àr större Àn oÀndlighet?

SELECT 'Infinity'::double precision < 'NaN'::double precision

?column?
boolean
---------
t

Titta dÀr! Det visar sig att det finns nÄgot som Àr större Àn oÀndlighet, och det Àr NaN! Samtidigt ser PostgreSQL-dokumentationen pÄ oss med Àrliga ögon och hÀvdar att NaN Àr av naturliga skÀl större Àn vilket annat tal som helst, och dÀrmed oÀndligheten. Det omvÀnda gÀller ocksÄ för -NaN. Hej, kalkylatorÀlskare! Men kom ihÄg att allt detta gÀller inom ramen för reella tal.

Ombalansering av ögon

SELECT round('2.5'::double precision)
     , round('2.5'::numeric)

      round      |  round
double precision | numeric
-----------------+---------
2                | 3

Ännu en ovĂ€ntad hĂ€lsning frĂ„n databasen. Och Ă„terigen mĂ„ste man komma ihĂ„g att olika ombalanseringar gĂ€ller för typerna double precision och numeric. För numeric — den vanliga, dĂ€r 0,5 rundas uppĂ„t, och för double precision — sker rundningen av 0,5 mot nĂ€rmaste jĂ€mna heltal.

Pengar Àr nÄgot speciellt

SELECT '10'::money::float8

ERROR:  kan inte konvertera typ money till double precision
LINE 1: SELECT '10'::money::float8
                          ^
********** Fel **********
ERROR: kan inte konvertera typ money till double precision
SQL-status: 42846
Symbol: 19

Enligt PostgreSQL Àr pengar inte ett reellt tal. Enligt vissa individer Àr det ocksÄ sÄ. Men vi mÄste komma ihÄg att typkonvertering av money endast Àr möjlig till typen numeric, precis som money endast kan konverteras frÄn typ numeric. Men med den kan man leka som man vill. Men det kommer inte att vara de samma pengarna.

Smallint och sekvensgenerering

SELECT *
  FROM generate_series(1::smallint, 5::smallint, 1::smallint)

ERROR:  funktionen generate_series(smallint, smallint, smallint) Àr inte unik
LINE 2:   FROM generate_series(1::smallint, 5::smallint, 1::smallint...
               ^
HINT:  Kunde inte vÀlja den bÀsta kandidatfunktionen. Du kan behöva lÀgga till explicita typkonverteringar.
********** Fel **********
ERROR: funktionen generate_series(smallint, smallint, smallint) Àr inte unik
SQL-status: 42725
Tips: Kunde inte vÀlja den bÀsta kandidatfunktionen. Du kan behöva lÀgga till explicita typkonverteringar.
Symbol: 18

PostgreSQL Àr inte för den som vill snÄla. Vilka sekvenser baserade pÄ smallint skulle det vara? int, minst! DÀrför, nÀr försöker att köra den nÀmnda frÄgan, försöker databasen att konvertera smallint till nÄgon annan heltalstyp och ser att sÄdana konverteringar kan finnas flera av. Vilken konvertering ska vÀljas? Det kan den inte avgöra, och dÀrför kraschar den med ett felmeddelande.

Dossier nummer tvÄ. "char"/char/varchar/text

Det finns en del konstigheter Àven bland teckentyperna. LÄt oss ocksÄ bekanta oss med dem.

Vad Àr det hÀr för trick?

SELECT 'PETYA'::"char"
     , 'PETYA'::"char"::bytea
     , 'PETYA'::char
     , 'PETYA'::char::bytea

 char  | bytea |    bpchar    | bytea
"char" | bytea | character(1) | bytea
-------+-------+--------------+--------
 ╹     | xd0  | П            | xd09f

Vad Ă€r den hĂ€r typen "char", vad Ă€r det för clown? Vi behöver inte sĂ„dana
 För att den lĂ„tsas vara en vanlig char, bara för att den Ă€r inom citat. Den skiljer sig frĂ„n en vanlig char utan citat, genom att den bara returnerar den första byten av den strĂ€ngrepresentationen, medan den normala char returnerar den första tecknet. I vĂ„rt fall Ă€r det första tecknet bokstaven П, som i Unicode-representation tar 2 byte, vilket indikeras av konverteringen av resultatet till typen bytea. Typen "char" tar bara den första byten av denna Unicode-representation. SĂ„ varför Ă€r denna typ nödvĂ€ndig? PostgreSQL-dokumentationen sĂ€ger att detta Ă€r en speciell typ, som anvĂ€nds för speciella behov. SĂ„ det kommer förmodligen inte att behövas. Men titta den i ögonen och gör inte misstag, nĂ€r du möter den med sitt speciella beteende.

Överskottsutrymmen. Borta frĂ„n ögonen, borta frĂ„n hjĂ€rtat.

SELECT 'abc   '::char(6)::bytea
     , 'abc   '::char(6)::varchar(6)::bytea
     , 'abc   '::varchar(6)::bytea

     bytea     |   bytea  |     bytea
     bytea     |   bytea  |     bytea
---------------+----------+----------------
x616263202020 | x616263 | x616263202020

Titta pÄ exemplet ovan. Jag har medvetet konverterat alla resultat till typ bytea för att tydligt kunna se vad som finns dÀr. Var finns de avslutande utrymmena efter konvertering till typ varchar(6)? Dokumentationen pÄstÄr lakoniskt: "Vid konvertering av vÀrdet character till en annan teckentyp slÀngs utfyllnadsutrymmen bort." Denna ogillande bör man komma ihÄg. Och observera att om en strÀngkonstant inom citat konverteras direkt till typ varchar(6), sÄ bevaras de avslutande utrymmena. SÄdana mirakel.

Dossier nummer tre. json/jsonb

JSON Àr en separat struktur som har sitt eget liv. DÀrför skiljer sig dess entiteter nÄgot frÄn PostgreSQL:s entiteter. HÀr Àr exempel.

Johnson & Johnson. KĂ€nn skillnaden

VÄLJ 'null'::jsonb ÄR NULL

?kolumn?
booleskt
---------
f

Det handlar om att JSON har sin egen null-enhet, som inte Ă€r en liknelse till NULL i PostgreSQL. Samtidigt kan sjĂ€lva JSON-objektet mycket vĂ€l ha vĂ€rdet NULL, sĂ„ uttrycket SELECT null::jsonb ÄR NULL (notera avsaknaden av enkla citat) returnerar denna gĂ„ng true.

En bokstav Àndrar allt

VÄLJ '{"1": [1, 2, 3], "2": [4, 5, 6], "1": [7, 8, 9]}'::json

                     json
                     json
------------------------------------------------
{"1": [1, 2, 3], "2": [4, 5, 6], "1": [7, 8, 9]}

---

VÄLJ '{"1": [1, 2, 3], "2": [4, 5, 6], "1": [7, 8, 9]}'::jsonb

             jsonb
             jsonb
--------------------------------
{"1": [7, 8, 9], "2": [4, 5, 6]}

Det handlar om att json och jsonb Àr helt olika strukturer. I json lagras objektet som det Àr, medan i jsonb lagras det som en parsead och indexerad struktur. Det Àr dÀrför vÀrdet för objektet under nyckel 1 byttes frÄn [1, 2, 3] till [7, 8, 9], vilket kom in i strukturen sist med samma nyckel.

Man kan inte dricka vatten frÄn ansiktet

VÄLJ '{"reading": 1.230e-5}'::jsonb
     , '{"reading": 1.230e-5}'::json

          jsonb         |         json
          jsonb         |         json
------------------------+----------------------
{"reading": 0.00001230} | {"reading": 1.230e-5}

PostgreSQL i sin implementation av JSONB Àndrar formateringen av flyttal till sitt klassiska utseende. För JSON-typen sker detta inte. Lite konstigt, men det Àr dess rÀtt.

Dossier nummer fyra. datum/tid/timestamp

Det finns ocksÄ vissa underligheter med datatyper för datum/tid. LÄt oss titta nÀrmare pÄ dem. Jag vill genast pÄpeka att vissa av beteendets egenskaper blir förstÄeliga om man har god insikt i hur man arbetar med tidszoner. Men det Àr ocksÄ ett Àmne för en separat artikel.

Min din inte förstÄ

VÄLJ '08-Jan-99'::datum

FEL: datum/tid fÀltvÀrde utanför intervall: "08-Jan-99"
RAD 1: VÄLJ '08-Jan-99'::datum
               ^
HINT: Kanske behöver du en annan "datastil" instÀllning.
********** Fel **********
FEL: datum/tid fÀltvÀrde utanför intervall: "08-Jan-99"
SQL-tillstÄnd: 22008
Hint: Kanske behöver du en annan "datastil" instÀllning.
Symbol: 8

Det verkar som att det inte finns nĂ„got som Ă€r oklart hĂ€r? Men Ă€ndĂ„ förstĂ„r inte databasen vad vi stĂ€llde först — Ă„ret eller dagen? Och den beslutar att det Ă€r den 99 januari 2008, vilket fĂ„r den att förlora förstĂ„ndet. AllmĂ€nt sett, vid överlĂ€mning av datum i textformat, mĂ„ste man vara mycket noga med hur korrekt databasen kĂ€nner igen dem (specifikt analysera parametern datastil med kommandot SHOW datastil), eftersom tvetydigheter i den frĂ„gan kan kosta mycket.

VarifrÄn kom du?

SELECT '04:05 Europe/Moscow'::time

ERROR: ogiltig inmatningssyntax för tids typ: "04:05 Europe/Moscow"
LINE 1: SELECT '04:05 Europe/Moscow'::time
               ^
********** Fel **********
ERROR: ogiltig inmatningssyntax för tids typ: "04:05 Europe/Moscow"
SQL-status: 22007
Symbol: 8

Varför kan databasen inte förstĂ„ den uttryckligen angivna tiden? För att tidszonen anges med ett fullstĂ€ndigt namn istĂ€llet för en förkortning, vilket bara har mening i samband med ett datum, eftersom det tar hĂ€nsyn till historien om förĂ€ndringar av tidszoner, och den fungerar inte utan ett datum. Och sjĂ€lva formuleringen av tidsstrĂ€ngen vĂ€cker frĂ„gor — vad menade programören egentligen? SĂ„ allt detta Ă€r logiskt, om man grĂ€ver lite djupare.

Vad Àr fel med honom?

FörestÀll dig en situation. Du har ett fÀlt i tabellen med typen timestamptz. Du vill indexera det. Men du inser att det inte alltid Àr motiverat att bygga ett index för detta fÀlt med hÀnsyn till dess höga selektivitet (nÀstan alla vÀrden av denna typ kommer att vara unika). DÀrför bestÀmmer du dig för att minska selektiviteten av indexet genom att omvandla den hÀr typen till datum. Och fÄr en överraskning:

CREATE INDEX "iIdent-DateLastUpdate"
  ON public."Ident" USING btree
  (("DTLastUpdate"::date));

ERROR: funktioner i index uttryck mÄste vara mÀrkta IMMUTABLE
********** Fel **********
ERROR: funktioner i index uttryck mÄste vara mÀrkta IMMUTABLE
SQL-status: 42P17

Vad Àr problemet? Det Àr sÄ att för att omvandla typen timestamptz till typ date anvÀnds vÀrdet av systemparametern TimeZone, vilket gör omvandlingsfunktionen beroende av en konfigurerbar parameter, dvs. förÀndringsbar (volatile). SÄdana funktioner Àr inte tillÄtna i ett index. I det hÀr fallet mÄste man uttryckligen ange i vilken tidszon omvandlingen av typen görs.

NÀr now inte Àr really now

Vi Àr vana vid att now() returnerar aktuellt datum/tid med hÀnsyn till tidszonen. Men titta pÄ följande förfrÄgningar:

START TRANSACTION;
SELECT now();

            now
  timestamp med tidszon
-----------------------------
2019-11-26 13:13:04.271419+03

...

SELECT now();

            now
  timestamp med tidszon
-----------------------------
2019-11-26 13:13:04.271419+03

...

SELECT now();

            now
  timestamp med tidszon
-----------------------------
2019-11-26 13:13:04.271419+03

COMMIT;

Datum/tid returneras alltid samma oavsett hur lÀnge som har gÄtt sedan senaste begÀran! Vad Àr problemet? Jo, now() Àr inte den aktuella tiden, utan tiden för början av den aktuella transaktionen. DÀrför Àndras den inte inom ramen för transaktionen. Alla begÀrningar som utförs utanför transaktionen kapslas in en transaktion implicit, sÄ vi mÀrker inte att tiden som ges av en enkel begÀran SELECT now(); faktiskt inte Àr aktuell... Om du vill fÄ den verkliga aktuella tiden, mÄste du anvÀnda funktionen clock_timestamp().

Dossier nummer fem. bit

Lite konstigt

SELECT '111'::bit(4)

 bit
bit(4)
------
1110

FrÄn vilken sida ska man lÀgga till bitar vid typförlÀngning? Det verkar som om det ska vara frÄn vÀnster. Men databasen har en annan Äsikt om det. Var försiktig: vid typkonvertering med olika bitlÀngder fÄr du nÄgot helt annat Àn vad du ville ha. Detta gÀller sÄvÀl tillÀgg av bitar till höger som beskÀrning av bitar. OcksÄ till höger...

Dossier nummer sex. Arrayer

Inte ens NULL slog till

SELECT ARRAY[1, 2] || NULL

?column?
integer[]
---------
{1,2}

Som normala mÀnniskor uppfostrade pÄ SQL förvÀntar vi oss att resultatet av detta uttryck ska vara NULL. Men icke! En array returneras. Varför? För i det hÀr fallet konverterar databasen NULL till en heltalsarray och anropar implicit funktionen array_cat. Men det förblir oklart varför denna "array-katt" inte nollstÀller arrayen. Detta beteende mÄste man ocksÄ bara memorera.

Sammanfattningsvis. Det finns mÄnga konstigheter. De flesta av dem Àr naturligtvis inte sÄ kritiska att det handlar om uppenbart ologiskt beteende. Andra förklaras av anvÀndarvÀnlighet eller hur ofta de tillÀmpas i vissa situationer. Men samtidigt finns det mÄnga överraskningar. DÀrför mÄste man kÀnna till dem. Om du hittar nÄgot annat konstigt eller ovanligt i beteendet hos nÄgon typ, skriv gÀrna i kommentarerna sÄ kompletterar jag med nöje de befintliga dossierna.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster