Suspektindaj tipoj

Estas nenio suspektinda pri ilia aspekto. Krome, ili eĉ ŝajnas al vi konataj bone kaj longe. Sed tio estas nur ĝis vi kontrolos ilin. Ĉi tie ili montras sian insidan naturon, laborante tute alie ol vi atendis. Kaj foje ili faras ion, kio igas viajn harojn stariĝi - ekzemple, ili perdas sekretajn datumojn konfiditajn al ili. Kiam vi alfrontas ilin, ili asertas, ke ili ne konas unu la alian, kvankam en la ombro ili laboras forte sub la sama kapuĉo. Estas tempo finfine alporti ilin al pura akvo. Ni ankaŭ traktu ĉi tiujn suspektindajn tipojn.

La tajpado de datumoj en PostgreSQL, malgraŭ ĝia tuta logiko, foje prezentas tre strangajn surprizojn. En ĉi tiu artikolo ni provos klarigi kelkajn el iliaj strangaĵoj, kompreni la kialon de ilia stranga konduto kaj kompreni kiel ne renkonti problemojn en ĉiutaga praktiko. Verdire, mi kompilis ĉi tiun artikolon ankaŭ kiel specon de konsultlibro por mi mem, konsultlibro, kiu povus esti facile aludita en polemikaj kazoj. Tial, ĝi estos replenigita kiam novaj surprizoj de suspektindaj tipoj estas malkovritaj. Do, ni iru, ho senlacaj datumbazaj spuristoj!

Dosiero numero unu. reala/duobla precizeco/numera/mono

Ŝajnus, ke nombraj tipoj estas la malplej problemaj laŭ surprizoj en konduto. Sed kiel ajn ĝi estas. Do ni komencu per ili. Do…

Forgesis kiel kalkuli

SELECT 0.1::real = 0.1

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

Kio estas la problemo? La problemo estas, ke PostgreSQL konvertas la netajpitan konstantan 0.1 al duobla precizeco kaj provas kompari ĝin kun 0.1 de reala tipo. Kaj ĉi tiuj estas tute malsamaj signifoj! La ideo estas reprezenti realajn nombrojn en maŝinmemoro. Ĉar 0.1 ne povas esti reprezentita kiel finhava binara frakcio (ĝi estus 0.0(0011) en binara), nombroj kun malsamaj bitprofundoj estos malsamaj, tial la rezulto ke ili ne estas egalaj. Ĝenerale, ĉi tio estas temo por aparta artikolo; mi ne skribos pli detale ĉi tie.

De kie venas la eraro?

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

Multaj homoj scias, ke PostgreSQL ebligas funkcian notacion por tipo-gisado. Tio estas, vi povas skribi ne nur 1::int, sed ankaŭ int(1), kiu estos ekvivalenta. Sed ne por tipoj, kies nomoj konsistas el pluraj vortoj! Tial, se vi volas ĵeti nombran valoron al duobla precizeca tipo en funkcia formo, uzu la kaŝnomon de ĉi tiu tipo float8, tio estas, SELECT float8(1).

Kio estas pli granda ol senfineco?

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

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

Rigardu, kia ĝi estas! Montriĝas, ke estas io pli granda ol senfineco, kaj ĝi estas NaN! Samtempe, la PostgreSQL-dokumentado rigardas nin per honestaj okuloj kaj asertas, ke NaN estas evidente pli granda ol iu ajn alia nombro, kaj, do, senfineco. La malo validas ankaŭ por -NaN. Saluton, amantoj de matematiko! Sed ni devas memori, ke ĉio ĉi funkcias en la kunteksto de realaj nombroj.

Okula rondigo

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

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

Alia neatendita saluto de la bazo. Denove, memoru, ke duobla precizeco kaj nombraj tipoj havas malsamajn rondigajn efikojn. Por nombra - la kutima maniero, kiam 0,5 estas rondigita supren, kaj por duobla precizeco - 0,5 estas rondigita al la plej proksima para entjero.

Mono estas io speciala

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

Laŭ PostgreSQL, mono ne estas vera nombro. Laŭ kelkaj individuoj ankaŭ. Ni devas memori, ke ĵeti la monspecon nur eblas al la nombra tipo, same kiel nur la nombra tipo povas esti ĵetita al la montipo. Sed nun vi povas ludi kun ĝi kiel via koro deziras. Sed ĝi ne estos la sama mono.

