ประเภทที่น่าสงสัย

ไม่มีอะไรน่าสงสัยเกี่ยวกับรูปร่างหน้าตาของพวกเขา ยิ่งไปกว่านั้น พวกเขายังดูเหมือนคุ้นเคยกับคุณเป็นอย่างดีและเป็นเวลานานอีกด้วย แต่นั่นก็จนกว่าคุณจะตรวจสอบเท่านั้น นี่คือที่ที่พวกเขาแสดงธรรมชาติที่ร้ายกาจ โดยทำงานแตกต่างไปจากที่คุณคาดไว้อย่างสิ้นเชิง และบางครั้งพวกเขาก็ทำบางอย่างที่ทำให้ผมของคุณยืนหยัด เช่น พวกเขาสูญเสียข้อมูลลับที่ได้รับความไว้วางใจ เมื่อคุณเผชิญหน้ากับพวกเขา พวกเขาอ้างว่าพวกเขาไม่รู้จักกัน แม้ว่าจะอยู่ในเงามืดพวกเขาก็ทำงานหนักภายใต้หน้ากากเดียวกันก็ตาม ถึงเวลาที่ต้องนำพวกมันลงน้ำสะอาดในที่สุด ให้เราจัดการกับประเภทที่น่าสงสัยเหล่านี้ด้วย

การพิมพ์ข้อมูลใน 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 ไปเป็นจำนวนเต็มประเภทอื่น และพบว่าอาจมีการแคสต์ดังกล่าวหลายครั้ง เลือกนักแสดงคนไหน? เธอไม่สามารถตัดสินใจเรื่องนี้ได้ จึงเกิดข้อผิดพลาดขึ้น

ไฟล์หมายเลขสอง "ถ่าน"/ถ่าน/varchar/ข้อความ

มีสิ่งแปลกประหลาดจำนวนหนึ่งปรากฏอยู่ในประเภทอักขระด้วย มาทำความรู้จักกับพวกเขากันด้วย

เทคนิคเหล่านี้คืออะไร?

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

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

นี่คือ "ถ่าน" แบบไหน ตัวตลกแบบไหน? เราไม่ต้องการสิ่งเหล่านั้น... เพราะมันแกล้งทำเป็นถ่านธรรมดาถึงแม้ว่ามันจะอยู่ในเครื่องหมายคำพูดก็ตาม และมันแตกต่างจากถ่านทั่วไปซึ่งไม่มีเครื่องหมายคำพูดตรงที่มันจะส่งออกเฉพาะไบต์แรกของการแสดงสตริง ในขณะที่ถ่านปกติจะส่งออกอักขระตัวแรก ในกรณีของเรา อักขระตัวแรกคือตัวอักษร P ซึ่งในการแทนค่า Unicode จะใช้พื้นที่ 2 ไบต์ ซึ่งเห็นได้จากการแปลงผลลัพธ์เป็นประเภท bytea และประเภท "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

ลองดูตัวอย่างที่ให้มา ฉันแปลงผลลัพธ์ทั้งหมดเป็นประเภท bytea โดยเฉพาะเพื่อให้มองเห็นได้ชัดเจนว่ามีอะไรอยู่บ้าง ช่องว่างต่อท้ายหลังจากส่งไปที่ 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 ซึ่งทำให้เธอทึ่ง โดยทั่วไปแล้ว เมื่อส่งวันที่ในรูปแบบข้อความ คุณจะต้องตรวจสอบอย่างรอบคอบว่าฐานข้อมูลจดจำข้อมูลเหล่านั้นได้อย่างถูกต้องเพียงใด (โดยเฉพาะอย่างยิ่ง วิเคราะห์พารามิเตอร์ 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

เกิดอะไรขึ้น? ความจริงก็คือในการแปลงประเภทการประทับเวลาเป็นประเภทวันที่ จะใช้ค่าของพารามิเตอร์ระบบ 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 โดยปริยาย แต่ก็ยังไม่มีความชัดเจนว่าทำไม "array cat" นี้จึงไม่รีเซ็ตอาเรย์ พฤติกรรมนี้ก็ต้องจดจำเช่นกัน

สรุป. มีของแปลกมากมาย แน่นอนว่าส่วนใหญ่ไม่ได้มีความสำคัญอย่างยิ่งในการพูดถึงพฤติกรรมที่ไม่เหมาะสมอย่างโจ่งแจ้ง และอื่นๆ อธิบายได้จากความง่ายในการใช้งานหรือความถี่ของการนำไปใช้ในบางสถานการณ์ แต่ในขณะเดียวกันก็มีเรื่องเซอร์ไพรส์มากมาย ดังนั้นคุณจำเป็นต้องรู้เกี่ยวกับพวกเขา หากคุณพบสิ่งอื่นที่แปลกหรือผิดปกติในพฤติกรรมทุกประเภท โปรดเขียนความคิดเห็น ฉันยินดีที่จะเพิ่มลงในเอกสารที่มีอยู่

ที่มา: will.com

เพิ่มความคิดเห็น