Không có gì đáng nghi ngờ về sự xuất hiện của họ. Hơn nữa, chúng thậm chí còn có vẻ quen thuộc với bạn trong một thời gian dài. Nhưng đó chỉ là cho đến khi bạn kiểm tra chúng. Đây là nơi chúng thể hiện bản chất quỷ quyệt, hoạt động hoàn toàn khác so với những gì bạn mong đợi. Và đôi khi họ làm điều gì đó khiến bạn dựng tóc gáy - chẳng hạn như họ làm mất dữ liệu bí mật được giao phó cho họ. Khi bạn đối đầu với họ, họ tuyên bố rằng họ không biết nhau, mặc dù trong bóng tối họ làm việc chăm chỉ dưới cùng một nhóm. Cuối cùng đã đến lúc đưa chúng đến nơi có nước sạch. Chúng ta cũng hãy đối phó với những loại đáng ngờ này.
Việc gõ dữ liệu trong PostgreSQL, với tất cả logic của nó, đôi khi gây ra những bất ngờ rất kỳ lạ. Trong bài viết này, chúng tôi sẽ cố gắng làm rõ một số điều kỳ quặc của chúng, hiểu lý do dẫn đến hành vi kỳ lạ của chúng và hiểu cách không gặp vấn đề trong thực hành hàng ngày. Thực sự mà nói, tôi biên soạn bài viết này cũng như một loại sách tham khảo cho riêng mình, một cuốn sách tham khảo có thể dễ dàng tham khảo trong những vụ án gây tranh cãi. Vì vậy, nó sẽ được bổ sung khi những bất ngờ mới từ những loại đáng ngờ được phát hiện. Vì vậy, hãy bắt đầu nào, hỡi những người theo dõi cơ sở dữ liệu không biết mệt mỏi!
Hồ sơ số một. độ chính xác thực/kép/số/tiền
Có vẻ như các loại số ít gặp vấn đề nhất về mặt gây bất ngờ trong hành vi. Nhưng dù thế nào đi chăng nữa. Vì vậy, hãy bắt đầu với họ. Vì thế…
Quên cách đếm
SELECT 0.1::real = 0.1
?column?
boolean
---------
f
Có chuyện gì vậy? Vấn đề là PostgreSQL chuyển đổi hằng số chưa được gõ 0.1 thành độ chính xác gấp đôi và cố gắng so sánh nó với 0.1 của loại thực. Và đây là những ý nghĩa hoàn toàn khác nhau! Ý tưởng là biểu diễn số thực trong bộ nhớ máy. Vì 0.1 không thể được biểu diễn dưới dạng phân số nhị phân hữu hạn (nó sẽ là 0.0(0011) ở dạng nhị phân), các số có độ sâu bit khác nhau sẽ khác nhau, do đó kết quả là chúng không bằng nhau. Nói chung đây là chủ đề dành cho một bài viết riêng, tôi sẽ không viết chi tiết hơn ở đây.
Lỗi đến từ đâu?
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
Nhiều người biết rằng PostgreSQL cho phép ký hiệu chức năng để truyền kiểu. Nghĩa là, bạn không chỉ có thể viết 1::int mà còn có thể viết int(1), tương đương. Nhưng không dành cho những loại có tên bao gồm nhiều từ! Do đó, nếu bạn muốn chuyển một giá trị số thành loại có độ chính xác gấp đôi ở dạng hàm, hãy sử dụng bí danh của loại float8 này, nghĩa là SELECT float8(1).
Cái gì lớn hơn vô cực?
SELECT 'Infinity'::double precision < 'NaN'::double precision
?column?
boolean
---------
t
Hãy nhìn xem nó như thế nào! Hóa ra có một thứ còn lớn hơn vô cùng, đó chính là NaN! Đồng thời, tài liệu PostgreSQL nhìn chúng tôi bằng con mắt trung thực và tuyên bố rằng NaN rõ ràng là lớn hơn bất kỳ số nào khác và do đó là vô cùng. Điều ngược lại cũng đúng với -NaN. Xin chào các bạn yêu toán! Nhưng chúng ta phải nhớ rằng tất cả điều này hoạt động trong bối cảnh số thực.
Làm tròn mắt
SELECT round('2.5'::double precision)
, round('2.5'::numeric)
round | round
double precision | numeric
-----------------+---------
2 | 3
Một lời chào bất ngờ khác từ căn cứ. Một lần nữa, hãy nhớ rằng độ chính xác kép và kiểu số có hiệu ứng làm tròn khác nhau. Đối với số - số thông thường, khi 0,5 được làm tròn và đối với độ chính xác kép - 0,5 được làm tròn về số nguyên chẵn gần nhất.
Tiền là thứ đặc biệt
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
Theo PostgreSQL, tiền không phải là con số thực. Theo một số cá nhân cũng vậy. Chúng ta cần nhớ rằng chỉ có thể chuyển kiểu tiền sang loại số, cũng như chỉ có thể chuyển kiểu số sang loại tiền. Nhưng bây giờ bạn có thể chơi với nó như trái tim bạn mong muốn. Nhưng nó sẽ không giống số tiền đó.
Smallint và tạo chuỗi
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 không muốn lãng phí thời gian vào những chuyện vặt vãnh. Những chuỗi này dựa trên Smallint là gì? int, không kém! Do đó, khi cố gắng thực hiện truy vấn trên, cơ sở dữ liệu sẽ cố gắng chuyển Smallint sang một số kiểu số nguyên khác và thấy rằng có thể có một số lần chuyển đổi như vậy. Chọn diễn viên nào? Cô ấy không thể quyết định điều này và do đó gặp lỗi.
Tập tin số hai. "char"/char/varchar/văn bản
Một số điều kỳ lạ cũng hiện diện trong các kiểu nhân vật. Chúng ta hãy làm quen với họ quá.
Đây là những loại thủ đoạn gì?
SELECT 'ПЕТЯ'::"char"
, 'ПЕТЯ'::"char"::bytea
, 'ПЕТЯ'::char
, 'ПЕТЯ'::char::bytea
char | bytea | bpchar | bytea
"char" | bytea | character(1) | bytea
-------+-------+--------------+--------
╨ | xd0 | П | xd09f
Đây là loại "char" gì, đây là loại hề gì? Chúng ta không cần những thứ đó... Bởi vì nó giả vờ là một char bình thường, mặc dù nó ở trong dấu ngoặc kép. Và nó khác với char thông thường, không có dấu ngoặc kép, ở chỗ nó chỉ xuất ra byte đầu tiên của biểu diễn chuỗi, trong khi char bình thường xuất ra ký tự đầu tiên. Trong trường hợp của chúng tôi, ký tự đầu tiên là chữ P, trong biểu diễn unicode chiếm 2 byte, bằng chứng là việc chuyển đổi kết quả sang loại bytetea. Và loại “char” chỉ lấy byte đầu tiên của biểu diễn unicode này. Vậy tại sao lại cần loại này? Tài liệu PostgreSQL nói rằng đây là loại đặc biệt được sử dụng cho các nhu cầu đặc biệt. Vì vậy, chúng tôi khó có thể cần nó. Nhưng hãy nhìn vào mắt anh ấy và bạn sẽ không nhầm lẫn khi bắt gặp anh ấy với cách cư xử đặc biệt.
Không gian bổ sung. Xa mặt cách lòng
SELECT 'abc '::char(6)::bytea
, 'abc '::char(6)::varchar(6)::bytea
, 'abc '::varchar(6)::bytea
bytea | bytea | bytea
bytea | bytea | bytea
---------------+----------+----------------
x616263202020 | x616263 | x616263202020
Hãy xem ví dụ đã cho. Tôi đặc biệt chuyển đổi tất cả các kết quả sang loại bytea để có thể nhìn thấy rõ ràng những gì ở đó. Đâu là khoảng trắng ở cuối sau khi truyền tới varchar(6)? Tài liệu nêu ngắn gọn: "Khi truyền giá trị của ký tự sang loại ký tự khác, khoảng trắng ở cuối sẽ bị loại bỏ." Sự không thích này phải được ghi nhớ. Và lưu ý rằng nếu một hằng chuỗi trích dẫn được truyền trực tiếp sang kiểu varchar(6), thì các khoảng trắng ở cuối sẽ được giữ nguyên. Đó là những điều kỳ diệu.
Tập tin số ba. json/jsonb
JSON là một cấu trúc riêng biệt có cuộc sống riêng. Do đó, các thực thể của nó và của PostgreSQL hơi khác nhau. Dưới đây là những ví dụ.
Johnson và Johnson. cảm nhận sự khác biệt
SELECT 'null'::jsonb IS NULL
?column?
boolean
---------
f
Vấn đề là JSON có thực thể null riêng, không giống với NULL trong PostgreSQL. Đồng thời, bản thân đối tượng JSON cũng có thể có giá trị NULL, do đó, biểu thức SELECT null::jsonb IS NULL (lưu ý rằng không có dấu ngoặc đơn) sẽ trả về true lần này.
Một chữ cái thay đổi mọi thứ
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]}
Vấn đề là json và jsonb là những cấu trúc hoàn toàn khác nhau. Trong json, đối tượng được lưu trữ nguyên trạng và trong jsonb, nó đã được lưu trữ ở dạng cấu trúc được lập chỉ mục, phân tích cú pháp. Đó là lý do tại sao trong trường hợp thứ hai, giá trị của đối tượng bằng khóa 1 được thay thế từ [1, 2, 3] thành [7, 8, 9], nằm trong cấu trúc ở cuối cùng với cùng một khóa.
Đừng uống nước từ mặt bạn
SELECT '{"reading": 1.230e-5}'::jsonb
, '{"reading": 1.230e-5}'::json
jsonb | json
jsonb | json
------------------------+----------------------
{"reading": 0.00001230} | {"reading": 1.230e-5}
PostgreSQL trong quá trình triển khai JSONB của nó sẽ thay đổi định dạng của số thực, đưa chúng về dạng cổ điển. Điều này không xảy ra đối với loại JSON. Hơi lạ một chút, nhưng anh ấy nói đúng.
Tập tin số bốn. ngày/giờ/dấu thời gian
Ngoài ra còn có một số điều kỳ lạ với loại ngày/giờ. Hãy nhìn vào chúng. Hãy để tôi đặt trước ngay rằng một số đặc điểm hành vi sẽ trở nên rõ ràng nếu bạn hiểu rõ bản chất của việc làm việc với các múi giờ. Nhưng đây cũng là một chủ đề cho một bài viết riêng biệt.
Tôi không hiểu bạn
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
Có vẻ như có điều gì khó hiểu ở đây? Nhưng cơ sở dữ liệu vẫn không hiểu chúng ta đặt gì ở vị trí đầu tiên ở đây—năm hay ngày? Và cô ấy quyết định rằng đó là ngày 99 tháng 2008 năm XNUMX, điều này khiến cô ấy choáng váng. Nói chung, khi truyền ngày tháng ở định dạng văn bản, bạn cần kiểm tra thật cẩn thận xem cơ sở dữ liệu đã nhận dạng chúng chính xác đến mức nào (đặc biệt, phân tích tham số datestyle bằng lệnh SHOW datestyle), vì sự mơ hồ trong vấn đề này có thể rất tốn kém.
Bạn từ đâu đến?
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
Tại sao cơ sở dữ liệu không thể hiểu được thời gian được chỉ định rõ ràng? Bởi vì múi giờ không có chữ viết tắt mà là tên đầy đủ, điều này chỉ có ý nghĩa trong bối cảnh ngày tháng, vì nó tính đến lịch sử thay đổi múi giờ và nó không hoạt động nếu không có ngày. Và chính cách diễn đạt của dòng thời gian đã đặt ra câu hỏi - ý của người lập trình viên thực sự là gì? Vì vậy, mọi thứ ở đây đều hợp lý nếu bạn nhìn vào nó.
Có chuyện gì với anh ấy vậy?
Hãy tưởng tượng tình huống này. Bạn có một trường trong bảng có loại timestamptz. Bạn muốn lập chỉ mục nó. Nhưng bạn hiểu rằng việc xây dựng chỉ mục trên trường này không phải lúc nào cũng hợp lý do tính chọn lọc cao của nó (hầu như tất cả các giá trị thuộc loại này sẽ là duy nhất). Vì vậy, bạn quyết định giảm tính chọn lọc của chỉ mục bằng cách chuyển loại thành một ngày. Và bạn nhận được một điều ngạc nhiên:
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
Có chuyện gì vậy? Thực tế là để chuyển loại dấu thời gian thành loại ngày, giá trị của tham số hệ thống TimeZone được sử dụng, điều này làm cho hàm chuyển đổi loại phụ thuộc vào tham số tùy chỉnh, tức là. bay hơi. Những chức năng như vậy không được phép trong chỉ mục. Trong trường hợp này, bạn phải chỉ ra rõ ràng việc truyền kiểu được thực hiện ở múi giờ nào.
Khi bây giờ thậm chí không phải là bây giờ
Chúng ta đã quen với việc now() trả về ngày/giờ hiện tại, có tính đến múi giờ. Nhưng hãy nhìn vào các truy vấn sau:
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;
Ngày/giờ được trả về giống nhau cho dù thời gian đã trôi qua bao lâu kể từ yêu cầu trước đó! Có chuyện gì vậy? Thực tế là now() không phải là thời điểm hiện tại mà là thời gian bắt đầu của giao dịch hiện tại. Do đó, nó không thay đổi trong giao dịch. Bất kỳ truy vấn nào được đưa ra ngoài phạm vi của một giao dịch đều được bao bọc hoàn toàn trong một giao dịch, đó là lý do tại sao chúng tôi không nhận thấy rằng thời gian được trả về bởi một lệnh SELECT now() đơn giản; trên thực tế, không phải thời gian hiện tại... Nếu bạn muốn có được thời gian hiện tại trung thực, bạn cần sử dụng hàm clock_timestamp().
Tập tin số năm. chút
Lạ lùng một chút
SELECT '111'::bit(4)
bit
bit(4)
------
1110
Các bit nên được thêm vào bên nào trong trường hợp mở rộng loại? Có vẻ như nó ở bên trái. Nhưng chỉ có cơ sở mới có ý kiến khác về vấn đề này. Hãy cẩn thận: nếu số chữ số không khớp khi truyền một loại, bạn sẽ không nhận được thứ mình muốn. Điều này áp dụng cho cả việc thêm bit vào bên phải và cắt bớt bit. Ngoài ra ở bên phải...
Tập tin số sáu. Mảng
Ngay cả NULL cũng không kích hoạt
SELECT ARRAY[1, 2] || NULL
?column?
integer[]
---------
{1,2}
Như những người bình thường đã học về SQL, chúng tôi mong đợi kết quả của biểu thức này là NULL. Nhưng nó không có ở đó. Một mảng được trả về. Tại sao? Bởi vì trong trường hợp này, cơ sở chuyển NULL thành một mảng số nguyên và gọi ngầm hàm array_cat. Nhưng vẫn chưa rõ vì sao “mèo mảng” này không reset mảng. Hành vi này cũng chỉ cần được ghi nhớ.
Tóm tắt. Có rất nhiều điều kỳ lạ. Tất nhiên, hầu hết họ đều không phê phán đến mức nói về hành vi không phù hợp một cách trắng trợn. Và những thứ khác được giải thích bằng tính dễ sử dụng hoặc tần suất áp dụng của chúng trong những tình huống nhất định. Nhưng đồng thời, có rất nhiều điều bất ngờ. Vì vậy, bạn cần phải biết về họ. Nếu bạn tìm thấy bất kỳ điều gì kỳ lạ hoặc bất thường trong hành vi của bất kỳ loại nào, hãy viết vào phần bình luận, tôi sẽ vui lòng bổ sung vào hồ sơ có sẵn về chúng.
Nguồn: www.habr.com