Jenis yang mencurigakan

Tidak ada yang mencurigakan tentang penampilan mereka. Lebih-lebih lagi, mereka kelihatan biasa kepada anda dengan baik dan untuk masa yang lama. Tetapi itu hanya sehingga anda menyemaknya. Di sinilah mereka menunjukkan sifat berbahaya mereka, bekerja dengan cara yang berbeza daripada yang anda jangkakan. Dan kadangkala mereka melakukan sesuatu yang membuatkan rambut anda berdiri tegak - contohnya, mereka kehilangan data rahsia yang diamanahkan kepada mereka. Apabila anda berhadapan dengan mereka, mereka mendakwa bahawa mereka tidak mengenali satu sama lain, walaupun dalam bayang-bayang mereka bekerja keras di bawah tudung yang sama. Sudah tiba masanya untuk membawa mereka ke air bersih. Marilah kita juga menangani jenis yang mencurigakan ini.

Penaipan data dalam PostgreSQL, untuk semua logiknya, kadangkala memberikan kejutan yang sangat pelik. Dalam artikel ini kami akan cuba menjelaskan beberapa kebiasaan mereka, memahami sebab tingkah laku aneh mereka dan memahami bagaimana untuk tidak menghadapi masalah dalam amalan seharian. Sejujurnya, artikel ini saya susun juga sebagai buku rujukan untuk diri saya sendiri, buku rujukan yang mudah dirujuk dalam kes kontroversi. Oleh itu, ia akan diisi semula apabila kejutan baharu daripada jenis yang mencurigakan ditemui. Jadi, mari pergi, wahai penjejak pangkalan data yang tidak mengenal penat lelah!

Dosis nombor satu. ketepatan sebenar/berganda/angka/wang

Nampaknya jenis angka adalah yang paling kurang bermasalah dari segi kejutan dalam tingkah laku. Tetapi tidak kira bagaimana keadaannya. Jadi mari kita mulakan dengan mereka. Jadi…

Lupa cara mengira

SELECT 0.1::real = 0.1

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

Apa masalahnya? Masalahnya ialah PostgreSQL menukar pemalar yang tidak ditaip 0.1 kepada ketepatan berganda dan cuba membandingkannya dengan 0.1 jenis sebenar. Dan ini adalah makna yang sama sekali berbeza! Ideanya adalah untuk mewakili nombor nyata dalam memori mesin. Memandangkan 0.1 tidak boleh diwakili sebagai pecahan perduaan terhingga (ia akan menjadi 0.0(0011) dalam perduaan), nombor dengan digit yang berbeza akan berbeza, maka hasilnya tidak sama. Secara umumnya, ini adalah topik untuk artikel berasingan; Saya tidak akan menulis dengan lebih terperinci di sini.

Dari mana datangnya kesilapan?

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

Ramai orang tahu bahawa PostgreSQL membenarkan tatatanda berfungsi untuk pemutus jenis. Iaitu, anda boleh menulis bukan sahaja 1::int, tetapi juga int(1), yang akan menjadi setara. Tetapi bukan untuk jenis yang namanya terdiri daripada beberapa perkataan! Oleh itu, jika anda ingin menghantar nilai berangka kepada jenis ketepatan menggandakan dalam bentuk berfungsi, gunakan alias jenis float8 ini, iaitu, SELECT float8(1).

Apa yang lebih besar daripada infiniti?

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

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

Tengok macam mana! Ternyata ada sesuatu yang lebih besar daripada infiniti, dan ia adalah NaN! Pada masa yang sama, dokumentasi PostgreSQL melihat kami dengan mata yang jujur ​​dan mendakwa bahawa NaN jelas lebih besar daripada mana-mana nombor lain, dan, oleh itu, infiniti. Perkara sebaliknya juga berlaku untuk -NaN. Hello, pencinta matematik! Tetapi kita mesti ingat bahawa semua ini beroperasi dalam konteks nombor nyata.

Membulat mata

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

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

Satu lagi sapaan yang tidak dijangka dari pangkalan. Sekali lagi, ingat bahawa jenis ketepatan berganda dan angka mempunyai kesan pembundaran yang berbeza. Untuk angka - cara biasa, apabila 0,5 dibundarkan ke atas, dan untuk ketepatan dua kali ganda - 0,5 dibundarkan ke arah integer genap terdekat.

Wang adalah sesuatu yang istimewa

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

Menurut PostgreSQL, wang bukanlah nombor sebenar. Menurut beberapa individu juga. Kita perlu ingat bahawa menghantar jenis wang hanya boleh dilakukan kepada jenis angka, sama seperti hanya jenis angka boleh dihantar ke jenis wang. Tetapi kini anda boleh bermain dengannya mengikut kehendak hati anda. Tetapi ia tidak akan menjadi wang yang sama.

