Nende välimuses ei tekita miski kahtlusi. Veelgi enam, nad näivad sulle tuttavad ja hea meelega tuttavad. Kuid see kestab ainult seni, kuni sa neid ei kontrolli. Siin nad näitavad oma salakavalat olemust, toimides just nii, nagu sa ei oodanud. Mõnikord teevad nad aga midagi, mis ajab juuksed püsti — näiteks kaotavad neile usaldatud salajased andmed. Kui sa nendega silmast silma kohtud, väidavad nad, et ei tunne teineteist, kuigi varjus töötavad nad usinasti sama katuse all. On aeg nad lõpuks päevavalgele tuua. Läheme siis nende kahtlaste tüüpidega lõpuks tegutsema.
Andmete tüpiseerimine PostgreSQL-is, vaatamata oma loogilisusele, toob mõnikord esile väga kummalisi üllatusi. Selles artiklis püüame selgitada mõningaid nende kapriise, välja selgitada nende kummalise käitumise põhjused ja mõista, kuidas igapäevases praktikas probleemide vältimiseks toime tulla. Tõtt-öelda koostasin selle artikli ka omamoodi käsiraamatuks endale, mille poole oleks lihtne keerulistes olukordades pöörduda. Seetõttu täiendan seda vastavalt sellele, kuidas uusi üllatusi ebamugavatest tüüpide kohta avastan. Nii et, teele siis, väsimatud andmebaasi seiklejad!
Koitus number üks. real/double precision/numeric/money
Tundub, et arvutüübid on kõige vähem probleemsed üllatuste osas. Kuid nii see ei ole. Seetõttu alustame neist. Nii et…
Oleme unustanud lugema
SELECT 0.1::real = 0.1
?column?
boolean
---------
fMis on probleem? Probleem on selles, et PostgreSQL teisendab tüübita konstandi 0.1 tüübiks double precision ja üritab seda võrrelda 0.1 tüübiga real. Need on aga täiesti erinevad väärtused! Probleem on reaalsete arvude esituses masina mälus. Kuna 0.1 ei ole võimalik esitada lõpliku binaarfraktsioonina (see oleks 0.0(0011) binaarses vormis), erinevad erineva täpsusega numbrid üksteisest, mistõttu on tulemus see, et nad ei ole võrdsed. Üldiselt on see teema, millest võiks kirjutada eraldi artiklis, nii et siin ma ei süvene sügavamalt.
Kus on viga?
SELECT double precision(1)
ERROR: süntaksiviga "("
LINE 1: SELECT double precision(1)
^
********** Viga **********
ERROR: süntaksiviga "("
SQL-olek: 42601
Sümbol: 24Paljud teavad, et PostgreSQL lubab tüübivääratõsist funktsionaalset kirjutamist. See tähendab, et võib kirjutada mitte ainult 1::int, vaid ka int(1), mis on ekvivalentne. Kuid mitte tüüpide puhul, mille nimed koosnevad mitmest sõnast! Seetõttu, kui soovite numbrilist väärtust tüübiks double precision funktsionaalses vormis tuua, kasutage selle tüübi aliast float8, st SELECT float8(1).
Mis on suurem kui lõpmatus?
SELECT 'Infinity'::double precision < 'NaN'::double precision
?veerg?
boolean
---------
tNoh, vaata, on olemas midagi, mis on suurem kui lõpmatus — ja see on NaN! Samal ajal vaatab PostgreSQLi dokumentatsioon meile otse silma ja väidab, et NaN on alltid igast muust arvust, seega ka lõpmatusest. Vastupidine kehtib ka -NaN jaoks. Tere, matemaatika huvilised! Kuid tuleb meeles pidada, et kõik see kehtib reaalnumbrite kontekstis.
Silmi mündiks
SELECT round('2.5'::double precision)
, round('2.5'::numeric)
round | round
double precision | numeric
-----------------+---------
2 | 3Veel üks ootamatu tervitus andmebaasist. Peame jällegi meeles pidama, et double precision ja numeric tüüpide puhul kehtivad erinevad ümardamised. Numeric puhul toimub tavaline ümardamine, kus 0,5 ümardatakse ülespoole, samas kui double precision puhul toimub 0,5 ümardamine lähimasse paarisarvude hulka.
Raha on midagi erilist
SELECT '10'::money::float8
ERROR: ei saa raha tüüpi tuua double precisioni
LINE 1: SELECT '10'::money::float8
^
********** Viga **********
ERROR: ei saa raha tüüpi tuua double precisioni
SQL-riik: 42846
Sümbol: 19PostgreSQL'i arvates ei ole raha reaalne number. Mõne üksiku arvates samuti. Peame siiski meeles pidama, et raha andmete tüüp on võimalik muuta vaid numbriliseks tüübiks; nii võib raha tüübi muuta ainult numbriliseks. Sellega saab mängida, nagu hing soovib. Kuid need ei ole juba need rahad.
Smallint ja järjestuste genereerimine
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.
********** Ошибка **********
ERROR: function generate_series(smallint, smallint, smallint) is not unique
SQL-состояние: 42725
Подсказка: Could not choose a best candidate function. You might need to add explicit type casts.
Символ: 18PostgreSQL ei armasta pisiasjadega tegeleda. Millised sellised järjestused smallint'i alusel? int, vähemalt! Seetõttu, kui proovite ülaltoodud päringut täita, üritab andmebaas smallint'i muuta mõneks muuks täisarvutüübi, ja näeb, et neid muundamisi võib olla mitu. Millist muundamist valida? Seda ta ei oska otsustada, ja seetõttu kukub ta vea tõttu kokku.
Kaust number kaks. «char»/char/varchar/text
Mõned kummalised omadused esinevad ka sümbolitüüpidel. Tutvume ka nendega.
Mis need trikid on?
SELECT 'PETJA'::"char"
, 'PETJA'::"char"::bytea
, 'PETJA'::char
, 'PETJA'::char::bytea
char | bytea | bpchar | bytea
"char" | bytea | character(1) | bytea
-------+-------+--------------+--------
╨ | xd0 | P | xd09fMis asi on typ „char“, kes see kloun on? Me ei vaja selliseid… Sest ta teeseldab, et on tavaline char, kuigi on jutumärkides. Ja ta erineb tavalisest char'ist, mis on ilma jutumärkideta, sellega, et tagastab ainult esimese byte stringi esituses, samas kui normaalne char tagastab esimese märgi. Meie puhul on esimene märk täht P, mis unicode-esituses võtab 2 byte, mida tõestab tulemuse konverteerimine tüübiks bytea. Ja tüüp „char“ võtab ainult selle unicode-esituse esimese byte. Milleks seda tüüpi vaja on? PostgreSQL dokumentatsioon ütleb, et see on eriline tüüp, mida kasutatakse erilistel vajadustel. Seega ei ole see tõenäoliselt meile vajalik. Kuid vaadake talle silma ja ärge eksige, kui kohtute temaga tema eriliste omadustega.
Liigsed tühikud. Silmist ära, südamest välja
VALI 'abc '::char(6)::bytea
, 'abc '::char(6)::varchar(6)::bytea
, 'abc '::varchar(6)::bytea
bytea | bytea | bytea
bytea | bytea | bytea
---------------+----------+----------------
x616263202020 | x616263 | x616263202020Vaata antud näidet. Ma tõin kõik tulemused tahtlikult tüüpi bytea, et oleks selgelt näha, mis seal on. Kus on tühikud pärast konverteerimist tüüpi varchar(6)? Dokumentatsioon väidab lakooniliselt: „Kui character tüüp muudetakse teise sümbolitüüpi, jäetakse täiendavad tühikud kõrvale.” Sellele mittesobivusele tuleb tähelepanu pöörata. Ja pane tähele, et kui stringikonstant sulgudes muudetakse kohe tüüpi varchar(6), säilivad lõpupunktid. Nii need asjad on.
Dossjee number kolm. json/jsonb
JSON on eraldiseisev struktuur, millel on oma elu. Seetõttu erinevad selle entiteedid ja PostgreSQL entiteedid pisut. Siin on näited.
Johnson & Johnson. Tunne erinevust
VALI 'null'::jsonb ON NULL
?veerg?
boolean
---------
fAsi on selles, et JSON-il on oma null-entiteet, mis ei ole PostgreSQL-is NULL-iga samaväärne. Samas võib JSON-objekt täiesti omada väärtust NULL, seega avaldist SELECT null::jsonb IS NULL (pange tähele, et üksikud jutumärgid puuduvad) tagastab seekord true.
Üks täht muudab kõike
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]}Asi on selles, et json ja jsonb on täiesti erinevad struktuurid. Jsonis hoitakse objekti sellisena, nagu see on, kuid jsonb-s hoitakse see juba lahti kirjutatud ja indekseeritud struktuurina. Just seetõttu muudeti teises juhus objekti väärtus võtme 1 all [1, 2, 3] pealt [7, 8, 9]-ks, mis tuli struktuuri viimasena sama võtme all.
Vette põske ei jooksu
SELECT '{"reading": 1.230e-5}'::jsonb
, '{"reading": 1.230e-5}'::json
jsonb | json
jsonb | json
------------------------+----------------------
{"reading": 0.00001230} | {"reading": 1.230e-5}PostgreSQLi JSONB rakenduses muudetakse reaalarvude vormindust, viies need klassikalisse vormi. JSON tüübi puhul seda ei toimu. Natuke kummaline, kuid see on tema õigus.
Fail nr neli. kuupäev/aeg/aegstemp
Kuupäeva ja ajaga on samuti mõned omapärad. Vaatame neid. Tuleb kohe öelda, et mõned käitumise eripärad saavad arusaadavaks, kui mõista ajavöönditega töötamise olemust. Kuid see on ka teema eraldi artiklile.
Mina ei saa aru sellest.
SELECT '08-Jan-99'::date
ERROR: kuupäeva/aja väli väljund väljastatud vahemikust: "08-Jan-99"
LINE 1: SELECT '08-Jan-99'::date
^
HINT: Võib-olla peate kasutama teistsugust "datestyle" seadistust.
********** Viga **********
ERROR: kuupäeva/aja väli väljund väljastatud vahemikust: "08-Jan-99"
SQL-seisund: 22008
Näpunäide: Võib-olla peate kasutama teistsugust "datestyle" seadistust.
Sümbol: 8Tundub, et siin pole midagi arusaamatut? Kuid siiski ei mõista andmebaas, mida me siin esimeseks panime — aasta või päev? Ja arvab, et see on 99. jaanuar 2008. aasta, mis lööb tal aju sassi. Üldiselt, kui edastada kuupäevi tekstivormingus, tuleb väga hoolikalt kontrollida, kui hästi andmebaas neid ära tunneb (eriti analüüsida parameetrit datestyle käsuga SHOW datestyle), kuna selles küsimuses esinevad ambivalentsid võivad maksta väga kalliks.
Kust sa selline tulnud oled?
VALI '04:05 Euroopa/Moskva'::aeg
VIGA: kehtetu sisendi süntaks tüübi aeg: "04:05 Euroopa/Moskva"
RIDA 1: VALI '04:05 Euroopa/Moskva'::aeg
^
********** Viga **********
VIGA: kehtetu sisendi süntaks tüübi aeg: "04:05 Euroopa/Moskva"
SQL-olek: 22007
Märk: 8Miks andmebaas ei saa aru selgelt määratletud ajast? Kuna ajavööndi jaoks on määratud mitte lühend, vaid täielik nimetus, mis omab tähendust ainult kuupäeva kontekstis, kuna arvestab ajavööndite ajaloo muutustega, ja see ilma kuupäevata ei toimi. Ja ka aega määratlev lause tekitab küsimusi — mida tegelikult programmeerija silmas pidas? Seega on kõik siin loogiline, kui uurida.
Mis tal on?
Kujutage ette olukorda. Teie tabelis on väljak tipus timestamptz. Soovite seda indekseerida. Kuid mõistate, et selle välja põhjal indeksi loomine pole alati mõistlik tema kõrge eristatavuse tõttu (peaaegu kõik selle tüübi väärtused on unikaalsed). Seetõttu otsustate indeksi eristatavust vähendada, muutes selle tüübi kuupäevaks. Ja saate üllatuse:
LOO INDEKS "iIdent-DateLastUpdate"
PUBLIC."Ident" KASUTADES btree
(("DTLastUpdate"::kuupäev));
VIGA: funktsioonid indeksi avaldises peavad olema märgitud IMMUTABLE
********** Viga **********
VIGA: funktsioonid indeksi avaldises peavad olema märgitud IMMUTABLE
SQL-olek: 42P17Milles on probleem? See on see, et timestamptz tüübi viimiseks date tüübiks kasutatakse süsteemi parameetrit TimeZone, mis muudab tüübiviimise sõltuvaks seadistatavast parameetrist, st muutlikuks. Sellised funktsioonid ei ole indeksis lubatud. Sellisel juhul tuleb selgelt näidata, millises ajavööndis tüübiviimist teostatakse.
Kuidas nüüd ei ole tõesti nüüd
Oleme harjunud, et now() tagastab praeguse kuupäeva/aega koos ajavööndiga. Kuid vaadake järgmisi päringuid:
START TRANSACTION;
SELECT now();
now
timestamp with time zone
-----------------------------
2019-11-26 13:13:04.271419+03
...
SELECT now();
now
timestamp with time zone
-----------------------------
2019-11-26 13:13:04.271419+03
...
SELECT now();
now
timestamp with time zone
-----------------------------
2019-11-26 13:13:04.271419+03
COMMIT;Kuupäev / kellaaeg tagastatakse alati sama, sõltumata sellest, kui kaua on möödunud eelmisest päringust! Asi on selles, et now() ei näita praegust aega, vaid käimasoleva tehingu algusaega. Seetõttu ei muutu see tehingu raames. Iga päring, mis käivitatakse tehingu väljaspool, ümbritsetakse kaudselt tehinguga, mistõttu me ei teadvusta, et SELECT now(); lihtsa päringu tagastatud aeg tegelikult ei ole praegune... Kui soovite saada tõelist praegust aega, peaksite kasutama funktsiooni clock_timestamp().
Fail number viis. bit
Veidi kummaline
SELECT '111'::bit(4)
bit
bit(4)
------
1110Kummalt poolt tuleks tüübi laiendamise korral bitte lisada? Tundub, et vasakult. Kuid andmebaasil on selles suhtes oma arvamus. Olge ettevaatlik: kui bitide arvud ei vasta tüüpide kokkuviimisel, saate täiesti midagi muud, kui soovisite. See kehtib nii bitide lisamise kohta paremalt kui ka bitide kärpimise kohta. Samuti paremalt...
Fail number kuus. Massiivid
Isegi NULL ei tulnud välja
SELECT ARRAY[1, 2] || NULL
?column?
integer[]
---------
{1,2}Nagu tavaliselt SQL-i põhjal haritud inimesed, eeldame, et selle väljendi tulemuseks on NULL. Kuid ei, siin tagastatakse massiiv. Miks? Sest antud juhul konverteerib andmebaas NULL-i täisarvuliseks massiiviks ja kutsub varjatult välja funktsiooni array_cat. Kuid jääb ebaselgeks, miks see "massiivne kass" massiivi nulliks ei tee. Sellist käitumist tuleb lihtsalt meeles pidada.
Kokkuvõttes. Imelikku on piisavalt. Enamik neist ei ole muidugi nii kriitilised, et rääkida silmatorkavast ebaadekvaatsest käitumisest. Teised aga selgitavad mugavust või nende rakendamise sagedust erinevates olukordades. Samas on üllatusi palju. Seetõttu on oluline neist teada. Kui leiate veel midagi kummalist või ebatavalist, andke kommenteerides teada, täiendaksin neid volitusi hea meelega.
Allikas: habr.com
