Şübhəli növlər

Onların görünüşündə şübhəli heç nə yoxdur. Üstəlik, onlar sizə yaxşı və uzun müddət tanış görünürlər. Ancaq bu, yalnız onları yoxlayana qədər. Burada onlar öz məkrli təbiətlərini nümayiş etdirirlər, gözlədiyinizdən tamamilə fərqli işləyirlər. Və bəzən saçınızı dirəyə salan bir şey edirlər - məsələn, onlara əmanət edilmiş gizli məlumatları itirirlər. Onlarla qarşılaşanda, kölgədə eyni başlıq altında çox çalışsalar da, bir-birlərini tanımadıqlarını iddia edirlər. Nəhayət onları təmiz suya gətirməyin vaxtı gəldi. Gəlin bu şübhəli növlərlə də məşğul olaq.

PostgreSQL-də məlumatların yazılması, bütün məntiqinə baxmayaraq, bəzən çox qəribə sürprizlər təqdim edir. Bu yazıda onların bəzi qəribəliklərini aydınlaşdırmağa, qəribə davranışlarının səbəbini başa düşməyə və gündəlik təcrübədə problemlərlə necə qarşılaşmamağı başa düşməyə çalışacağıq. Düzünü desəm, mən bu məqaləni həm də özüm üçün bir növ istinad kitabı, mübahisəli işlərdə asanlıqla istinad oluna biləcək bir istinad kitabı kimi tərtib etmişəm. Buna görə də, şübhəli növlərdən yeni sürprizlər aşkar edildikdə, o, doldurulacaq. Beləliklə, gedək, ey yorulmaz verilənlər bazası izləyiciləri!

Bir nömrəli dosye. real/ikiqat dəqiqlik/rəqəm/pul

Davranışdakı sürprizlər baxımından rəqəmsal növlər ən az problemli olduğu görünür. Amma necə olursa olsun. Beləliklə, onlardan başlayaq. Belə ki…

Saymağı unutmuşam

SELECT 0.1::real = 0.1

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

Nə məsələdir? Problem ondadır ki, PostgreSQL tipsiz 0.1 sabitini ikiqat dəqiqliyə çevirir və onu real tipli 0.1 ilə müqayisə etməyə çalışır. Və bunlar tamamilə fərqli mənalardır! İdeya maşın yaddaşında həqiqi ədədləri təmsil etməkdir. 0.1 sonlu ikili kəsr kimi göstərilə bilmədiyi üçün (ikilikdə 0.0(0011) olardı), bit dərinlikləri fərqli olan ədədlər fərqli olacaq, nəticədə onlar bərabər deyildir. Ümumiyyətlə, bu ayrı bir məqalənin mövzusudur, burada daha ətraflı yazmayacağam.

Səhv haradan gəlir?

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

Çoxları bilir ki, PostgreSQL tip tökmə üçün funksional nota imkan verir. Yəni təkcə 1::int deyil, həm də ekvivalent olacaq int(1) yaza bilərsiniz. Ancaq adları bir neçə sözdən ibarət olan növlər üçün deyil! Buna görə də, funksional formada ikiqat dəqiqlik növünə rəqəmli dəyər köçürmək istəyirsinizsə, bu tip float8 ləqəbindən, yəni SELECT float8(1) istifadə edin.

Sonsuzluqdan böyük nədir?

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

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

Görün necədir! Məlum oldu ki, sonsuzluqdan daha böyük bir şey var və o, NaN! Eyni zamanda, PostgreSQL sənədləri bizə vicdanla baxır və NaN-nin hər hansı digər rəqəmdən açıq-aşkar böyük olduğunu və buna görə də sonsuz olduğunu iddia edir. Bunun əksi -NaN üçün də doğrudur. Salam, riyaziyyat həvəskarları! Ancaq yadda saxlamalıyıq ki, bütün bunlar real rəqəmlər kontekstində işləyir.

Gözlərin yuvarlaqlaşdırılması

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

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

Bazadan daha bir gözlənilməz salam. Yenə də unutmayın ki, ikiqat dəqiqlik və ədədi növlərin müxtəlif yuvarlaqlaşdırma effektləri var. Rəqəm üçün - adi bir, 0,5 yuvarlaqlaşdırıldıqda və ikiqat dəqiqlik üçün - 0,5 ən yaxın cüt tam ədədə yuvarlaqlaşdırılır.

Pul xüsusi bir şeydir

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

PostgreSQL-ə görə pul real rəqəm deyil. Bəzi şəxslərə görə də. Yadda saxlamalıyıq ki, pul növünün yalnız rəqəmli tipə ötürülməsi mümkündür, necə ki, pul növünə yalnız rəqəmli tip verilə bilər. Amma indi ürəyin istədiyi kimi onunla oynaya bilərsən. Amma eyni pul olmayacaq.