Smallint dan penjanaan jujukan

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 tidak suka membuang masa dengan perkara kecil. Apakah jujukan ini berdasarkan smallint? int, tidak kurang! Oleh itu, apabila cuba melaksanakan pertanyaan di atas, pangkalan data cuba menghantar smallint kepada beberapa jenis integer lain, dan melihat bahawa mungkin terdapat beberapa hantaran sedemikian. Pelakon mana yang hendak dipilih? Dia tidak boleh memutuskan perkara ini, dan oleh itu ranap dengan ralat.

Fail nombor dua. "char"/char/varchar/text

Beberapa keanehan juga terdapat dalam jenis watak. Jom kenali mereka juga.

Apakah jenis helah ini?

SELECT 'ΠŸΠ•Π’Π―'::"char"
     , 'ΠŸΠ•Π’Π―'::"char"::bytea
     , 'ΠŸΠ•Π’Π―'::char
     , 'ΠŸΠ•Π’Π―'::char::bytea

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

Apakah jenis "char" ini, jenis badut apakah ini? Kami tidak memerlukannya... Kerana ia berpura-pura menjadi watak biasa, walaupun ia dalam petikan. Dan ia berbeza daripada aksara biasa, yang tanpa petikan, kerana ia hanya mengeluarkan bait pertama bagi perwakilan rentetan, manakala aksara biasa mengeluarkan aksara pertama. Dalam kes kami, aksara pertama ialah huruf P, yang dalam perwakilan unicode mengambil masa 2 bait, seperti yang dibuktikan dengan menukar hasil kepada jenis bait. Dan jenis "char" hanya mengambil bait pertama perwakilan unikod ini. Kemudian mengapa jenis ini diperlukan? Dokumentasi PostgreSQL mengatakan bahawa ini adalah jenis khas yang digunakan untuk keperluan khas. Jadi kita tidak mungkin memerlukannya. Tapi tengok mata dia tak silap bila jumpa dia dengan perangai istimewa dia.

Ruang tambahan. Di luar pandangan, di luar fikiran

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

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

Lihat contoh yang diberikan. Saya menukar khas semua hasil kepada jenis bait, supaya ia dapat dilihat dengan jelas apa yang ada. Di manakah ruang mengekor selepas menghantar ke varchar(6)? Dokumentasi dengan ringkas menyatakan: "Apabila menghantar nilai aksara kepada jenis aksara lain, ruang kosong mengekor akan dibuang." Kebencian ini harus diingat. Dan ambil perhatian bahawa jika pemalar rentetan yang dipetik dilemparkan terus ke taip varchar(6), ruang mengekor dikekalkan. Begitulah mukjizatnya.

Fail nombor tiga. json/jsonb

JSON ialah struktur berasingan yang menjalani kehidupannya sendiri. Oleh itu, entitinya dan entiti PostgreSQL sedikit berbeza. Berikut adalah contoh.

Johnson dan Johnson. rasai kelainannya

SELECT 'null'::jsonb IS NULL

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

Masalahnya ialah JSON mempunyai entiti null sendiri, yang bukan analog NULL dalam PostgreSQL. Pada masa yang sama, objek JSON itu sendiri mungkin mempunyai nilai NULL, jadi ungkapan SELECT null::jsonb IS NULL (perhatikan ketiadaan petikan tunggal) akan kembali benar kali ini.

Satu huruf mengubah segalanya

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

Masalahnya ialah json dan jsonb adalah struktur yang sama sekali berbeza. Dalam json, objek disimpan sebagaimana adanya, dan dalam jsonb ia sudah disimpan dalam bentuk struktur yang dihuraikan dan diindeks. Itulah sebabnya dalam kes kedua, nilai objek dengan kunci 1 digantikan daripada [1, 2, 3] kepada [7, 8, 9], yang masuk ke dalam struktur pada bahagian paling akhir dengan kunci yang sama.

Jangan minum air dari muka anda

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

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

PostgreSQL dalam pelaksanaan JSONBnya mengubah pemformatan nombor nyata, membawanya kepada bentuk klasik. Ini tidak berlaku untuk jenis JSON. Agak pelik, tetapi dia betul.

Fail nombor empat. tarikh/masa/cap masa

Terdapat juga beberapa keanehan dengan jenis tarikh/masa. Mari lihat mereka. Izinkan saya membuat tempahan segera supaya beberapa ciri tingkah laku menjadi jelas jika anda memahami dengan baik intipati bekerja dengan zon waktu. Tetapi ini juga topik untuk artikel berasingan.

Milik saya tidak faham

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

