Kahtlased tüübid

Nende välimuses pole midagi kahtlast. Veelgi enam, need tunduvad teile isegi hästi ja pikka aega tuttavad. Kuid seda ainult seni, kuni te neid kontrollite. Siin näitavad nad oma salakavalat olemust, töötades täiesti teisiti, kui ootasite. Ja mõnikord teevad nad midagi, mis paneb su juuksed püsti tõusma – näiteks kaotavad nad neile usaldatud salajased andmed. Nendega silmitsi seistes väidavad nad, et nad ei tunne üksteist, kuigi varjus teevad nad sama kapoti all kõvasti tööd. On aeg need lõpuks puhta vette tuua. Tegeleme ka nende kahtlaste tüüpidega.

Andmete sisestamine PostgreSQL-is pakub kogu oma loogikale vaatamata mõnikord väga kummalisi üllatusi. Selles artiklis püüame selgitada mõningaid nende veidrusi, mõista nende kummalise käitumise põhjuseid ja mõista, kuidas igapäevases praktikas mitte probleeme tekkida. Tõtt-öelda koostasin selle artikli ka enda jaoks omamoodi teatmeteosena, teatmeteosena, millele saab vastuolulistel juhtudel kergesti viidata. Seetõttu täiendatakse seda, kui avastatakse uusi üllatusi kahtlastelt tüüpidelt. Niisiis, lähme, oh väsimatuid andmebaasijälgijaid!

Toimik number üks. päris/topelttäpsus/numbriline/raha

Näib, et numbritüübid on käitumise üllatuste osas kõige vähem problemaatilised. Aga kuidas see ka poleks. Nii et alustame nendega. Nii…

Unustasid, kuidas lugeda

SELECT 0.1::real = 0.1

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