Smallint və ardıcıllıq generasiyası

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 xırda şeylərə vaxt sərf etməyi sevmir. Smallint-ə əsaslanan bu ardıcıllıqlar hansılardır? int, az deyil! Buna görə də, yuxarıdakı sorğunu yerinə yetirmək istəyərkən verilənlər bazası smallint-i hansısa başqa tam ədəd növünə köçürməyə çalışır və görür ki, bir neçə belə cast ola bilər. Hansı aktyoru seçmək lazımdır? O, buna qərar verə bilmir və buna görə də xəta ilə çökür.

Fayl nömrəsi iki. "char"/char/varchar/mətn

Xarakter tiplərində də bir sıra qəribəliklər mövcuddur. Gəlin onlarla da tanış olaq.

Bunlar hansı hiylələrdir?

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

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

Bu nə cür “char”dır, bu nə təlxəkdir? Bizə bunlar lazım deyil... Çünki dırnaq içində olsa da özünü adi bir simvol kimi göstərir. Və o, dırnaq işarəsi olmayan adi simvoldan onunla fərqlənir ki, o, sətir təsvirinin yalnız ilk baytını verir, normal simvol isə birinci simvolu verir. Bizim vəziyyətimizdə birinci simvol P hərfidir, unicode təmsilində 2 bayt yer tutur, nəticənin bayt tipinə çevrilməsi sübut olunur. Və "char" növü bu unicode təmsilinin yalnız ilk baytını alır. Bəs bu tip nə üçün lazımdır? PostgreSQL sənədləri bunun xüsusi ehtiyaclar üçün istifadə edilən xüsusi bir növ olduğunu söyləyir. Deməli, bizim buna ehtiyacımız yoxdur. Amma onun gözlərinin içinə baxın, xüsusi davranışı ilə qarşılaşanda yanılmazsınız.

Əlavə boşluqlar. Gözdən uzaq, ağıldan kənar

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

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

Verilən nümunəyə nəzər salın. Mən bütün nəticələri xüsusi olaraq bayt növünə çevirdim ki, orada olanlar aydın görünsün. Varchar(6)-a ötürüldükdən sonra arxadakı boşluqlar haradadır? Sənədlərdə qısa şəkildə deyilir: "Xarakterin dəyərini başqa simvol növünə köçürərkən, arxadakı boşluq ləğv edilir." Bu xoşagəlməzliyi xatırlamaq lazımdır. Və nəzərə alın ki, sitat gətirilən sətir sabiti birbaşa varchar(6) növünə ötürülürsə, arxadakı boşluqlar qorunur. Möcüzələr belədir.

Üç nömrəli fayl. json/jsonb

JSON öz həyatını yaşayan ayrı bir quruluşdur. Buna görə də, onun qurumları və PostgreSQL-in obyektləri bir qədər fərqlidir. Budur nümunələr.

Johnson və Johnson. fərqi hiss et

SELECT 'null'::jsonb IS NULL

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

Məsələ ondadır ki, JSON-un PostgreSQL-də NULL-un analoqu olmayan öz null obyekti var. Eyni zamanda, JSON obyektinin özü də NULL dəyərinə malik ola bilər, ona görə də SELECT null::jsonb IS NULL ifadəsi (tək dırnaqların olmamasına diqqət yetirin) bu dəfə doğru qaytaracaq.

Bir hərf hər şeyi dəyişir

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

İş ondadır ki, json və jsonb tamamilə fərqli strukturlardır. json-da obyekt olduğu kimi saxlanılır və jsonb-da o, artıq təhlil edilmiş, indeksləşdirilmiş struktur şəklində saxlanılır. Məhz buna görə də ikinci halda obyektin 1 açarı ilə dəyəri eyni açarla struktura ən sonunda daxil olan [1, 2, 3]-dən [7, 8, 9]-a dəyişdirildi.

Üzünüzdən su içməyin

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

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

PostgreSQL, JSONB tətbiqində real ədədlərin formatını dəyişdirərək onları klassik formaya gətirir. Bu JSON növü üçün baş vermir. Bir az qəribədir, amma o, haqlıdır.

Dördüncü fayl. tarix/saat/zaman damgası

Tarix/saat növləri ilə bağlı bəzi qəribəliklər də var. Gəlin onlara baxaq. Dərhal qeyd edim ki, saat qurşağı ilə işləməyin mahiyyətini yaxşı başa düşsəniz, bəzi davranış xüsusiyyətləri aydınlaşacaq. Amma bu da ayrı bir məqalənin mövzusudur.

Mənimkilər başa düşmür

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