Nampaknya apa yang tidak dapat difahami di sini? Tetapi pangkalan data masih tidak memahami apa yang kami letakkan di tempat pertama di sini-tahun atau hari? Dan dia memutuskan bahawa ia adalah 99 Januari 2008, yang mengejutkannya. Secara umumnya, apabila menghantar tarikh dalam format teks, anda perlu menyemak dengan teliti sejauh mana pangkalan data mengecamnya dengan betul (khususnya, menganalisis parameter gaya tarikh dengan arahan SHOW datestyle), kerana kekaburan dalam perkara ini boleh menjadi sangat mahal.

Dari mana anda dapat ini?

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

Mengapa pangkalan data tidak dapat memahami masa yang dinyatakan secara eksplisit? Kerana zon waktu tidak mempunyai singkatan, tetapi nama penuh, yang masuk akal hanya dalam konteks tarikh, kerana ia mengambil kira sejarah perubahan zon waktu, dan ia tidak berfungsi tanpa tarikh. Dan kata-kata garis masa menimbulkan persoalan - apakah sebenarnya yang dimaksudkan oleh pengaturcara? Oleh itu, semuanya logik di sini, jika anda melihatnya.

Apa masalah dia?

Bayangkan keadaannya. Anda mempunyai medan dalam jadual anda dengan taip cap waktu. Anda mahu mengindeksnya. Tetapi anda faham bahawa membina indeks pada medan ini tidak selalu wajar kerana selektivitinya yang tinggi (hampir semua nilai jenis ini akan menjadi unik). Jadi anda memutuskan untuk mengurangkan selektiviti indeks dengan menghantar jenis ke tarikh. Dan anda mendapat kejutan:

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

Apa masalahnya? Hakikatnya ialah untuk menghantar jenis cap waktu kepada jenis tarikh, nilai parameter sistem Zon Waktu digunakan, yang menjadikan fungsi penukaran jenis bergantung pada parameter tersuai, i.e. tidak menentu. Fungsi sedemikian tidak dibenarkan dalam indeks. Dalam kes ini, anda mesti menunjukkan secara eksplisit di zon waktu mana jenis cast dilakukan.

Bila sekarang bukan sekarang pun

Kami sudah biasa now() mengembalikan tarikh/masa semasa, dengan mengambil kira zon waktu. Tetapi lihat pertanyaan berikut:

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;

Tarikh/masa dikembalikan sama tidak kira berapa lama masa telah berlalu sejak permintaan sebelumnya! Apa masalahnya? Hakikatnya ialah now() bukan masa semasa, tetapi masa mula transaksi semasa. Oleh itu, ia tidak berubah dalam transaksi. Sebarang pertanyaan yang dilancarkan di luar skop transaksi dibungkus dalam transaksi secara tersirat, itulah sebabnya kami tidak perasan bahawa masa yang dikembalikan oleh SELECT mudah sekarang(); sebenarnya, bukan yang semasa... Jika anda ingin mendapatkan masa semasa yang jujur, anda perlu menggunakan fungsi clock_timestamp().

Fail nombor lima. sedikit

Pelik sikit

SELECT '111'::bit(4)

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

Bahagian mana yang harus ditambah bit sekiranya sambungan jenis? Ia kelihatan di sebelah kiri. Tetapi hanya pangkalan yang mempunyai pendapat yang berbeza mengenai perkara ini. Berhati-hati: jika bilangan digit tidak sepadan semasa menghantar jenis, anda tidak akan mendapat apa yang anda inginkan. Ini terpakai kepada kedua-dua menambah bit ke kanan dan memotong bit. Juga di sebelah kanan...

Fail nombor enam. Tatasusunan

Malah NULL tidak menyala

SELECT ARRAY[1, 2] || NULL

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

Seperti yang dibangkitkan oleh orang biasa tentang SQL, kami menjangkakan hasil ungkapan ini adalah NULL. Tetapi ia tidak ada di sana. Tatasusunan dikembalikan. kenapa? Kerana dalam kes ini asas membuang NULL kepada tatasusunan integer dan secara tersirat memanggil fungsi array_cat. Tetapi masih tidak jelas mengapa "kucing tatasusunan" ini tidak menetapkan semula tatasusunan. Tingkah laku ini juga hanya perlu diingat.

rumuskan. Terdapat banyak perkara pelik. Sebilangan besar daripada mereka, sudah tentu, tidak begitu kritikal untuk bercakap tentang tingkah laku yang tidak wajar secara terang-terangan. Dan yang lain dijelaskan dengan kemudahan penggunaan atau kekerapan penggunaannya dalam situasi tertentu. Tetapi pada masa yang sama, terdapat banyak kejutan. Oleh itu, anda perlu tahu tentang mereka. Jika anda mendapati apa-apa lagi yang pelik atau luar biasa dalam tingkah laku mana-mana jenis, tulis dalam komen, saya dengan senang hati akan menambah kepada dosiers yang tersedia pada mereka.

Sumber: www.habr.com

Tambah komen