Mis viga? Probleem on selles, et PostgreSQL teisendab tüpiseerimata konstandi 0.1 topelttäpsuseks ja proovib seda võrrelda reaaltüübi 0.1-ga. Ja need on täiesti erinevad tähendused! Idee seisneb reaalarvude esitamises masina mälus. Kuna 0.1 ei saa esitada lõpliku kahendmurruna (binaarselt oleks see 0.0(0011), siis on erineva bitisügavusega arvud erinevad, mistõttu need ei ole võrdsed. Üldiselt on see eraldi artikli teema, ma ei hakka siin täpsemalt kirjutama.

Kust viga tuleb?

SELECT double precision(1)

ERROR:  syntax error at or near "("
LINE 1: SELECT double precision(1)
                               ^
********** Ошибка **********
ERROR: syntax error at or near "("
SQL-состояние: 42601
Символ: 24

Paljud inimesed teavad, et PostgreSQL võimaldab tüübivalamisel funktsionaalset tähistust. See tähendab, et saate kirjutada mitte ainult 1::int, vaid ka int(1), mis on samaväärne. Kuid mitte tüüpidele, kelle nimed koosnevad mitmest sõnast! Seetõttu, kui soovite funktsionaalsel kujul numbrilise väärtuse kahekordse täpsusega tüüpi üle kanda, kasutage seda tüüpi pseudonüümi float8, st SELECT float8(1).

Mis on suurem kui lõpmatus?

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

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

Vaata, kuidas see on! Selgub, et on midagi lõpmatusest suuremat ja see on NaN! Samal ajal vaatab PostgreSQL-i dokumentatsioon meile ausate silmadega otsa ja väidab, et NaN on ilmselgelt suurem kui mis tahes muu arv ja seega ka lõpmatus. Vastupidine kehtib ka -NaN puhul. Tere, matemaatikahuvilised! Kuid me peame meeles pidama, et see kõik toimib reaalarvude kontekstis.

Silmade ümardamine

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

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

Veel üks ootamatu tervitus baasilt. Jällegi pidage meeles, et kahekordse täpsusega ja numbritüüpidel on erinevad ümardamisefektid. Numbrite puhul - tavaline, kui 0,5 ümardatakse ülespoole, ja topelttäpsuse korral - 0,5 ümardatakse lähima paaristäisarvu suunas.

Raha on midagi erilist

SELECT '10'::money::float8

ERROR:  cannot cast type money to double precision
LINE 1: SELECT '10'::money::float8
                          ^
********** Ошибка **********
ERROR: cannot cast type money to double precision
SQL-состояние: 42846
Символ: 19

PostgreSQLi järgi ei ole raha reaalne arv. Mõne inimese sõnul ka. Peame meeles pidama, et rahatüübi ülekandmine on võimalik ainult numbriliigiks, nii nagu rahatüübiks saab kanda ainult numbrilist tüüpi. Nüüd aga saad sellega mängida nii nagu süda ihkab. Aga see ei ole sama raha.

Smallint ja jada 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.
Символ: 18

PostgreSQL-ile ei meeldi raisata aega pisiasjadele. Mis need jadad põhinevad smallint'il? int, mitte vähem! Seetõttu proovib andmebaas ülaltoodud päringut sooritades edastada smallint mõnele muule täisarvutüübile ja näeb, et selliseid ülekandeid võib olla mitu. Millist kihti valida? Ta ei saa seda otsustada ja jookseb seetõttu veaga kokku.

Fail number kaks. "char"/char/varchar/text

Tegelastüüpides on ka hulk veidrusi. Saagem ka nendega tuttavaks.

Mis nipid need on?

SELECT 'ПЕТЯ'::"char"
     , 'ПЕТЯ'::"char"::bytea
     , 'ПЕТЯ'::char
     , 'ПЕТЯ'::char::bytea

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

Mis tüüpi "söe" see on, mis kloun see on? Meil pole neid vaja... Sest see teeskleb tavalise tähena, kuigi on jutumärkides. Ja see erineb tavalisest tähemärgist, mis on ilma jutumärkideta, selle poolest, et see väljastab ainult stringi esituse esimese baidi, samas kui tavaline tähemärk väljastab esimese märgi. Meie puhul on esimene märk P-täht, mis unicode'i esituses võtab enda alla 2 baiti, mida tõendab tulemuse teisendamine bytea tüüpi. Ja tüüp "char" võtab selle unicode'i esituses ainult esimese baidi. Milleks siis seda tüüpi vaja on? PostgreSQL-i dokumentatsioon ütleb, et see on eritüüp, mida kasutatakse erivajaduste jaoks. Nii et tõenäoliselt me ​​seda ei vaja. Kuid vaadake talle silma ja te ei eksi, kui kohtute tema erilise käitumisega.

Lisaruumid. Silma alt ära, meelest ära

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

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

Heitke pilk toodud näitele. Teisendasin spetsiaalselt kõik tulemused bytea tüüpi, nii et oleks selgelt näha, mis seal on. Kus on tühikud pärast varchar(6) valamist? Dokumentatsioonis on lühidalt kirjas: "Märgi väärtuse ülekandmisel teisele märgitüübile jäetakse tühik. Seda ebameeldivust tuleb meeles pidada. Ja pange tähele, et kui tsiteeritud stringikonstant kantakse otse tüüpi varchar(6), säilivad lõputühikud. Sellised on imed.

Fail number kolm. json/jsonb

JSON on eraldi struktuur, mis elab oma elu. Seetõttu on selle olemid ja PostgreSQL-i olemid veidi erinevad. Siin on näited.

Johnson ja Johnson. tunneta erinevust

SELECT 'null'::jsonb IS NULL

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

Asi on selles, et JSON-il on oma nullüksus, mis pole PostgreSQL-i NULL-i analoog. Samal ajal võib JSON-objekti enda väärtus olla NULL, nii et avaldis SELECT null::jsonb IS NULL (pange tähele, et jutumärkide puudumine) tagastab seekord tõene.

Ü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. Json-is salvestatakse objekt sellisena, nagu see on, ja jsonb-s on see juba salvestatud parsitud, indekseeritud struktuuri kujul. Seetõttu asendati teisel juhul objekti väärtus võtmega 1 [1, 2, 3] asemel [7, 8, 9], mis tuli struktuuri päris lõpus sama võtmega.

Ärge jooge vett oma näost

SELECT '{"reading": 1.230e-5}'::jsonb
     , '{"reading": 1.230e-5}'::json

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

PostgreSQL muudab oma JSONB-rakenduses reaalarvude vormingut, viies need klassikalisele kujule. JSON-tüübi puhul seda ei juhtu. Veidi imelik, aga tal on õigus.

Faili number neli. kuupäev/kellaaeg/ajatempel

Kuupäeva/kellaaja tüüpidega on ka veidrusi. Vaatame neid. Lubage mul kohe teha reservatsioon, et mõned käitumisomadused saavad selgeks, kui mõistate hästi ajavöönditega töötamise olemust. Kuid see on ka eraldi artikli teema.

Minu oma ei saa aru

SELECT '08-Jan-99'::date

ERROR:  date/time field value out of range: "08-Jan-99"
LINE 1: SELECT '08-Jan-99'::date
               ^
HINT:  Perhaps you need a different "datestyle" setting.
********** Ошибка **********
ERROR: date/time field value out of range: "08-Jan-99"
SQL-состояние: 22008
Подсказка: Perhaps you need a different "datestyle" setting.
Символ: 8

Näib, et mis siin arusaamatut on? Kuid andmebaas ei saa ikka veel aru, mille me siin esikohale paneme – aasta või päeva? Ja ta otsustab, et käes on 99. jaanuar 2008, mis paneb ta pähe. Üldiselt peate kuupäevade tekstivormingus edastamisel väga hoolikalt kontrollima, kui õigesti andmebaas need ära tundis (eriti analüüsige parameetrit datestyle käsuga SHOW datestyle), kuna selles küsimuses võivad ebaselgused olla väga kallid.

Kust sa selle said?

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

ERROR:  invalid input syntax for type time: "04:05 Europe/Moscow"
LINE 1: SELECT '04:05 Europe/Moscow'::time
               ^
********** Ошибка **********
ERROR: invalid input syntax for type time: "04:05 Europe/Moscow"
SQL-состояние: 22007
Символ: 8

Miks andmebaas selgesõnaliselt määratud kellaajast aru ei saa? Kuna ajavööndil ei ole lühendit, vaid täisnimi, mis on mõttekas ainult kuupäeva kontekstis, kuna see võtab arvesse ajavööndi muutuste ajalugu ja ilma kuupäevata see ei tööta. Ja juba ajajoone sõnastus tekitab küsimusi – mida programmeerija tegelikult mõtles? Seetõttu on siin kõik loogiline, kui vaadata.

Mis tal viga on?

Kujutage ette olukorda. Teie tabelis on väli ajatempli tüübiga. Tahad seda indekseerida. Kuid saate aru, et indeksi loomine sellel väljal ei ole selle kõrge selektiivsuse tõttu alati õigustatud (peaaegu kõik seda tüüpi väärtused on ainulaadsed). Seega otsustate vähendada indeksi selektiivsust, määrates tüübi kuupäevale. Ja saate üllatuse:

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

ERROR:  functions in index expression must be marked IMMUTABLE
********** Ошибка **********
ERROR: functions in index expression must be marked IMMUTABLE
SQL-состояние: 42P17

Mis viga? Fakt on see, et ajatempli tüübi ülekandmiseks kuupäevatüübile kasutatakse TimeZone süsteemiparameetri väärtust, mis muudab tüübi teisendusfunktsiooni sõltuvaks kohandatud parameetrist, st. muutlik. Sellised funktsioonid pole indeksis lubatud. Sel juhul peate selgelt märkima, millises ajavööndis tüübi ülekandmine toimub.

Kui praegu pole isegi praegu

Oleme harjunud, et now() tagastab praeguse kuupäeva/kellaaja, võttes arvesse ajavööndit. 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 samaks, olenemata sellest, kui palju aega on möödunud eelmisest päringust! Mis viga? Fakt on see, et now() ei ole praegune kellaaeg, vaid praeguse tehingu algusaeg. Seetõttu see tehingu raames ei muutu. Kõik päringud, mis käivitatakse väljaspool tehingu ulatust, pakitakse tehingusse kaudselt, mistõttu me ei märka, et aeg, mille tagastab lihtne SELECT now(); tegelikult mitte praegune... Kui soovid saada ausat praegust aega, pead kasutama funktsiooni clock_timestamp().

Faili number viis. natuke

Veidi imelik

SELECT '111'::bit(4)

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

Kummale poole tuleks tüübilaienduse korral bitte lisada? Tundub, et see on vasakul. Kuid ainult baasil on selles küsimuses erinev arvamus. Olge ettevaatlik: kui tüübi ülekandmisel numbrite arv ei ühti, ei saa te seda, mida tahtsite. See kehtib nii bittide paremale lisamise kui ka kärpimise kohta. Samuti paremal...

Faili number kuus. Massiivid

Isegi NULL ei lasknud

SELECT ARRAY[1, 2] || NULL

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

Tavaliste SQL-i kasutavate inimestena eeldame, et selle avaldise tulemus on NULL. Aga seda seal polnud. Tagatakse massiiv. Miks? Kuna sel juhul heidab baas täisarvu massiivile NULL-i ja kutsub kaudselt välja funktsiooni array_cat. Kuid ikkagi jääb ebaselgeks, miks see "massiivikass" massiivi ei lähtesta. Seda käitumist tuleb ka lihtsalt meeles pidada.

Tehke kokkuvõte. Kummalisi asju on palju. Enamik neist pole muidugi nii kriitilised, et rääkida räigelt sobimatust käitumisest. Ja teisi seletatakse kasutuslihtsuse või nende teatud olukordades rakendatavuse sagedusega. Kuid samal ajal on palju üllatusi. Seetõttu peate nende kohta teadma. Kui leiate mis tahes tüüpi käitumises midagi muud kummalist või ebatavalist, kirjutage kommentaaridesse, lisan hea meelega nende kohta saadaolevaid toimikuid.

Allikas: www.habr.com

Lisa kommentaar