Күдікті түрлері

Олардың сыртқы түріне күдікті ештеңе жоқ. Сонымен қатар, олар сізге жақсы және ұзақ уақыт бойы таныс болып көрінеді. Бірақ бұл сіз оларды тексергенше ғана. Міне, олар сіз күткеннен мүлдем басқаша жұмыс істеп, өздерінің жасырын табиғатын көрсетеді. Ал кейде олар шашыңызды тік ұстайтын нәрсені жасайды - мысалы, олар өздеріне сеніп тапсырылған құпия деректерді жоғалтады. Олармен бетпе-бет келсең, көлеңкеде бір капюшонның астында тынымсыз еңбек еткенімен, бірін-бірі танымайтынын алға тартады. Ақырында оларды таза суға жеткізу уақыты келді. Осы күдікті түрлермен де айналысайық.

PostgreSQL-де деректерді теру, оның барлық логикасына қарамастан, кейде өте таңқаларлық тосын сыйлар тудырады. Бұл мақалада біз олардың кейбір ерекшеліктерін түсіндіруге тырысамыз, олардың оғаш мінез-құлқының себебін түсінеміз және күнделікті тәжірибеде қиындықтарға тап болмау керектігін түсінеміз. Шынымды айтсам, мен бұл мақаланы өзім үшін бір анықтамалық, даулы істерге оңай сілтеме жасайтын анықтамалық ретінде құрастырдым. Сондықтан ол күдікті түрлерден жаңа тосынсыйлар табылған сайын толықтырылады. Ендеше, кеттік, тынымсыз дерекқор трекерлері!

Бірінші файл. нақты/қос дәлдік/сандық/ақша

Сандық түрлер мінез-құлықтағы тосынсыйлар тұрғысынан ең аз проблемалы болып көрінеді. Бірақ бұл қалай болса да. Ендеше солардан бастайық. Сонымен…

Санауды ұмытып қалдым

SELECT 0.1::real = 0.1

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

Не болды? Мәселе мынада, PostgreSQL типтелмеген 0.1 тұрақты мәнін қос дәлдікке түрлендіреді және оны нақты түрдегі 0.1-мен салыстыруға тырысады. Және бұл мүлдем басқа мағыналар! Идеясы машина жадында нақты сандарды көрсету. 0.1-ді соңғы екілік бөлшек ретінде көрсету мүмкін болмағандықтан (ол екілікте 0.0(0011) болар еді), цифрлары әртүрлі сандар әртүрлі болады, демек олар тең емес нәтиже. Жалпы, бұл бөлек мақаланың тақырыбы, мен мұнда толығырақ жазбаймын.

Қате қайдан шығады?

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

Көптеген адамдар PostgreSQL типті трансляциялау үшін функционалды белгілерге мүмкіндік беретінін біледі. Яғни, тек 1::int емес, сонымен қатар эквивалент болатын int(1) жазуға болады. Бірақ аттары бірнеше сөзден тұратын түрлер үшін емес! Сондықтан, функционалдық пішіндегі қос дәлдік түріне сандық мәнді шығарғыңыз келсе, осы түрдегі float8 бүркеншік атын пайдаланыңыз, яғни SELECT float8(1).

Шексіздіктен үлкен не бар?

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

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

Қараңызшы, бұл қандай! Шексіздіктен де үлкен нәрсе бар екен, бұл NaN! Сонымен бірге, PostgreSQL құжаттамасы бізге шынайы көзбен қарайды және NaN кез келген басқа саннан, демек, шексіздіктен көп екенін мәлімдейді. Керісінше -NaN үшін де дұрыс. Сәлем, математика сүйер қауым! Бірақ мұның бәрі нақты сандар контекстінде жұмыс істейтінін есте ұстауымыз керек.

Көзді дөңгелектеу

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

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

Базадан тағы бір күтпеген сәлем. Қайтадан, қос дәлдік пен сандық түрлердің әртүрлі дөңгелектеу әсерлері бар екенін есте сақтаңыз. Сандық үшін - кәдімгі, 0,5 дөңгелектенген кезде, ал қос дәлдік үшін - 0,5 ең жақын жұп бүтін санға қарай дөңгелектенеді.

Ақша - бұл ерекше нәрсе

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 бойынша ақша нақты сан емес. Кейбір адамдардың айтуы бойынша да. Ақша түрін тек сандық түрге шығаруға болатынын есте ұстауымыз керек, сол сияқты тек сандық түрді ақша түріне айналдыруға болады. Бірақ қазір сіз онымен жүрегіңіз қалағандай ойнай аласыз. Бірақ бұл бірдей ақша болмайды.

