أنواع مشبوهة

لا يوجد شيء مريب حول مظهرهم. علاوة على ذلك، يبدو أنها مألوفة لك جيدا ولفترة طويلة. ولكن هذا فقط حتى يتم التحقق منها. هذا هو المكان الذي يظهرون فيه طبيعتهم الخبيثة، ويعملون بشكل مختلف تمامًا عما كنت تتوقعه. وأحيانًا يقومون بشيء يجعل شعرك يقف على نهايته - على سبيل المثال، يفقدون البيانات السرية الموكلة إليهم. عندما تواجههم، يزعمون أنهم لا يعرفون بعضهم البعض، على الرغم من أنهم يعملون بجد في الظل تحت نفس الغطاء. لقد حان الوقت لإحضارهم أخيرًا إلى المياه النظيفة. دعونا نتعامل أيضًا مع هذه الأنواع المشبوهة.

كتابة البيانات في 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؟ كثافة العمليات، لا أقل! لذلك، عند محاولة تنفيذ الاستعلام أعلاه، تحاول قاعدة البيانات تحويل الخط الصغير إلى نوع صحيح آخر، وترى أنه قد يكون هناك العديد من هذه القوالب. أي طاقم تختار؟ لا يمكنها أن تقرر هذا، وبالتالي تتعطل بسبب خطأ.

الملف رقم اثنين "شار"/شار/فارتشار/نص

يوجد أيضًا عدد من الشذوذات في أنواع الشخصيات. دعونا نتعرف عليهم أيضا.

أي نوع من الحيل هذه؟

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

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

أي نوع من "شار" هذا، أي نوع من المهرج هذا؟ نحن لسنا بحاجة إلى تلك... لأنها تبدو وكأنها حرف عادي، على الرغم من أنها بين علامتي اقتباس. وهو يختلف عن الحرف العادي، الذي لا يحتوي على علامات اقتباس، حيث أنه يُخرج فقط البايت الأول من تمثيل السلسلة، بينما يُخرج الحرف العادي الحرف الأول. في حالتنا، الحرف الأول هو الحرف P، والذي في تمثيل Unicode يستهلك 2 بايت، كما يتضح من تحويل النتيجة إلى نوع bytea. ويأخذ النوع "char" البايت الأول فقط من تمثيل Unicode هذا. ثم لماذا هذا النوع مطلوب؟ تقول وثائق 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 لديه كيان فارغ خاص به، وهو ليس مماثلاً لـ NULL في PostgreSQL. في الوقت نفسه، قد يكون لكائن 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 هو الذي أذهلها. بشكل عام، عند إرسال التواريخ بتنسيق نصي، تحتاج إلى التحقق بعناية شديدة من مدى صحة التعرف عليها من قبل قاعدة البيانات (على وجه الخصوص، تحليل معلمة نمط التاريخ باستخدام أمر إظهار نمط التاريخ)، لأن الغموض في هذه المسألة يمكن أن يكون مكلفًا للغاية.

من أين أتيت؟

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

ماذا جرى؟ الحقيقة هي أنه لإرسال نوع طابع زمني إلى نوع تاريخ، يتم استخدام قيمة معلمة نظام 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;

يتم إرجاع التاريخ/الوقت كما هو بغض النظر عن مقدار الوقت الذي انقضى منذ الطلب السابق! ماذا جرى؟ الحقيقة هي أن الآن () ليس الوقت الحالي، ولكن وقت بدء المعاملة الحالية. ولذلك، فإنه لا يتغير داخل الصفقة. أي استعلام يتم إطلاقه خارج نطاق المعاملة يتم تضمينه في معاملة ضمنيًا، ولهذا السبب لا نلاحظ أن الوقت الذي يتم إرجاعه بواسطة عملية 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

إضافة تعليق