انواع مشکوک

هیچ چیز مشکوکی در ظاهر آنها وجود ندارد. علاوه بر این، آنها حتی برای شما به خوبی و برای مدت طولانی آشنا به نظر می رسند. اما این فقط تا زمانی است که آنها را بررسی کنید. اینجاست که آنها ماهیت موذیانه خود را نشان می دهند و کاملاً متفاوت از آنچه انتظار داشتید کار می کنند. و گاهی اوقات کاری انجام می دهند که موهای شما سیخ می شود - برای مثال، اطلاعات محرمانه ای را که به آنها سپرده شده است از دست می دهند. وقتی با آنها روبه‌رو می‌شوید، ادعا می‌کنند که یکدیگر را نمی‌شناسند، اگرچه در سایه‌ها با پشتکار زیر یک کلاه کار می‌کنند. وقت آن است که بالاخره آنها را به آب تمیز بیاوریم. اجازه دهید به این انواع مشکوک نیز بپردازیم.

تایپ داده در 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، پول یک عدد واقعی نیست. به گفته برخی افراد نیز. باید به خاطر داشته باشیم که ریختن نوع پول فقط به نوع عددی امکان پذیر است، همانطور که فقط نوع عددی را می توان به نوع پولی ریخت. اما اکنون می توانید آن طور که دلتان می خواهد با آن بازی کنید. اما این همان پول نخواهد بود.

نسل کوچک و توالی

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 دوست ندارد وقت خود را با چیزهای بی اهمیت تلف کند. این دنباله ها بر اساس کوچکترین کدامند؟ int، نه کمتر! بنابراین، هنگام تلاش برای اجرای پرس و جوی بالا، پایگاه داده سعی می کند کوچکترین را به یک نوع عدد صحیح دیگر ارسال کند و می بیند که ممکن است چندین ارسال از این قبیل وجود داشته باشد. کدام بازیگران را انتخاب کنیم؟ او نمی تواند در این مورد تصمیم بگیرد، و بنابراین با یک خطا از کار می افتد.

فایل شماره دو "char"/char/varchar/text

تعدادی از موارد عجیب و غریب نیز در انواع شخصیت وجود دارد. بیایید آنها را هم بشناسیم.

اینها چه ترفندهایی هستند؟

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

به مثال ارائه شده دقت کنید. من به طور ویژه تمام نتایج را به نوع بایت تبدیل کردم، به طوری که آنچه در آنجا بود به وضوح قابل مشاهده بود. فضاهای انتهایی پس از ریخته گری به وارچار(6) کجاست؟ در مستندات به اختصار آمده است: "هنگامی که ارزش کاراکتر را به یک نوع کاراکتر دیگر داده می شود، فضای خالی انتهایی کنار گذاشته می شود." این بیزاری را باید به خاطر داشت. و توجه داشته باشید که اگر یک ثابت رشته نقل شده مستقیماً به نوع varchar (6) ریخته شود، فضاهای انتهایی حفظ می شوند. این معجزات هستند.

فایل شماره سه json/jsonb

JSON یک ساختار جداگانه است که زندگی خود را دارد. بنابراین، موجودیت‌های آن و PostgreSQL کمی متفاوت هستند. در اینجا نمونه هایی وجود دارد.

جانسون و جانسون تفاوت را احساس کنید

SELECT 'null'::jsonb IS NULL

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

مسئله این است که JSON موجودیت تهی خود را دارد که آنالوگ NULL در PostgreSQL نیست. در همان زمان، خود شی JSON ممکن است مقدار NULL را داشته باشد، بنابراین عبارت SELECT null::jsonb IS NULL (به عدم وجود نقل قول های تکی توجه کنید) این بار true خواهد شد.

یک حرف همه چیز را تغییر می دهد

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 است، که ذهن او را منفجر می کند. به طور کلی، هنگام انتقال تاریخ ها در قالب متن، باید با دقت بررسی کنید که پایگاه داده چقدر آنها را به درستی تشخیص داده است (به ویژه، پارامتر datestyle را با دستور SHOW 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

چرا پایگاه داده نمی تواند زمان مشخص شده را درک کند؟ زیرا منطقه زمانی دارای مخفف نیست، بلکه یک نام کامل دارد، که فقط در زمینه تاریخ معنا دارد، زیرا تاریخچه تغییرات منطقه زمانی را در نظر می گیرد و بدون تاریخ کار نمی کند. و خود جمله بندی خط زمانی سؤالاتی را ایجاد می کند - منظور برنامه نویس واقعاً چه بوده است؟ بنابراین، اگر به آن نگاه کنید، همه چیز در اینجا منطقی است.

چه بلایی سرش آمده؟

وضعیت را تصور کنید. شما یک فیلد در جدول خود با نوع timestamptz دارید. شما می خواهید آن را ایندکس کنید. اما می دانید که ایجاد یک شاخص در این زمینه به دلیل انتخاب بالای آن همیشه قابل توجیه نیست (تقریباً همه مقادیر از این نوع منحصر به فرد خواهند بود). بنابراین شما تصمیم می گیرید که انتخابی بودن شاخص را با ریختن نوع به یک تاریخ کاهش دهید. و شما یک سورپرایز خواهید داشت:

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

موضوع چیه؟ واقعیت این است که برای فرستادن نوع timestamptz به نوع تاریخ، از مقدار پارامتر سیستم TimeZone استفاده می‌شود که تابع تبدیل نوع را به یک پارامتر سفارشی وابسته می‌کند. فرار. چنین توابعی در ایندکس مجاز نیستند. در این صورت، باید به صراحت مشخص کنید که در کدام منطقه زمانی نوع ریخته گری انجام می شود.

وقتی الان اصلاً الان نیست

ما عادت کرده‌ایم که now() تاریخ/زمان فعلی را با در نظر گرفتن منطقه زمانی برگردانیم. اما به سوالات زیر توجه کنید:

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() زمان فعلی نیست، بلکه زمان شروع تراکنش جاری است. بنابراین، در داخل معامله تغییر نمی کند. هر درخواستی که خارج از محدوده یک تراکنش راه اندازی شود، به طور ضمنی در یک تراکنش پیچیده می شود، به همین دلیل است که ما متوجه نمی شویم که زمان بازگشتی با یک SELECT 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

اضافه کردن نظر