Şüpheli tipler

Görünüşlerinde şüphe uyandıracak hiçbir şey yok. Üstelik size çok iyi ve uzun süredir tanıdık geliyorlar. Ama bu sadece onları kontrol edene kadar. Burası, beklediğinizden tamamen farklı bir şekilde çalışarak sinsi doğalarını gösterdikleri yerdir. Bazen tüylerinizi diken diken edecek şeyler yaparlar; örneğin kendilerine emanet edilen gizli verileri kaybederler. Karşılarına çıktığınızda birbirlerini tanımadıklarını iddia ediyorlar ama aynı kukuleta altında gölgede çok çalışıyorlar. Sonunda onları temiz suya getirmenin zamanı geldi. Bu şüpheli tiplerle de ilgilenelim.

PostgreSQL'de veri yazmak, tüm mantığına rağmen bazen çok tuhaf sürprizler ortaya çıkarabiliyor. Bu makalede onların bazı tuhaflıklarını açıklığa kavuşturmaya, tuhaf davranışlarının nedenini anlamaya ve günlük uygulamalarda sorunlarla nasıl karşılaşılmayacağını anlamaya çalışacağız. Doğrusunu söylemek gerekirse bu makaleyi aynı zamanda kendim için bir nevi başvuru kitabı, ihtilaflı durumlarda kolaylıkla başvurulabilecek bir referans kitabı olarak da derledim. Bu nedenle şüpheli türlerden yeni sürprizler keşfedildikçe yenilenecektir. O halde haydi gidelim, ah yorulmak bilmez veritabanı takipçileri!

Bir numaralı dosya. gerçek/çift duyarlık/sayısal/para

Davranıştaki sürprizler açısından sayısal türlerin en az sorunlu olduğu görülüyor. Ama nasıl olursa olsun. Öyleyse onlarla başlayalım. Bu yüzden…

Nasıl sayılacağını unuttum

SELECT 0.1::real = 0.1

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

Sorun ne? Sorun, PostgreSQL'in türlenmemiş sabit 0.1'i çift duyarlıklıya dönüştürmesi ve onu gerçek türdeki 0.1 ile karşılaştırmaya çalışmasıdır. Ve bunlar tamamen farklı anlamlar! Buradaki fikir, makine hafızasındaki gerçek sayıları temsil etmektir. 0.1, sonlu bir ikili kesir olarak temsil edilemediğinden (ikili olarak 0.0(0011) olacaktır), farklı bit derinliklerine sahip sayılar farklı olacaktır, dolayısıyla eşit olmadıkları sonucuna varılacaktır. Genel olarak bu ayrı bir yazı konusu, burada daha detaylı yazmayacağım.

Hata nereden geliyor?

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

Birçok kişi PostgreSQL'in tip dökümü için fonksiyonel gösterime izin verdiğini biliyor. Yani sadece 1::int değil aynı zamanda eşdeğer olacak int(1) de yazabilirsiniz. Ancak isimleri birkaç kelimeden oluşan türler için değil! Bu nedenle, işlevsel biçimde çift duyarlıklı türe sayısal bir değer atamak istiyorsanız, bu float8 türünün takma adını, yani SELECT float8(1)'i kullanın.

Sonsuzluktan daha büyük ne var?

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

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

Bakın nasıl bir şey! Sonsuzluktan daha büyük bir şeyin olduğu ortaya çıktı ve bu NaN! Aynı zamanda PostgreSQL belgeleri bize dürüst gözlerle bakıyor ve NaN'nin diğer tüm sayılardan açıkça daha büyük olduğunu ve dolayısıyla sonsuz olduğunu iddia ediyor. -NaN için de bunun tersi geçerlidir. Merhaba matematik severler! Ancak tüm bunların reel sayılar bağlamında işlediğini unutmamalıyız.

Göz yuvarlama

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

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

Üstekten beklenmedik bir selamlama daha. Yine çift duyarlıklı ve sayısal türlerin farklı yuvarlama etkilerine sahip olduğunu unutmayın. Sayısal için - olağan olanı, 0,5 yukarı yuvarlandığında ve çift kesinlik için - 0,5 en yakın çift tam sayıya doğru yuvarlanır.

Para özel 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'e göre para gerçek bir sayı değildir. Bazı kişilere göre de. Para türüne yalnızca sayısal türün atanabileceği gibi, para türünün de yalnızca sayısal türe dönüştürülebileceğini unutmamamız gerekir. Ama artık onunla kalbinizin istediği gibi oynayabilirsiniz. Ama aynı para olmayacak.