Smallint және тізбекті генерация

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 уақытты ұсақ-түйекке жұмсауды ұнатпайды. Smallint негізіндегі бұл тізбектер қандай? int, кем емес! Сондықтан, жоғарыдағы сұрауды орындауға тырысқанда, дерекқор smallint-ті қандай да бір басқа бүтін түрге шығаруға тырысады және мұндай трансляциялардың бірнеше болуы мүмкін екенін көреді. Қай актерлік құрамды таңдау керек? Ол мұны шеше алмайды, сондықтан қателікпен апатқа ұшырайды.

Екінші файл. "char"/char/varchar/мәтін

Кейіпкерлер түрлерінде де бірқатар оғаштықтар бар. Олармен де танысайық.

Бұл қандай трюктар?

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

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

Бұл қандай «чар» түрі, бұл қандай сайқымазақ? Бізге ондайлар керек емес... Өйткені ол тырнақшаға алынғанымен, кәдімгі таңба болып көрінеді. Және оның тырнақшасыз қарапайым таңбадан айырмашылығы, ол жолды ұсынудың тек бірінші байтын шығарады, ал қалыпты таңба бірінші таңбаны шығарады. Біздің жағдайда бірінші таңба P әрпі болып табылады, ол юникодты ұсынуда 2 байт алады, бұл нәтижені байт түріне түрлендіру арқылы дәлелденеді. Ал «char» түрі осы юникодты ұсынудың тек бірінші байтты алады. Сонда бұл түр не үшін қажет? PostgreSQL құжаттамасы бұл арнайы қажеттіліктер үшін қолданылатын арнайы түр екенін айтады. Сондықтан оның бізге қажет болуы екіталай. Бірақ оның көзіне қараңыз, ерекше мінез-құлқымен кездескенде қателеспейсіз.

Қосымша бос орындар. Көзден тыс, санадан тыс

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

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

Келтірілген мысалға назар аударыңыз. Мен барлық нәтижелерді байт түріне арнайы түрлендірдім, сонда ол жерде не бар екені анық көрінді. Varchar(6) трансляциясынан кейін кейінгі бос орындар қайда? Құжаттамада қысқаша былай делінген: «Таңбаның мәнін басқа таңба түріне шығарған кезде, кейінгі бос орын жойылады». Бұл ұнатпауды есте сақтау керек. Егер тырнақшаға алынған жол тұрақтысы тікелей varchar(6) түріне шығарылса, кейінгі бос орындар сақталатынын ескеріңіз. Ғажайыптар осындай.

Үшінші файл. json/jsonb

JSON - бұл жеке өмір сүретін жеке құрылым. Сондықтан оның нысандары мен PostgreSQL нысандары сәл өзгеше. Міне мысалдар.

Джонсон және Джонсон. айырмашылықты сезіну

SELECT 'null'::jsonb IS NULL

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

Мәселе мынада, JSON-тың PostgreSQL-тегі NULL аналогы емес өзінің нөлдік нысаны бар. Бұл ретте JSON нысанының өзінде NULL мәні болуы мүмкін, сондықтан SELECT null::jsonb IS NULL өрнегі (жалғыз тырнақшалардың жоқтығын ескеріңіз) бұл жолы ақиқат мәнін қайтарады.

Бір әріп бәрін өзгертеді

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

Мәселе мынада, json мен jsonb мүлдем басқа құрылымдар. json-да нысан сол күйінде сақталады, ал jsonb-де ол талданған, индекстелген құрылым түрінде сақталған. Сондықтан екінші жағдайда объектінің мәні 1 перне арқылы [1, 2, 3] орнынан [7, 8, 9] ауыстырылды, ол құрылымға дәл сол кілтпен ең соңында келді.

Бетіңізден су ішпеңіз

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

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

PostgreSQL өзінің JSONB енгізуінде нақты сандар пішімдеуін өзгертіп, оларды классикалық пішінге келтіреді. Бұл JSON түрі үшін болмайды. Біртүрлі, бірақ ол дұрыс.

Төртінші файл. күн/уақыт/уақыт белгісі

Сондай-ақ күн/уақыт түрлерінің кейбір оғаштары бар. Оларды қарастырайық. Уақыт белдеулерімен жұмыс істеудің мәнін жақсы түсінсеңіз, кейбір мінез-құлық ерекшеліктері анық болатынын бірден ескертемін. Бірақ бұл да бөлек мақаланың тақырыбы.

Менің сенікі түсінбейді

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

Бұл жерде не түсініксіз болып көрінеді? Бірақ дерекқор әлі де біз мұнда бірінші орынға не қойғанымызды түсінбейді - жыл немесе күн? Және ол 99 жылдың 2008 қаңтары деп шешті, бұл оның ақылын оятады. Жалпы айтқанда, күндерді мәтін пішімінде жіберген кезде, дерекқор оларды қаншалықты дұрыс танығанын мұқият тексеру керек (атап айтқанда, SHOW datestyle пәрменімен datestyle параметрін талдаңыз), өйткені бұл мәселедегі түсініксіздіктер өте қымбат болуы мүмкін.