Smallint kaj sekvencogeneracio

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 ne ŝatas perdi tempon per bagateloj. Kio estas ĉi tiuj sekvencoj bazitaj sur smallint? int, ne malpli! Tial, provante efektivigi la ĉi-supran demandon, la datumbazo provas gisi smallint al iu alia entjera tipo, kaj vidas ke povas ekzisti pluraj tiaj rolantaroj. Kiun rolantaron elekti? Ŝi ne povas decidi ĉi tion, kaj tial kraŝas pro eraro.

Dosiero numero du. "char"/char/varchar/text

Kelkaj strangaĵoj ankaŭ ĉeestas en karakterospecoj. Ni ankaŭ konu ilin.

Kiaj lertaĵoj estas ĉi tiuj?

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

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

Kia "char" ĉi tio estas, kia klaŭno ĉi tio estas? Ni ne bezonas tiujn... Ĉar ĝi ŝajnigas esti ordinara char, kvankam ĝi estas inter citiloj. Kaj ĝi diferencas de regula signo, kiu estas sen citiloj, en tio, ke ĝi eligas nur la unuan bajton de la ĉena prezento, dum normala signo eligas la unuan signon. En nia kazo, la unua signo estas la litero P, kiu en la unikoda reprezento okupas 2 bajtojn, kiel pruvas konvertado de la rezulto al la bajta tipo. Kaj la tipo "char" prenas nur la unuan bajton de ĉi tiu unikoda prezento. Kial do necesas ĉi tiu tipo? La PostgreSQL-dokumentado diras, ke ĉi tio estas speciala tipo uzata por specialaj bezonoj. Do ni verŝajne ne bezonos ĝin. Sed rigardu liajn okulojn kaj vi ne eraros, kiam vi renkontos lin kun lia speciala konduto.

Kromaj spacoj. For de vido, for de menso

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

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

Rigardu la ekzemplon donita. Mi speciale konvertis ĉiujn rezultojn al la bajtea tipo, tiel ke estis klare videbla kio estis tie. Kie estas la postaj spacoj post ĵetado al varchar(6)? La dokumentaro koncize deklaras: "Kiam oni ĵetas la valoron de karaktero al alia signotipo, malantaŭa blankspaco estas forĵetita." Ĉi tiu malŝato devas esti memorita. Kaj rimarku, ke se citita ĉenkonstanto estas ĵetita rekte al tipo varchar(6), la postaj spacoj estas konservitaj. Tiaj estas la mirakloj.

Dosiero numero tri. json/jsonb

JSON estas aparta strukturo, kiu vivas sian propran vivon. Tial ĝiaj estaĵoj kaj tiuj de PostgreSQL estas iomete malsamaj. Jen ekzemploj.

Johnson kaj Johnson. senti la diferencon

SELECT 'null'::jsonb IS NULL

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

La afero estas, ke JSON havas sian propran nulan enton, kiu ne estas la analogo de NULL en PostgreSQL. Samtempe, la JSON-objekto mem povas bone havi la valoron NULL, do la esprimo SELECT null::jsonb IS NULL (notu la foreston de unuopaj citiloj) revenos vera ĉi-foje.

Unu letero ŝanĝas ĉion

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]}

La afero estas, ke json kaj jsonb estas tute malsamaj strukturoj. En json, la objekto estas konservita kiel estas, kaj en jsonb ĝi jam estas konservita en la formo de analizita, indeksita strukturo. Tial en la dua kazo, la valoro de la objekto per ŝlosilo 1 estis anstataŭigita de [1, 2, 3] al [7, 8, 9], kiu venis en la strukturon ĉe la fino mem per la sama ŝlosilo.

Ne trinku akvon el via vizaĝo

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

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

PostgreSQL en ĝia JSONB-efektivigo ŝanĝas la formatadon de realaj nombroj, alportante ilin al la klasika formo. Ĉi tio ne okazas por la tipo JSON. Iom strange, sed li pravas.

Dosiero numero kvar. dato/tempo/tempomarko

Estas ankaŭ kelkaj strangaĵoj kun dataj/tempospecoj. Ni rigardu ilin. Permesu al mi tuj rezervi, ke iuj el la kondutismaj trajtoj evidentiĝas, se vi bone komprenas la esencon labori kun horzonoj. Sed ĉi tio ankaŭ estas temo por aparta artikolo.

Miaj viaj ne komprenas

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