Smallint ve dizi üretimi

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 önemsiz şeylerle zaman kaybetmeyi sevmez. Smallint'e dayalı bu diziler nelerdir? int, daha az değil! Bu nedenle, yukarıdaki sorguyu yürütmeye çalışırken, veritabanı, Smallint'i başka bir tamsayı türüne dönüştürmeye çalışır ve bu türden birkaç dönüştürmenin olabileceğini görür. Hangi oyuncu kadrosunu seçmeli? Buna karar veremiyor ve bu nedenle bir hatayla çöküyor.

İki numaralı dosya. "char"/char/varchar/metin

Karakter türlerinde de bir takım tuhaflıklar mevcut. Onları da tanıyalım.

Bunlar ne tür hileler?

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

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

Bu nasıl bir "karakter", nasıl bir palyaço? Bunlara ihtiyacımız yok... Çünkü tırnak içinde olmasına rağmen sıradan bir karaktermiş gibi davranıyor. Ve tırnak işaretleri olmayan normal bir karakterden farklıdır; çünkü normal bir karakter ilk karakteri verirken, dize gösteriminin yalnızca ilk baytını çıkarır. Bizim durumumuzda ilk karakter, sonucun bytea türüne dönüştürülmesiyle kanıtlandığı gibi, unicode gösteriminde 2 bayt kaplayan P harfidir. Ve "char" türü bu unicode gösterimin yalnızca ilk baytını alır. O halde neden bu türe ihtiyaç duyuldu? PostgreSQL belgeleri bunun özel ihtiyaçlar için kullanılan özel bir tür olduğunu söylüyor. Dolayısıyla buna ihtiyacımız olması pek mümkün değil. Ama gözlerinin içine baktığınızda, onun özel davranışlarıyla karşılaştığınızda yanılmayacaksınız.

Ekstra boşluklar. Gözden ırak olan gönülden de ırak olur

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

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

Verilen örneğe bir göz atın. Tüm sonuçları özel olarak bytea türüne dönüştürdüm, böylece orada ne olduğu açıkça görülebilsin. Varchar(6)'ya aktarıldıktan sonra arkadaki boşluklar nerede? Belgeler kısa ve öz bir şekilde şunu belirtiyor: "Karakterin değeri başka bir karakter türüne aktarılırken, sondaki boşluklar atılır." Bu hoşnutsuzluğun hatırlanması gerekiyor. Ve eğer alıntılanan bir dize sabiti doğrudan varchar(6) türüne dönüştürülürse, sondaki boşlukların korunduğunu unutmayın. Mucizeler bunlar.

Üç numaralı dosya. json/jsonb

JSON kendi hayatını yaşayan ayrı bir yapıdır. Bu nedenle, onun varlıkları ile PostgreSQL'in varlıkları biraz farklıdır. İşte örnekler.

Johnson ve Johnson. farkı Hisset

SELECT 'null'::jsonb IS NULL

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

Mesele şu ki, JSON'un PostgreSQL'deki NULL'un benzeri olmayan kendi boş varlığı vardır. Aynı zamanda, JSON nesnesinin kendisi de NULL değerine sahip olabilir, bu nedenle SELECT null::jsonb IS NULL ifadesi (tek tırnakların olmadığına dikkat edin) bu sefer true değerini döndürecektir.

Bir harf her şeyi değiştirir

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

Mesele şu ki, json ve jsonb tamamen farklı yapılardır. Json'da nesne olduğu gibi depolanır ve jsonb'de zaten ayrıştırılmış, indekslenmiş bir yapı biçiminde depolanır. Bu nedenle ikinci durumda, nesnenin 1 anahtarındaki değeri [1, 2, 3] yerine aynı anahtarla yapıya en sonunda gelen [7, 8, 9] olarak değiştirildi.

Yüzünüzden su içmeyin

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

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

PostgreSQL, JSONB uygulamasında gerçek sayıların formatını değiştirerek onları klasik forma getirir. JSON türü için bu durum söz konusu değildir. Biraz tuhaf ama haklı.

Dört numaralı dosya. tarih/saat/zaman damgası

Tarih/saat türlerinde de bazı tuhaflıklar vardır. Şimdi onlara bakalım. Hemen bir rezervasyon yapayım ki, zaman dilimleriyle çalışmanın özünü iyi anlarsanız bazı davranışsal özellikler netleşir. Ancak bu aynı zamanda ayrı bir makalenin konusu.

benim anlamıyorum seninkini

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