Мұны қайдан алдың?

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

Неліктен дерекқор нақты көрсетілген уақытты түсінбейді? Өйткені уақыт белдеуінде аббревиатура жоқ, толық аты бар, ол тек күн контекстінде мағына береді, өйткені ол уақыт белдеуінің өзгеру тарихын ескереді және ол күнсіз жұмыс істемейді. Уақыт сызығының тұжырымының өзі сұрақтар тудырады - бағдарламашы шынымен нені білдірді? Сондықтан, қарасаңыз, мұнда бәрі қисынды.

Оған не болды?

Жағдайды елестетіп көріңіз. Кестеде timetamtz түрі бар өріс бар. Сіз оны индекстегіңіз келеді. Бірақ сіз бұл өрісте индексті құру оның жоғары селективтілігіне байланысты әрқашан ақтала бермейтінін түсінесіз (осы түрдегі барлық дерлік мәндер бірегей болады). Осылайша, түрді күнге шығару арқылы индекстің таңдамалылығын азайтуды шешесіз. Ал сіз тосын сый аласыз:

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

Не болды? Уақыт белгісінің түрін күн түріне шығару үшін TimeZone жүйе параметрінің мәні пайдаланылады, бұл түр түрлендіру функциясын реттелетін параметрге тәуелді етеді, яғни. өзгермелі. Мұндай функцияларға индексте рұқсат етілмейді. Бұл жағдайда трансляция түрі қай уақыт белдеуінде орындалатынын нақты көрсетуіңіз керек.

Қазір тіпті қазір емес кезде

Біз қазір() уақыт белдеуін ескере отырып, ағымдағы күнді/уақытты қайтаруға үйреніп қалдық. Бірақ келесі сұрауларды қараңыз:

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;

Алдыңғы сұраудан қанша уақыт өтсе де, күн/уақыт бірдей қайтарылады! Не болды? Өйткені, now() ағымдағы уақыт емес, ағымдағы транзакцияның басталу уақыты. Сондықтан ол транзакция ішінде өзгермейді. Транзакция аумағынан тыс іске қосылған кез келген сұрау транзакцияға жасырын түрде оралады, сондықтан біз қарапайым ТАҢДАУ арқылы қайтарылған уақытты қазір байқамаймыз(); шын мәнінде, ағымдағы уақыт емес... Адал ағымдағы уақытты алғыңыз келсе, clock_timestamp() функциясын пайдалануыңыз керек.

Файл нөмірі бес. бит

Біртүрлі

SELECT '111'::bit(4)

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

Түр кеңейтілген жағдайда биттерді қай жаққа қосу керек? Сол жақта тұрған сияқты. Бірақ бұл мәселеде тек базаның пікірі басқаша. Сақ болыңыз: егер түрді шығару кезінде цифрлар саны сәйкес келмесе, сіз қалаған нәрсені ала алмайсыз. Бұл биттерді оң жаққа қосуға да, биттерді кесуге де қатысты. Сондай-ақ оң жақта...

Файл нөмірі алты. Массивтер

Тіпті NULL де жұмыс істемеді

SELECT ARRAY[1, 2] || NULL

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

SQL-де өскен қалыпты адамдар ретінде біз бұл өрнектің нәтижесі NULL болады деп күтеміз. Бірақ ол жерде болмады. Массив қайтарылады. Неліктен? Өйткені бұл жағдайда негіз NULL мәнін бүтін массивке шығарады және жасырын түрде array_cat функциясын шақырады. Бірақ неге бұл «массив мысық» массивді қалпына келтірмейтіні әлі де түсініксіз. Бұл мінез-құлықты да есте сақтау керек.

Қорытындылау. Біртүрлі нәрселер көп. Олардың көпшілігі, әрине, ашық түрде орынсыз мінез-құлық туралы айтатындай сыни емес. Ал басқалары пайдаланудың қарапайымдылығымен немесе олардың белгілі бір жағдайларда қолдану жиілігімен түсіндіріледі. Бірақ сонымен бірге көптеген тосынсыйлар бар. Сондықтан сіз олар туралы білуіңіз керек. Егер сіз кез-келген түрдің мінез-құлқында тағы бір оғаш немесе әдеттен тыс нәрсені тапсаңыз, түсініктемелерде жазыңыз, мен оларда қол жетімді файлдарды қосуға қуаныштымын.

Ақпарат көзі: www.habr.com

пікір қалдыру