Ŝajnus, ke kio ĉi tie estas nekomprenebla? Sed la datumbazo ankoraŭ ne komprenas, kion ni metis en la unua loko ĉi tie—la jaro aŭ la tago? Kaj ŝi decidas ke ĝi estas januaro 99, 2008, kiu krevigas ŝian menson. Ĝenerale, kiam vi transdonas datojn en tekstformato, vi devas tre zorge kontroli kiom ĝuste la datumbazo ilin rekonis (precipe analizu la datstilan parametron per la komando SHOW datstilo), ĉar ambiguecoj en ĉi tiu afero povas esti tre multekostaj.

De kie vi akiris ĉi tion?

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

Kial la datumbazo ne povas kompreni la eksplicite specifitan tempon? Ĉar la horzono ne havas mallongigon, sed plenan nomon, kio havas sencon nur en la kunteksto de dato, ĉar ĝi konsideras la historion de la horzonŝanĝoj, kaj ĝi ne funkcias sen dato. Kaj la vortumo mem de la templinio levas demandojn - kion vere volis diri la programisto? Sekve, ĉio estas logika ĉi tie, se vi rigardas ĝin.

Kio estas al li?

Imagu la situacion. Vi havas kampon en via tabelo kun tajpa timestamptz. Vi volas indeksi ĝin. Sed vi komprenas, ke konstrui indekson pri ĉi tiu kampo ne ĉiam pravigas pro ĝia alta selektiveco (preskaŭ ĉiuj valoroj de ĉi tiu tipo estos unikaj). Do vi decidas redukti la selektivecon de la indekso ĵetante la tipon al dato. Kaj vi ricevas surprizon:

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

Kio estas la problemo? La fakto estas, ke por gisi timestamptz-tipo al dato-tipo, la valoro de la TimeZone-sistema parametro estas uzata, kio faras la tipkonvertan funkcion dependa de kutima parametro, t.e. volatila. Tiaj funkcioj ne estas permesitaj en la indekso. En ĉi tiu kazo, vi devas eksplicite indiki en kiu horzono la tipa rolado estas farita.

Kiam nun estas eĉ nun tute ne

Ni kutimas nun() redoni la aktualan daton/tempon, konsiderante la horzonon. Sed rigardu la sekvajn demandojn:

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;

La dato/tempo estas resendita la sama kiom ajn tempo pasis ekde la antaŭa peto! Kio estas la problemo? Fakte, nun() ne estas la nuna tempo, sed la komenca tempo de la nuna transakcio. Tial ĝi ne ŝanĝiĝas ene de la transakcio. Ajna demando lanĉita ekster la amplekso de transakcio estas envolvita en transakcio implicite, tial ni ne rimarkas, ke la tempo redonita de simpla SELECT nun(); fakte, ne la nunan... Se vi volas akiri honestan aktualan tempon, vi devas uzi la funkcion clock_timestamp().

Dosiero numero kvin. iom

Iom strange

SELECT '111'::bit(4)

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

Kiu flanko devus esti aldonitaj la bitoj en kazo de tipa etendo? Ĝi ŝajnas esti maldekstre. Sed nur la bazo havas alian opinion pri ĉi tiu afero. Atentu: se la nombro da ciferoj ne kongruas dum ĵetado de tipo, vi ne ricevos tion, kion vi deziris. Ĉi tio validas por kaj aldonado de bitoj dekstre kaj tondado de bitoj. Ankaŭ dekstre...

Dosiero numero ses. Tabeloj

Eĉ NULL ne pafis

SELECT ARRAY[1, 2] || NULL

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

Kiel normalaj homoj levitaj sur SQL, ni atendas ke la rezulto de ĉi tiu esprimo estu NULL. Sed ĝi ne estis tie. Tabelo estas resendita. Kial? Ĉar ĉi-kaze la bazo ĵetas NULL al entjera tabelo kaj implicite vokas la funkcion array_cat. Sed ankoraŭ restas neklara kial ĉi tiu "ararkato" ne rekomencigas la tabelon. Ĉi tiu konduto ankaŭ nur bezonas esti memorita.

Resumu. Estas multe da strangaĵoj. La plej multaj el ili, kompreneble, ne estas tiom kritikaj por paroli pri evidente netaŭga konduto. Kaj aliaj estas klarigitaj per facileco de uzo aŭ la ofteco de ilia aplikebleco en certaj situacioj. Sed samtempe estas multaj surprizoj. Tial vi devas scii pri ili. Se vi trovas ion alian strangan aŭ nekutima en la konduto de ajna tipo, skribu en la komentoj, mi volonte aldonos al la dosieroj disponeblaj pri ili.

fonto: www.habr.com

Aldoni komenton