Në pamjen e tyre, nuk ka asgjë që ngjall dyshime. Më tepër, ato madje të duken të njohura dhe të njohura prej kohësh. Por kjo është vetëm deri në momentin që ti i kontrollon. Këtu, ato do të zbulojnë natyrën e tyre të rrezikshme, duke funksionuar krejt ndryshe nga ç'ke pritur. Disa herë, hedhin diçka që të çon në frikë, si p.sh., humbin të dhëna me besim. Kur i sfidon ballë për ballë, ato deklarojnë se nuk e njohin njëra-tjetrën, ndonëse në hije punojnë me intensitet nën të njëjtën mbulesë. Ka ardhur koha t'i zbulojmë njëherë e mirë. Le të merremi me këta individë të dyshimtë.
Kategorizimi i të dhënave në PostgreSQL, pavarësisht logjikës së vet, në të vërtetë ofron nganjëherë surpriza të çuditshme. Në këtë artikull, do të përpiqemi të sqarojmë disa nga kapriciot e tyre, të kuptojmë arsyen e sjelljes së tyre të çuditshme dhe të kuptojmë se si të shmangim probleme në praktikën e përditshme. Të vërtetën, e kam hartuar këtë artikull gjithashtu si një lloj udhëzuesi për veten time, një udhëzues që mund të kthehem lehtësisht në rast të shqetësimeve. Për këtë arsye, do të plotësohet me kalimin e kohës me zbulimet e reja nga këta individë të dyshimtë. Prandaj, le të nisim, o kërkues të palodhur të databazave!
Dosja numër një. real/double precision/numeric/money
Duket se llojet numerike janë më pak problematike nga këndvështrimi i surprizave në sjellje. Por ndodhin të papritura. Prandaj, le të fillojmë me ta.
Kemi harruar të numërojmë
SELECT 0.1::real = 0.1
?column?
boolean
---------
fÇfarë ndodh? Problemi është se PostgreSQL e kthen konstantën e paetiketuar 0.1 në tipin double precision dhe përpiqet ta krahasojë atë me 0.1 tip real. Këto janë vlera të ndryshme! Problemi është në përfaqësimin e numrave me pikë të lëvizshme në memorien e makinës. Sepse 0.1 nuk mund të përfaqësohet si një fraksion binar i përfunduar (do të jetë 0.0(0011) në format binar), numrat me saktësi të ndryshme do të ndryshojnë, dhe kështu rezultati është se ata nuk janë të barabartë. Në të vërtetë, kjo është një temë për një artikull tjetër, nuk do të shkruaj më shumë këtu.
Nga vjen gabimi?
SELECT double precision(1)
ERROR: syntax error at or near "("
LINE 1: SELECT double precision(1)
^
********** Gabim **********
ERROR: syntax error at or near "("
SQL-shteti: 42601
Simboli: 24Shumë e dinë se PostgreSQL lejon regjistrimin funksional të prodhimeve të tipeve. Kështu, mund të shkruash jo vetëm 1::int, por edhe int(1), që është ekuivalente. Por jo për llojet, emri i të cilëve përbëhet nga disa fjalë! Prandaj, nëse do të duash të kthehesh në tipin double precision në mënyrë funksionale, përdor aliase e këtij tipi float8, pra SELECT float8(1).
Çfarë është më e madhe se pafundësia?
SELECT 'Infinity'::double precision < 'NaN'::double precision
?column?
boolean
---------
tKështu që! Duket se ka diçka që është më e madhe se pafundësia, dhe kjo është NaN! Ndërkohë, dokumentacioni i PostgreSQL na shikon me sy të sinqertë dhe dëshmon se NaN është më i madh se çdo numër tjetër, dhe kështu, se pafundësia. E njëjta gjë vlen dhe për -NaN. Përshëndetje, dashamirë të analizës matematikore! Por duhet mbajtur mend se e gjithë kjo zbatohet në kontekstin e numrave me pikë të lëvizshme.
Rrethimi i syve
SELECT round('2.5'::double precision)
, round('2.5'::numeric)
round | round
double precision | numeric
-----------------+---------
2 | 3Një tjetër përshëndetje e papritur nga baza. Dhe sërish duhet të mbahet mend se rrethimet për llojet double precision dhe numeric veprojnë ndryshe. Për numeric - është zakonor, kur 0,5 rrethoshet lart, ndërsa për double precision - rrethimi i 0,5 ndodh drejt numrit më të afërt çift.
Para janë diçka speciale
SELECT '10'::money::float8
ERROR: cannot cast type money to double precision
LINE 1: SELECT '10'::money::float8
^
********** Gabim **********
ERROR: cannot cast type money to double precision
SQL-shteti: 42846
Simboli: 19Sipas PostgreSQL, paratë nuk janë numra me pikë të lëvizshme. Disa individë mendojnë njësoj. Ne duhet të mbajmë mend se kthimi i tipit money është i mundur vetëm ndaj tipit numeric, ashtu si dhe tipit money mund t'i kthejmë vetëm tipit numeric. Por tani mund të luajmë me të, si të duam. Por ato nuk do të jenë më paratë e vërteta.
Smallint dhe gjenerimi i sekuencave
SELECT *
FROM generate_series(1::smallint, 5::smallint, 1::smallint)
ERROR: function generate_series(smallint, smallint, smallint) is not unique
LINE 2: FROM generate_series(1::smallint, 5::smallint, 1::smallint...
^
HINT: Could not choose a best candidate function. You might need to add explicit type casts.
********** Gabim **********
ERROR: function generate_series(smallint, smallint, smallint) is not unique
SQL-shteti: 42725
Këshillë: Could not choose a best candidate function. You might need to add explicit type casts.
Simboli: 18PostgreSQL nuk është në humor për të bërë kompakte. Cilat janë ato sekuenca mbi bazën e smallint? int, as më pak! Prandaj, kur përpiqesh të ekzekutosh këtë pyetje, baza përpiqet të kthejë smallint në ndonjë tip tjetër të plotë dhe sheh se ka shumë mundësi të tilla. Cila kthim duhet të zgjidhë? Kjo nuk mund ta vendosë, dhe për këtë shkak bie me gabimin.
Dosja numri dy. «char»/char/varchar/text
Ka disa çuditëri dhe te llojet simbolike. Le të njohim edhe ato.
Çfarë janë këto truqe?
SELECT 'PETYA'::"char"
, 'PETYA'::"char"::bytea
, 'PETYA'::char
, 'PETYA'::char::bytea
char | bytea | bpchar | bytea
"char" | bytea | karakter(1) | bytea
-------+-------+--------------+--------
╨ | xd0 | П | xd09fÇfarë është ky tip «char», çfarë është ky karagjoz? Ne nuk na duhen të tillë… Sepse ai bëhet se është një char i zakonshëm, pavarësisht se është në thonjëza. Ndërsa ai dallon nga char i zakonshëm, që është pa thonjëza, sepse tregon vetëm bajtin e parë të përfaqësimit të vargut, ndërsa char normal tregon karakterin e parë. Në rastin tonë, karakteri i parë është letra П, e cila në përfaqësimin unicode zë 2 bajta, siç tregon konvertimi i rezultatit në tipin bytea. Ndërsa tipi «char» merr vetëm bajtin e parë të këtij përfaqësimi unicode. Atëherë çfarë është ky tip i nevojshëm? Dokumentacioni i PostgreSQL thotë se ky është një tip special, i përdorur për nevoja të veçanta. Prandaj, ndoshta nuk do ta kemi nevojë. Por shikoni në sytë e tij dhe mos gaboni kur të takoni atë me sjelljen e tij të veçantë.
Hapësira të tepërta. Jashtë syve, jashtë zemrës
SELECT 'abc '::char(6)::bytea
, 'abc '::char(6)::varchar(6)::bytea
, 'abc '::varchar(6)::bytea
bytea | bytea | bytea
bytea | bytea | bytea
---------------+----------+----------------
x616263202020 | x616263 | x616263202020Shikoni shembullin e dhënë. E kam sjellë të gjithë rezultatet në tipin bytea, për të qenë vizualisht të qartë se çfarë ndodhet atje. Ku janë hapësirat pas rezultateve që u konvertuan në tipin varchar(6)? Dokumentacioni thotë me shkurt: «Duke konvertuar vlerën karakter në një tip tjetër simbolik, hapësirat shtesë hiqen». Duhet ta mbani mend këtë. Dhe vini re se, nëse një konstante vargu në thonjëza konvertohet menjëherë në tipin varchar(6), hapësirat përfundimtare mbahen. Këto janë çudira.
Dosja numri tre. json/jsonb
JSON është një strukturë e veçantë, që jeton jetën e saj. Pra, entitetet e saj dhe entitetet e PostgreSQL dallohet pak. Ja disa shembuj.
Johnson dhe Johnson. Ndihuni të ndryshimin
SELECT 'null'::jsonb IS NULL
?column?
boolean
---------
fE gjithë çështja është se JSON ka entitetin e tij null, i cili nuk është ekuivalent me NULL në PostgreSQL. Në të njëjtën kohë, vetë objekti JSON mund të ketë vlerën NULL, prandaj shprehja SELECT null::jsonb IS NULL (vini re mungesën e thonjëzave) kësaj here do të kthejë true.
Një shkronjë ndryshon gjithçka
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]}E gjithë çështja është se json dhe jsonb janë struktura krejtësisht të ndryshme. Në json, objekti ruhet siç është, ndërsa në jsonb ruhet si një strukturë e përmbledhur dhe e indekseve. Pikërisht për këtë arsye, në rastin e dytë, vlera e objektit sipas çelësit 1 u zëvendësua nga [1, 2, 3] në [7, 8, 9], e cila erdhi në strukturë në fund me të njëjtin çelës.
Nuk pihet ujë nga fytyra
SELECT '{"reading": 1.230e-5}'::jsonb
, '{"reading": 1.230e-5}'::json
jsonb | json
jsonb | json
------------------------+----------------------
{"reading": 0.00001230} | {"reading": 1.230e-5}PostgreSQL në implementimin JSONB ndryshon formatimin e numrave realë, duke i sjellë në formën klasike. Për tipin JSON kjo nuk ndodh. Pak e çuditshme, por e drejta e tij.
Dosja numri katër. date/time/timestamp
Me tipet e datës/vijës kohore gjithashtu ka disa çuditëri. Le të shohim ato. Menjëherë dua të theksoj se disa nga veçoritë e sjelljes bëhen të qartë, nëse kupton mirë thelbin e punës me zona kohore. Por kjo gjithashtu është një temë për një artikull të veçantë.
Unë nuk kuptoj
SELECT '08-Jan-99'::date
ERROR: vlera e fushës date/time jashtë rangut: "08-Jan-99"
LINE 1: SELECT '08-Jan-99'::date
^
HINT: Ndoshta keni nevojë për një cilësim tjetër "datestyle".
********** Gabim **********
ERROR: vlera e fushës date/time jashtë rangut: "08-Jan-99"
SQL-stati: 22008
Sugjerim: Ndoshta keni nevojë për një cilësim tjetër "datestyle".
Simbol: 8Duket se nuk ka asgjë të paqartë këtu? Por megjithatë, baza nuk kupton se çfarë kemi vendosur në vend të parë — vitin apo ditën? Dhe vendos që kjo është janari 99 i vitit 2008, gjë që i çon në tronditje. Në përgjithësi, në rastin e kalimit të datave në format tekstual, duhet të kontrolloni me shumë kujdes se sa saktë i ka njohur baza ato (veçanërisht, analizoni parametrin datestyle me komandën SHOW datestyle), pasi paqartësitë në këtë çështje mund të kushtojnë shumë shtrenjtë.
Nga ku doli ky?
SELECT '04:05 Europe/Moscow'::time
ERROR: sintaksë e pavlefshme për tipin time: "04:05 Europe/Moscow"
LINE 1: SELECT '04:05 Europe/Moscow'::time
^
********** Gabim **********
ERROR: sintaksë e pavlefshme për tipin time: "04:05 Europe/Moscow"
SQL-stati: 22007
Simbol: 8Pse baza nuk mund ta kuptojë qartë kohën e përcaktuar? Sepse për zonën e kohës është cituar jo abbreviatura, por emri i plotë, i cili ka kuptim vetëm në kontekstin e datës, pasi merr parasysh historinë e ndryshimeve të zonave të kohës, dhe ajo nuk funksionon pa datën. Po ashtu, formulimi i vetë vargjeve të kohës ngre pyetje — çfarë kishte në të vërtetë parasysh programuesi? Prandaj, gjithçka është logjike, nëse merret në dorë.
Çfarë ka ndodhur me të?
Imagjinoni një situatë. Në tabelën tuaj ka një fushë me tipin timestamptz. Do të donit ta indeksoni. Por kuptoni se ndërtimi i indeksit mbi këtë fushë nuk është gjithmonë i arsyeshëm për shkak të selektivitetit të tij të lartë (gati të gjitha vlerat e këtij tipi do të jenë unike). Prandaj, vendosni të ulni selektivitetin e indeksit duke e kthyer këtë tip në datë. Dhe merrni një surprizë:
CREATE INDEX "iIdent-DateLastUpdate"
ON public."Ident" USING btree
(("DTLastUpdate"::date));
ERROR: functions in index expression must be marked IMMUTABLE
********** Gabim **********
ERROR: functions in index expression must be marked IMMUTABLE
SQL-shtet: 42P17Cila është çështja? Faktikisht, për të kthyer tipin timestamptz në tipin date përdoret vlera e parametrave sistemikë TimeZone, që e bën funksionin e kthimit të tipit të varur nga parametra të konfiguruar, pra të ndryshueshëm (volatile). Këto funksione në indeks nuk lejohen. Në këtë rast, duhet të tregoni qartë se në cilën zonë kohe bëhet kthimi i tipit.
Kur ora tani nuk është fare ora tani
Jemi mësuar që now() kthen datën/aftësinë aktuale duke marrë parasysh zonën e kohës. Por shikoni kërkesat e mëposhtme:
START TRANSACTION;
SELECT now();
tani
timestamp me zonë kohore
-----------------------------
2019-11-26 13:13:04.271419+03
...
SELECT now();
tani
timestamp me zonë kohore
-----------------------------
2019-11-26 13:13:04.271419+03
...
SELECT now();
tani
timestamp me zonë kohore
-----------------------------
2019-11-26 13:13:04.271419+03
COMMIT;Data/koha kthehet e njëjtë pavarësisht se sa kohë ka kaluar që nga kërkesa e mëparshme! Cila është çështja? E vërteta është se now() — nuk është koha aktuale, por koha e fillimit të transaksionit aktual. Prandaj, brenda kuadrit të transaksionit ajo nuk ndryshon. Çdo kërkesë që ekzekutohet jashtë kuadrit të transaksionit, e kthen atë në një transaksion implicit, kështu që ne nuk e vëmë re se koha që kthehet nga kërkesa e thjeshtë SELECT now(); në të vërtetë nuk është aktuale... Nëse dëshironi të merrni kohën reale, duhet të përdorni funksionin clock_timestamp().
Dossier numri pesë. bit
Paksa e çuditshme
SELECT '111'::bit(4)
bit
bit(4)
------
1110Nga ana e cilës duhet të shtoni bitë në rastin e zgjerimit të tipit? Duke dukur, duket se nga e majta. Por vetëm se baza ka një mendim tjetër për këtë. Kini kujdes: në rast të mos përputhjes së numrit të bitëve gjatë kthimit të tipit do të merrni diçka krejt ndryshe nga çfarë deshit. Kjo i përket si shtimit të bitëve nga e djathta, ashtu edhe shkurtimit të tyre. Edhe kjo nga e djathta...
Dossier numri gjashtë. Arrays
As NULL nuk doli
SELECT ARRAY[1, 2] || NULL
?column?
integer[]
---------
{1,2}Si njerëz normalë, të rritur në SQL, ne presim që rezultati i këtij shprehjeje të jetë NULL. Por jo kështu. Kthehet një array. Pse? Sepse në këtë rast baza e kthen NULL në një array të numrave të plotë dhe thirr një funksion array_cat në mënyrë implicite. Por ende mbetet e paqartë pse ky 'macja array' nuk e nulon arrayn. Një sjellje e tillë gjithashtu duhet thjesht të mbahet mend.
Të përmbledhim. Ka shumë çuditëri. Shumica e tyre, sigurisht, nuk janë aq kritike sa të flasësh për një sjellje të papërshtatshme. Ndërsa të tjera shpjegohen me lehtësinë e përdorimit ose me frekuencën e aplikimit të tyre në situata të caktuara. Por gjithashtu ka shumë surpriza. Prandaj, duhet t'i dini ato. Nëse gjeni diçka tjetër të çuditshme ose të pazakontë në sjelljen e disa llojeve, shkruani në komente, me kënaqësi do të shtoj informacione në doracakët e tanishëm.
Burimi: habr.com