Belə görünür ki, burada anlaşılmaz nə var? Lakin verilənlər bazası hələ də başa düşmür ki, biz burada birinci yerə nə qoyduq - il yoxsa gün? Və o, 99-ci ilin 2008 yanvarı olduğuna qərar verir ki, ağlını başından alır. Ümumiyyətlə, tarixləri mətn formatında ötürərkən, verilənlər bazasının onları nə dərəcədə düzgün tanıdığını çox diqqətlə yoxlamaq lazımdır (xüsusən, SHOW datestyle əmri ilə datestyle parametrini təhlil edin), çünki bu məsələdə qeyri-müəyyənliklər çox baha ola bilər.

Bunu hardan almisan?

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

Nə üçün verilənlər bazası açıq şəkildə göstərilən vaxtı başa düşə bilmir? Çünki saat qurşağının abreviaturası yox, tam adı var ki, bu da yalnız tarix kontekstində məna kəsb edir, çünki saat qurşağının dəyişmə tarixini nəzərə alır və tarixsiz işləmir. Və zaman xəttinin özü suallar doğurur - proqramçı həqiqətən nə demək istəyirdi? Ona görə də baxsanız, burada hər şey məntiqlidir.

Onun nə günahı var?

Vəziyyəti təsəvvür edin. Cədvəlinizdə timestampz tipli sahə var. Siz onu indeksləşdirmək istəyirsiniz. Ancaq başa düşürsən ki, bu sahədə bir indeks qurmaq həmişə yüksək seçiciliyə görə əsaslandırılmır (bu növün demək olar ki, bütün dəyərləri unikal olacaq). Beləliklə, növü bir tarixə köçürməklə indeksin seçiciliyini azaltmağa qərar verdiniz. Və sürprizlə qarşılaşırsınız:

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

Nə məsələdir? Fakt budur ki, timestampz növünü tarix növünə köçürmək üçün TimeZone sistem parametrinin dəyəri istifadə olunur ki, bu da növün çevrilməsi funksiyasını xüsusi parametrdən asılı edir, yəni. uçucu. İndeksdə belə funksiyalara icazə verilmir. Bu halda, növün çəkilişinin hansı saat qurşağında həyata keçirildiyini açıq şəkildə göstərməlisiniz.

İndi heç indi də olmayanda

Biz indi() vaxt zonasını nəzərə alaraq cari tarixi/saatı qaytarmağa öyrəşmişik. Ancaq aşağıdakı sorğulara baxın:

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;

Əvvəlki sorğudan nə qədər vaxt keçməsindən asılı olmayaraq tarix/saat eyni şəkildə qaytarılır! Nə məsələdir? Fakt budur ki, now() cari vaxt deyil, cari əməliyyatın başlama vaxtıdır. Buna görə də, əməliyyat daxilində dəyişmir. Tranzaksiya çərçivəsindən kənarda işə salınan istənilən sorğu gizli şəkildə əməliyyata daxil edilir, buna görə də biz vaxtın indi sadə SELECT (); əslində indiki vaxt deyil... Dürüst cari vaxtı əldə etmək istəyirsinizsə, clock_timestamp() funksiyasından istifadə etməlisiniz.

Fayl nömrəsi beş. az

Bir az qəribə

SELECT '111'::bit(4)

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

Tip uzadılması halında bitlər hansı tərəfə əlavə edilməlidir? Deyəsən soldadır. Ancaq bu məsələdə yalnız baza fərqli fikirdədir. Ehtiyatlı olun: bir növü yayarkən rəqəmlərin sayı uyğun gəlmirsə, istədiyinizi əldə etməyəcəksiniz. Bu, həm sağa bitlərin əlavə edilməsinə, həm də bitlərin kəsilməsinə aiddir. Həm də sağda...

Fayl nömrəsi altı. Massivlər

Hətta NULL atəş etməyib

SELECT ARRAY[1, 2] || NULL

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

SQL-də böyüyən normal insanlar kimi bu ifadənin nəticəsinin NULL olmasını gözləyirik. Amma orda yoxdu. Massiv qaytarılır. Niyə? Çünki bu halda baza tam ədəd massivinə NULL verir və dolayısı ilə array_cat funksiyasını çağırır. Ancaq bu "massiv pişiyi"nin serialı niyə sıfırlamadığı hələ də aydın deyil. Bu davranışı da xatırlamaq lazımdır.

Ümumiləşdirin. Çox qəribə şeylər var. Onların əksəriyyəti, əlbəttə ki, açıq-aydın yersiz davranışlar haqqında danışacaq qədər tənqidi deyil. Digərləri isə istifadənin asanlığı və ya müəyyən hallarda tətbiq olunma tezliyi ilə izah olunur. Ancaq eyni zamanda, bir çox sürprizlər var. Buna görə də, onlar haqqında bilmək lazımdır. Hər hansı bir növün davranışında başqa qəribə və ya qeyri-adi bir şey taparsanız, şərhlərdə yazın, onlarda mövcud olan dosyelərə əlavə etməkdən məmnun olaram.

Mənbə: www.habr.com

Добавить комментарий