I deres ytre fremtoning vekker ingenting mistanke. Tvert imot, de virker til og med godt og lenge kjente for deg. Men dette gjelder kun sĂ„ lenge du ikke sjekker dem. Det er her de viser sin listige natur, og oppfĂžrer seg ganske annerledes enn du forventet. Og noen ganger gjĂžr de noe som rett og slett fĂ„r hĂ„ret til Ă„ reise seg â for eksempel, de mister betrodde hemmelige opplysninger. NĂ„r du konfronterer dem ansikt til ansikt, hevder de at de ikke kjenner hverandre, selv om de i skjul jobber under samme tak. Det er pĂ„ hĂžy tid Ă„ fĂ„ dem til Ă„ stĂ„ til ansvar. La oss ogsĂ„ bli kjent med disse mistenkelige typene.
Datatyper i PostgreSQL, til tross for sin logikk, kan av og til by pÄ merkelige overraskelser. I denne artikkelen vil vi prÞve Ä klargjÞre noen av deres sÊrheter, forstÄ Ärsakene til deres merkelige oppfÞrsel og se hvordan man unngÄr problemer i den daglige praksis. For Ä vÊre Êrlig, skrev jeg denne artikkelen delvis som en slags hÄndbok for meg selv, en hÄndbok som jeg lett kan referere til i kontroversielle saker. Derfor vil den bli oppdatert etter hvert som jeg oppdager nye overraskelser fra de mistenkelige typene. La oss sette i gang, umettelige databasedetektiver!
Mappe nummer én. real/double precision/numeric/money
Det kan virke som talltyper er de minst problematiske med tanke pĂ„ overraskelser i atferden. Men slik er det ikke. Derfor begynner vi med dem. SĂ„âŠ
De har glemt hvordan man teller
SELECT 0.1::real = 0.1
?column?
boolean
---------
fHva er greia? Jo, PostgreSQL konverterer den utypede konstanten 0.1 til typen double precision og prÞver Ä sammenligne den med 0.1 av typen real. Og dette er helt forskjellige verdier! Grunnen ligger i representasjonen av flyttall i maskinens minne. Siden 0.1 ikke kan representeres som en endelig binÊrbrÞk (dette vil bli 0.0(0011) i binÊr form), vil tall med ulik presisjon vÊre forskjellige, derav resultatet om at de ikke er like. Generelt sett, dette er et tema for en annen artikkel, sÄ jeg vil ikke gÄ nÊrmere inn pÄ det her.
Hvor kommer feilen fra?
SELECT double precision(1)
ERROR: syntax error at or near "("
LINE 1: SELECT double precision(1)
^
********** Feil **********
ERROR: syntax error at or near "("
SQL-status: 42601
Symbol: 24Mange vet at PostgreSQL tillater funksjonell typekonvertering. Det vil si at du kan skrive ikke bare 1::int, men ogsÄ int(1), og det vil vÊre likestilt. Men ikke for typer hvis navn bestÄr av flere ord! SÄ, hvis du Þnsker Ä konvertere et numerisk verd til double precision i funksjonell form, bruk aliaset for denne typen float8, altsÄ SELECT float8(1).
Hva er stĂžrre enn uendelig?
SELECT 'Infinity'::double precision < 'NaN'::double precision
?column?
boolean
---------
tSlik er det! Det viser seg at det finnes noe som er stĂžrre enn uendelig, og det er NaN! Samtidig ser PostgreSQL-dokumentasjonen oss i Ăžynene og hevder at NaN uten tvil er stĂžrre enn noe annet tall, og dermed stĂžrre enn uendelig. Det motsatte gjelder for -NaN. Hei, matematikere! Men husk at alt dette fungerer i konteksten av reelle tall.
Runding av Ăžyne
SELECT round('2.5'::double precision)
, round('2.5'::numeric)
round | round
double precision | numeric
-----------------+---------
2 | 3En annen uventet hilsen fra databasen. Og igjen mÄ vi huske at det er ulike rundingsregler for double precision og numeric. For numeric er det vanlig runding, der 0,5 rundes oppover, mens for double precision skjer 0,5-runding mot nÊrmeste partall.
Penger er noe spesielt
SELECT '10'::money::float8
ERROR: kan ikke kaste typen money til double precision
LINE 1: SELECT '10'::money::float8
^
********** Feil **********
ERROR: kan ikke kaste typen money til double precision
SQL-status: 42846
Symbol: 19IfÞlge PostgreSQL er penger ikke et reelt tall. IfÞlge noen individer er det ogsÄ slik. Vi mÄ huske at konvertering fra typen money kun er mulig til typen numeric, akkurat som det kun er mulig Ä konvertere fra type numeric til type money. Men med det kan man leke som man vil. Men det vil ikke vÊre de samme pengene.
Smallint og generering av sekvenser
SELECT *
FROM generate_series(1::smallint, 5::smallint, 1::smallint)
ERROR: funksjonen generate_series(smallint, smallint, smallint) er ikke unik
LINE 2: FROM generate_series(1::smallint, 5::smallint, 1::smallint...
^
HINT: Kunne ikke velge en best kandidatfunksjon. Du mÄ muligens legge til eksplisitte typekonverteringer.
********** Feil **********
ERROR: funksjonen generate_series(smallint, smallint, smallint) er ikke unik
SQL-status: 42725
Tips: Kunne ikke velge en best kandidatfunksjon. Du mÄ muligens legge til eksplisitte typekonverteringer.
Symbol: 18PostgreSQL er ikke av den snÄle typen. Hvilke sekvenser basert pÄ smallint? int, minst! SÄ nÄr man prÞver Ä utfÞre den ovennevnte forespÞrselen, forsÞker databasen Ä konvertere smallint til en annen heltalltype, og ser at det kan vÊre flere slike konverteringer. Hvilken konvertering skal velges? Det kan den ikke avgjÞre, og derfor krasjer den med en feil.
Dossier nummer to. «char»/char/varchar/text
Det er ogsÄ en del merkelige ting med tegntypene. La oss ogsÄ bli kjent med dem.
Hva er dette for noe fusk?
SELECT 'PETYA'::"char"
, 'PETYA'::"char"::bytea
, 'PETYA'::char
, 'PETYA'::char::bytea
char | bytea | bpchar | bytea
"char" | bytea | character(1) | bytea
-------+-------+--------------+--------
âš | xd0 | Đ | xd09fHva er denne typen «char», hva er denne klovnen? Vi trenger ikke slike... Fordi den later som en vanlig char, selv om den er i anfĂžrselstegn. Den skiller seg fra en vanlig char, som ikke er i anfĂžrselstegn, ved at den bare viser fĂžrste byte av den strengrepresentasjonen, mens en normal char viser fĂžrste tegn. I vĂ„rt tilfelle er fĂžrste tegn â bokstaven Đ, som i unicode-representasjon tar 2 bytes, noe som bekrefter konverteringen av resultatet til type bytea. Tja, typen «char» tar bare den fĂžrste byte av denne unicode-representasjonen. SĂ„ hva skal vi med denne typen? PostgreSQL-dokumentasjonen sier at dette er en spesiell type, brukt for spesielle behov. SĂ„ den vil neppe vĂŠre nĂždvendig for oss. Men se den i Ăžynene og ikke gjĂžr feil nĂ„r du mĂžter den med sin spesielle oppfĂžrsel.
UnĂždvendige mellomrom. Ute av syne, ute av sinnet
SELECT 'abc '::char(6)::bytea
, 'abc '::char(6)::varchar(6)::bytea
, 'abc '::varchar(6)::bytea
bytea | bytea | bytea
bytea | bytea | bytea
---------------+----------+----------------
x616263202020 | x616263 | x616263202020Ta en kikk pÄ det gitte eksempelet. Jeg har med vilje gjort alle resultatene til type bytea for Ä gjÞre det tydelig hva som finnes. Hvor er de avsluttende mellomrommene etter konvertering til type varchar(6)? Dokumentasjonen slÄr fast: «NÄr verdien character konverteres til en annen tegn type, blir tilleggsmellomrom fjernet». Denne aversjonen mÄ huskes. Og merk at hvis en strengkonstant i anfÞrselstegn umiddelbart konverteres til type varchar(6), beholdes de avsluttende mellomrommene. Slike vidunder.
Dossier nummer tre. json/jsonb
JSON er en egen struktur som lever sitt eget liv. Derfor skiller dens enheter seg litt fra enhetene i PostgreSQL. Her er eksempler.
Johnson & Johnson. Kjenner forskjellen
SELECT 'null'::jsonb IS NULL
?column?
boolean
---------
fPoenget er at JSON har sin egen null-verdi, som ikke er lik NULL i PostgreSQL. Samtidig kan selve JSON-objektet godt ha en NULL-verdi, sÄ uttrykket SELECT null::jsonb IS NULL (merk fravÊret av enkle anfÞrselstegn) vil nÄ returnere true.
Ăn bokstav endrer alt
SELECT '{"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]}
---
SELECT '{"1": [1, 2, 3], "2": [4, 5, 6], "1": [7, 8, 9]}'::jsonb
jsonb
jsonb
--------------------------------
{"1": [7, 8, 9], "2": [4, 5, 6]}Poenget er at json og jsonb er helt forskjellige strukturer. I json lagres objektet som det er, mens i jsonb lagres det i en analysert indeksert struktur. Det er derfor verdien av objektet ved nĂžkkelen 1 ble endret fra [1, 2, 3] til [7, 8, 9], som kom inn i strukturen til slutt med samme nĂžkkel.
Av ansiktet kan man ikke drikke vann
SELECT '{"reading": 1.230e-5}'::jsonb
, '{"reading": 1.230e-5}'::json
jsonb | json
jsonb | json
------------------------+----------------------
{"reading": 0.00001230} | {"reading": 1.230e-5}PostgreSQL i sin implementasjon av JSONB endrer formateringen av flyttall og bringer dem til klassisk form. For JSON-typen skjer ikke dette. Litt merkelig, men det er dens rett.
Dossier nummer fire. dato/tid/timestamp
Det er ogsÄ noen merkelige ting med datatyper/dato/tid. La oss se pÄ dem. Jeg mÄ umiddelbart si at noen av egenskapene blir forstÄelige hvis man forstÄr hvordan tidssoner fungerer. Men dette er ogsÄ et tema for en egen artikkel.
Min, din skjĂžnner ikke
SELECT '08-Jan-99'::date
ERROR: date/tid feltverdi utenfor rekkevidde: "08-Jan-99"
LINE 1: SELECT '08-Jan-99'::date
^
HINT: Kanskje du trenger en annen "datestyle"-innstilling.
********** Feil **********
ERROR: date/tid feltverdi utenfor rekkevidde: "08-Jan-99"
SQL-tilstand: 22008
Hint: Kanskje du trenger en annen "datestyle"-innstilling.
Symbol: 8Det kan virke uklart? Men databasen forstÄr ikke hva vi har satt fÞrst - Är eller dag? Og bestemmer at det er 99. januar 2008, noe som forvirrer den. Generelt bÞr du vÊre veldig forsiktig nÄr du sender datoer i tekstformat for Ä sjekke hvor nÞyaktig databasen gjenkjente dem (spesielt ved Ä analysere datestyle-parameteren med SHOW datestyle), siden uklarheter pÄ dette omrÄdet kan bli veldig dyre.
Hvor kom du fra?
VELG '04:05 Europa/Moskva'::time
FEIL: ugyldig inndata-syntaks for type time: "04:05 Europa/Moskva"
LINE 1: VELG '04:05 Europa/Moskva'::time
^
********** Feil **********
FEIL: ugyldig inndata-syntaks for type time: "04:05 Europa/Moskva"
SQL-status: 22007
Tegn: 8Hvorfor kan ikke databasen forstĂ„ det angitte tidspunktet? Fordi det for tidssonen er oppgitt fullstendig navn, ikke forkortelse, som kun gir mening i konteksten av en dato, ettersom det tar hensyn til historikken av tidssoneendringer, og det fungerer ikke uten en dato. Og selve formuleringen av tidsstrengen reiser spĂžrsmĂ„l â hva mente programmereren egentlig? Derfor er det logisk nĂ„r man ser nĂŠrmere pĂ„ det.
Hva er problemet?
Tenk deg en situasjon. Du har et felt i tabellen med typen timestamptz. Du vil indeksere det. Men du innser at det ikke alltid er riktig Ä bygge en indeks basert pÄ dette feltet pÄ grunn av dets hÞye selektivitet (nÊrmest alle verdier av denne typen vil vÊre unike). Derfor bestemmer du deg for Ä redusere indeksens selektivitet ved Ä konvertere denne typen til en dato. Og sÄ fÄr du en overraskelse:
LAG INDEKS "iIdent-DateLastUpdate"
PĂ
offentlig."Ident" BRUKER btree
(("DTLastUpdate"::date));
FEIL: funksjoner i indeksuttrykket mÄ merkes IMMUTABLE
********** Feil **********
FEIL: funksjoner i indeksuttrykket mÄ merkes IMMUTABLE
SQL-status: 42P17Hva er problemet? Det er at nÄr man konverterer typen timestamptz til typen date, brukes verdien av systemparameteren TimeZone, som gjÞr konverteringsfunksjonen avhengig av en konfigurerbar parameter, altsÄ foranderlig (volatile). Slike funksjoner er ikke tillatt i indeksen. I dette tilfellet mÄ man eksplisitt angi hvilken tidssone konverteringen skjer i.
NÄr now helt klart ikke er now
Vi er vant til at now() returnerer dagens dato/tid med hensyn til tidssonen. Men se pÄ de fÞlgende forespÞrslene:
START TRANSAKSJON;
VELG now();
nÄ
tidsstempel med tidssone
-----------------------------
2019-11-26 13:13:04.271419+03
...
VELG now();
nÄ
tidsstempel med tidssone
-----------------------------
2019-11-26 13:13:04.271419+03
...
VELG now();
nÄ
tidsstempel med tidssone
-----------------------------
2019-11-26 13:13:04.271419+03
FORPLIKT;Dato/tid returneres likt uansett hvor mye tid som har gÄtt siden forrige forespÞrsel! Hva er grunnen? Det er fordi now() ikke er den nÄvÊrende tiden, men tiden for starten av den nÄvÊrende transaksjonen. Derfor endres den ikke innenfor rammen av transaksjonen. Enhver forespÞrsel som kjÞres utenfor rammen av transaksjonen, pakkes utrykkelig i en transaksjon, sÄ vi merker ikke at tiden som returneres av en enkel SELECT now(); faktisk ikke er den nÄvÊrende tiden... Hvis du vil ha en Êrlig nÄvÊrende tid, mÄ du bruke funksjonen clock_timestamp().
Dossier nummer fem. bit
Merkelig en smule
SELECT '111'::bit(4)
bit
bit(4)
------
1110Fra hvilken side skal biter legges til i tilfelle utvidelse av typen? Det virker som om det er til venstre. Men databasen har en annen mening om dette. VÊr forsiktig: ved uoverensstemmelse i antall biter ved typekonvertering, vil du fÄ noe helt annet enn Þnsket. Dette gjelder bÄde for Ä legge til biter til hÞyre og for Ä kutte biter. OgsÄ til hÞyre...
Dossier nummer seks. Arrayer
Selv NULL fikk ikke utslag
SELECT ARRAY[1, 2] || NULL
?kolonne?
integer[]
---------
{1,2}Som normale mennesker oppvokst pÄ SQL, forventer vi at resultatet av dette uttrykket vil vÊre NULL. Men ikke sÄ raskt. Det returneres et array. Hvorfor? Fordi i dette tilfellet konverterer databasen NULL til et heltallsarray og kaller utrykkelig funksjonen array_cat. Men det forblir fortsatt uklart hvorfor denne «arraykatten» ikke nuller ut arrayet. Denne atferden mÄ ogsÄ rettes opp.
La oss oppsummere. Det er mange merkelige ting. De fleste av dem er selvfÞlgelig ikke sÄ kritiske at man mÄ snakke om Äpenbart uegnet atferd. Andre forklares med brukervennlighet eller hvor ofte de brukes i ulike situasjoner. Men samtidig finnes det mange overraskelser. Derfor er det nÞdvendig Ä vite om dem. Hvis du finner noe annet rart eller uvanlig i oppfÞrselen til noen typer, skriv kommentarer, sÄ skal jeg gjerne oppdatere eksisterende dossier.
Kilde: habr.com