Görünüşe göre burada anlaşılmaz olan ne? Ancak veri tabanı burada neyi ilk sıraya koyduğumuzu hala anlamıyor; yıl mı yoksa gün mü? Ve 99 Ocak 2008'in aklını başından aldığına karar verir. Genel olarak konuşursak, tarihleri ​​​​metin biçiminde iletirken, veritabanının bunları ne kadar doğru tanıdığını çok dikkatli bir şekilde kontrol etmeniz gerekir (özellikle, bu konudaki belirsizlikler çok pahalı olabileceğinden, datestyle parametresini SHOW datestyle komutuyla analiz edin).

Nereden geldin?

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

Veritabanı neden açıkça belirtilen zamanı anlayamıyor? Çünkü saat diliminin bir kısaltması değil, tam adı vardır, bu yalnızca tarih bağlamında anlamlıdır, çünkü saat dilimi değişikliklerinin geçmişini dikkate alır ve tarih olmadan çalışmaz. Ve zaman çizelgesinin ifadesi şu soruları gündeme getiriyor: Programcı gerçekte ne demek istedi? Dolayısıyla burada bakarsanız her şey mantıklı.

O'nun nesi var?

Durumu hayal edin. Tablonuzda timestamptz türünde bir alanınız var. Bunu indekslemek istiyorsunuz. Ancak, yüksek seçiciliği nedeniyle bu alanda bir dizin oluşturmanın her zaman haklı olmadığını anlıyorsunuz (bu türdeki hemen hemen tüm değerler benzersiz olacaktır). Böylece türü bir tarihe çevirerek endeksin seçiciliğini azaltmaya karar verirsiniz. Ve bir sürprizle karşılaşacaksı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

Sorun ne? Gerçek şu ki, bir zaman damgası türünü bir tarih türüne dönüştürmek için TimeZone sistem parametresinin değeri kullanılır; bu, tür dönüştürme işlevini özel bir parametreye bağımlı hale getirir; uçucu. Dizinde bu tür işlevlere izin verilmez. Bu durumda tür değişikliğinin hangi saat diliminde gerçekleştirildiğini açıkça belirtmeniz gerekir.

Şimdi şimdi bile değilken

Now() işlevinin, saat dilimini dikkate alarak geçerli tarih/saati döndürmesine alışkınız. Ancak aşağıdaki sorgulara bakı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;

Önceki istekten bu yana ne kadar zaman geçerse geçsin tarih/saat aynı şekilde döndürülür! Sorun ne? Gerçek şu ki now() geçerli zaman değil, geçerli işlemin başlangıç ​​zamanıdır. Dolayısıyla işlem içerisinde değişmez. Bir işlemin kapsamı dışında başlatılan herhangi bir sorgu, örtülü olarak bir işleme sarılır; bu nedenle, basit bir SELECT now(); tarafından döndürülen zamanın döndürüldüğünü fark etmeyiz. aslında şu anki değil... Eğer dürüst bir şimdiki zaman elde etmek istiyorsanız, watch_timestamp() fonksiyonunu kullanmanız gerekir.

Beş numaralı dosya. biraz

Biraz tuhaf

SELECT '111'::bit(4)

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

Tip uzatması durumunda bitler hangi tarafa eklenmelidir? Solda görünüyor. Ancak bu konuda sadece tabanın farklı görüşü var. Dikkatli olun: Bir türü belirlerken basamak sayısı eşleşmiyorsa istediğinizi elde edemezsiniz. Bu, hem sağa bit ekleme hem de bitleri kırpma için geçerlidir. Ayrıca sağda...

Altı numaralı dosya. Diziler

NULL bile ateşlenmedi

SELECT ARRAY[1, 2] || NULL

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

SQL'de yetişmiş normal insanlar olarak bu ifadenin sonucunun NULL olmasını bekliyoruz. Ama orada değildi. Bir dizi döndürülür. Neden? Çünkü bu durumda taban NULL'u bir tamsayı dizisine dönüştürür ve dolaylı olarak array_cat işlevini çağırır. Ancak bu "dizi kedisinin" neden diziyi sıfırlamadığı hala belirsizliğini koruyor. Bu davranışın da hatırlanması gerekiyor.

Özetle. Pek çok tuhaf şey var. Elbette bunların çoğu, açıkça uygunsuz davranışlardan bahsedecek kadar eleştirel değil. Diğerleri ise kullanım kolaylığı veya belirli durumlarda uygulanabilirlik sıklığı ile açıklanmaktadır. Ama aynı zamanda pek çok sürpriz de var. Bu nedenle onlar hakkında bilgi sahibi olmanız gerekir. Herhangi bir tür davranışta tuhaf veya olağandışı bir şey bulursanız, yorumlarınıza yazın, bunlarla ilgili mevcut dosyalara eklemekten memnuniyet duyarım.

Kaynak: habr.com

Yorum ekle