Kiểu JSON và JSONB trong PostgreSQL
Xem thửTình huống mở đầu
Ứng dụng bán hàng của Tạp Hóa Cô Tấm gửi mỗi đơn dưới dạng một tài liệu JSON: mã khách, danh sách mặt hàng, trạng thái đã thanh toán. Cô Tấm lưu thẳng vào một cột và muốn hỏi: đơn nào của khách có mã lớn hơn 9? Cô gõ WHERE payload->>'user_id' > '9' và nhận về 0 dòng, dù bảng có khách mã 10, 11 và 12. Cô thử tìm đơn có mã hàng A1 thì chậm hẳn khi bảng lớn lên.
Hãy đoán trước: vì sao 10 không lớn hơn 9 trong truy vấn đó, và cần đổi gì để tìm trong JSON nhanh?
Ôn nhanh bài trước: cột timestamptz lưu những gì khi bạn nhập '2026-09-14 09:00+07'?
Đáp án: một mốc UTC duy nhất là 02:00 ngày 14, không lưu tên múi giờ.
Mục tiêu bài học
Sau bài này bạn có thể:
- Phân biệt
jsonvàjsonbvề cách lưu, so sánh bằng và chỉ mục, rồi chọn kiểu cho một cột. - Dự đoán kiểu và giá trị của
->,->>và#>>, kể cả với JSON null và khóa không tồn tại. - Dùng
@>và?để tìm trongjsonb, và nhận ra bẫy so sánh chữ thay vì số. - Giải thích chỉ mục GIN và chỉ mục biểu thức giúp truy vấn nào và không giúp truy vấn nào.
Bức tranh lớn
Xuyên suốt bài, json là bản photocopy của phiếu giỏ hàng viết tay: giữ nguyên nét chữ, khoảng trắng, cả chỗ viết trùng. jsonb là phiếu đã được ghi vào ngăn tủ có nhãn, nhanh tìm nhưng đã được sắp lại gọn gàng. Dự án xuyên suốt là bảng orders của tiệm trong database shop, với cột payload chứa giỏ hàng.
Hình dưới trả lời một câu hỏi: khi nào chọn json, khi nào chọn jsonb?
Đang tải sơ đồ…
Cách đọc hình: đọc từ trên xuống. Ô xanh dương là tài liệu cần lưu. Hai ô cam là hai câu hỏi liên tiếp. Ô xanh lá là lựa chọn mặc định, ô tím là trường hợp hiếm cần chữ gốc. Hai nhánh cuối cùng đều dẫn về jsonb, trừ khi bạn thật sự cần nguyên văn.
Chỗ ẩn dụ không còn đúng: tủ ngăn kéo thật giữ nguyên tờ phiếu. jsonb không giữ lại tờ gốc, nó giữ dạng đã phân tích.
Khái niệm cốt lõi
Bài này có bốn khái niệm.
1. json giữ chữ, jsonb phân tích
json kiểm tra cú pháp rồi lưu nguyên văn chuỗi, kể cả dấu cách, thứ tự khóa và khóa lặp. jsonb phân tích chuỗi thành dạng nhị phân: bỏ dấu cách thừa, sắp lại khóa, giữ giá trị cuối khi khóa lặp, và chuẩn hóa số như 1e2 thành 100. jsonb có toán tử bằng và chỉ mục, json thì không.
2. Toán tử truy cập
-> lấy một phần tử và trả về cùng kiểu JSON (jsonb). ->> trả về text. #>> đi theo đường dẫn và trả về text. Khóa không tồn tại cho NULL của SQL. Khóa có giá trị null của JSON lại cho một giá trị JSON null, không phải NULL của SQL; chỉ ->> mới đổi nó thành NULL của SQL.
3. Chứa và tồn tại
@> hỏi: vế trái có chứa vế phải không, kể cả lồng nhau trong mảng. ? hỏi: khóa này có tồn tại ở tầng ngoài cùng không. Cả hai chỉ có ở jsonb và dùng được chỉ mục GIN.
4. Chỉ mục
Chỉ mục GIN trên cột jsonb giúp @>, ?, ?| và ?&. Nó không giúp so sánh payload->>'user_id' = '42'; việc này cần chỉ mục biểu thức B-tree trên chính biểu thức đó. Và chỉ mục biểu thức chỉ khớp khi câu truy vấn viết giống hệt biểu thức: thêm ::int là mất.
Ví dụ đầu tiên
Ví dụ nhỏ nhất: cùng một chuỗi JSON vào hai kiểu.
SELECT '{"b": 1, "a": 2, "a": 3}'::json AS as_json,
'{"b": 1, "a": 2, "a": 3}'::jsonb AS as_jsonb;
+----------------------------+------------------+
| as_json | as_jsonb |
+----------------------------+------------------+
| {"b": 1, "a": 2, "a": 3} | {"a": 3, "b": 1} |
+----------------------------+------------------+
(1 row)
Dự đoán trước khi xem kết quả: cột nào giữ ba dấu cách, và khóa a lặp hai lần còn lại giá trị nào?
Kết quả [Đã chạy: PostgreSQL 16.15, bật \pset border 2]: json giữ nguyên chuỗi, kể cả ba dấu cách và hai khóa a. jsonb ra {"a": 3, "b": 1}: sắp lại khóa, bỏ dấu cách, giữ a cuối cùng.
SELECT '{"price": 1.10, "n": 1e2}'::json AS as_json,
'{"price": 1.10, "n": 1e2}'::jsonb AS as_jsonb;
+---------------------------+---------------------------+
| as_json | as_jsonb |
+---------------------------+---------------------------+
| {"price": 1.10, "n": 1e2} | {"n": 100, "price": 1.10} |
+---------------------------+---------------------------+
(1 row)
Số cũng được chuẩn hóa: 1e2 thành 100 ở jsonb, còn 1.10 giữ nguyên hai chữ số lẻ. Cách so sánh bằng cũng khác nhau:
SELECT '{"a": 1, "b": 2}'::jsonb = '{"b": 2, "a": 1}'::jsonb AS same_value;
+------------+
| same_value |
+------------+
| t |
+------------+
(1 row)
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
'{"b": 1, "a": 2, "a": 3}'::json | Kiểm tra cú pháp và giữ nguyên chuỗi | json là bản sao chép chữ gốc |
'...'::jsonb | Phân tích thành cấu trúc nhị phân | Cho phép so sánh, chỉ mục, truy vấn nhanh |
"a": 2, "a": 3 | Hai lần khóa a | Cố ý để thấy jsonb giữ giá trị cuối |
'{"a": 1, "b": 2}'::jsonb = '{"b": 2, "a": 1}'::jsonb | So hai tài liệu cùng nội dung khác thứ tự | jsonb so theo giá trị nên ra t |
Bây giờ đến bảng orders với cột jsonb giữ giỏ hàng của ba khách.
CREATE TABLE orders (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
payload jsonb NOT NULL
);
INSERT INTO orders (payload) VALUES
('{"user_id": 10, "items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}],
"paid": true, "note": null}'),
('{"user_id": 11, "items": [{"sku": "A1", "qty": 5}], "paid": false}'),
('{"user_id": 12, "items": [], "paid": true, "coupon": "SAVE5"}');
SELECT id, payload FROM orders ORDER BY id;
+----+----------------------------------------------------------------------------------------------------------+
| id | payload |
+----+----------------------------------------------------------------------------------------------------------+
| 1 | {"note": null, "paid": true, "items": [{"qty": 2, "sku": "A1"}, {"qty": 1, "sku": "B7"}], "user_id": 10} |
| 2 | {"paid": false, "items": [{"qty": 5, "sku": "A1"}], "user_id": 11} |
| 3 | {"paid": true, "items": [], "coupon": "SAVE5", "user_id": 12} |
+----+----------------------------------------------------------------------------------------------------------+
(3 rows)
Kết quả [Đã chạy: PostgreSQL 16.15]: mỗi tài liệu hiển thị với khóa đã sắp lại, theo thứ tự độ dài rồi tới chữ cái. Hàng 1 có "note": null, hàng 3 có "coupon".
Lấy giá trị ra bằng -> và ->>:
SELECT id,
payload->'user_id' AS user_json,
payload->>'user_id' AS user_text,
pg_typeof(payload->>'user_id') AS text_type
FROM orders
ORDER BY id;
+----+-----------+-----------+-----------+
| id | user_json | user_text | text_type |
+----+-----------+-----------+-----------+
| 1 | 10 | 10 | text |
| 2 | 11 | 11 | text |
| 3 | 12 | 12 | text |
+----+-----------+-----------+-----------+
(3 rows)
SELECT id,
(payload->'items'->0->>'qty')::int AS first_qty,
payload #>> '{items,0,sku}' AS first_sku
FROM orders
ORDER BY id;
+----+-----------+-----------+
| id | first_qty | first_sku |
+----+-----------+-----------+
| 1 | 2 | A1 |
| 2 | 5 | A1 |
| 3 | NULL | NULL |
+----+-----------+-----------+
(3 rows)
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
payload->'user_id' | Lấy giá trị khóa user_id, giữ kiểu jsonb | Dùng khi cần đi tiếp vào trong |
payload->>'user_id' | Lấy giá trị khóa user_id và đổi thành text | Dùng khi cần so sánh hoặc ép sang kiểu SQL |
payload->'items'->0->>'qty' | Đi vào mảng items, phần tử 0, lấy qty dạng text | Mỗi -> đi một bước, ->> cuối cùng đổi sang text |
(... )::int | Ép text sang số nguyên | Cần để cộng trừ hoặc so sánh theo số |
payload #>> '{items,0,sku}' | Đi theo đường dẫn, trả về text | Cách viết gọn cho chuỗi -> nối nhau |
Xem từng bước
Animation dưới đây cho cùng một chuỗi vào hai cột. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: PostgreSQL 16.15, chuỗi {"b": 1, "a": 2, "a": 3}]:
| Điều | Cột json | Cột jsonb |
|---|---|---|
| Giá trị lưu | {"b": 1, "a": 2, "a": 3} | {"a": 3, "b": 1} |
| Dấu cách thừa | Giữ | Bỏ |
| Khóa lặp | Giữ cả hai | Giữ giá trị cuối |
| So sánh bằng | Lỗi operator does not exist | Chạy được |
| Chỉ mục GIN | Không tạo được | Tạo được |
Bạn vừa làm được: nói trước cột json và jsonb lưu ra gì, và điều gì chỉ làm được với jsonb.
Concept Check
Cột payload kiểu jsonb có ba đơn, với user_id là 10, 11 và 12.
Câu 1. payload->>'user_id' > '9' trả về bao nhiêu đơn?
- A. 3, vì cả ba mã lớn hơn 9.
- B. 0, vì text so sánh từng ký tự.
- C. 1, vì chỉ mã 12 lớn hơn 9.
- D. Lỗi, vì không so sánh text với số.
Câu 2. payload @> '{"paid": true}' hỏi điều gì?
- A. Khóa
paidcó tồn tại hay không. - B. Giá trị
paidcó kiểu boolean hay không. - C. Tài liệu có chứa cặp
paidbằng true hay không. - D. Tài liệu có bằng đúng cặp
paidhay không.
Câu 3. Với đơn có "note": null, payload->'note' IS NULL cho kết quả nào?
- A. true, vì giá trị null của JSON là NULL của SQL.
- B. false, vì đó là giá trị JSON
nullchứ không phải NULL của SQL. - C. NULL, vì mọi phép so sánh với null đều ra NULL.
- D. Lỗi, vì khóa
notecó giá trị không hợp lệ.
Đáp án và giải thích:
- Câu 1: B.
->>trả về text, và text so sánh ký tự đầu trước: '1' nhỏ hơn '9'. A và C quên điều đó. D sai vì có phép so text với text. - Câu 2: C.
@>là toán tử chứa, không phải bằng hay tồn tại khóa. A mô tả?. B không có toán tử nào như vậy. D nhầm với=. - Câu 3: B. Giá trị JSON
nulllà một giá trị có thật nên->trả vềnullcủa JSON. Chỉ->>mới đổi nó thành NULL của SQL. C và D sai vì không có phép so sánh nào ở đây.
Làm cùng mình
Cô Tấm muốn danh sách đơn đã thanh toán, kèm mã khách ở dạng số và mã mặt hàng đầu tiên.
Bước 1. Vẽ ra giấy
Với ba đơn, ghi đơn nào đã thanh toán, và mã mặt hàng đầu tiên của mỗi đơn đó. Đơn nào có mảng items rỗng?
Bước 2. Điền chỗ trống
SELECT id,
-- TODO 1: user_id lấy ra dạng số nguyên, đặt tên cột là user_id
-- TODO 2: sku của mặt hàng đầu tiên dạng text, đặt tên cột là first_sku
FROM orders
WHERE -- TODO 3: chỉ lấy đơn đã thanh toán, dùng toán tử chứa @>
ORDER BY id;
Gợi ý: ->> trả text nên cần ép sang số. Đường dẫn tới sku đầu tiên là items, 0, sku.
Chạy xong bạn sẽ thấy [Đã chạy] hai hàng, trong đó đơn 3 có first_sku là NULL vì mảng rỗng. Đáp án ở mục Lời giải và đáp án.
Bước 3. Đoán trước một truy vấn
Truy vấn payload->>'paid' = true có chạy được không? Ghi dự đoán rồi đối chiếu ở mục Thử phá nó đi.
Thử phá nó đi
Ba lỗi người mới thường gặp với JSON [Đã chạy: PostgreSQL 16.15]:
SELECT '{"a": 1, bad}'::jsonb;
ERROR: invalid input syntax for type json
LINE 1: SELECT '{"a": 1, bad}'::jsonb;
^
DETAIL: Token "bad" is invalid.
CONTEXT: JSON data, line 1: {"a": 1, bad...
SELECT '{"a": 1}'::json = '{"a": 1}'::json;
ERROR: operator does not exist: json = json
LINE 1: SELECT '{"a": 1}'::json = '{"a": 1}'::json;
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
SELECT id FROM orders WHERE payload->>'paid' = true;
ERROR: operator does not exist: text = boolean
LINE 1: SELECT id FROM orders WHERE payload->>'paid' = true;
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
| Code lỗi | Thông báo lỗi | Nguyên nhân | Cách sửa |
|---|---|---|---|
'{"a": 1, bad}'::jsonb | invalid input syntax for type json | Chuỗi không phải JSON hợp lệ | Sửa chuỗi: khóa phải trong nháy kép và giá trị phải đúng cú pháp |
'{"a": 1}'::json = '{"a": 1}'::json | operator does not exist: json = json | Kiểu json không có toán tử bằng | Ép sang jsonb rồi so sánh, hoặc lưu jsonb |
payload->>'paid' = true | operator does not exist: text = boolean | ->> trả text, không so được với boolean | Viết = 'true', hoặc (payload->>'paid')::boolean, hoặc payload @> '{"paid": true}' |
Hiểu lầm quan trọng nhất của bài này là tin ->> trả về số khi giá trị là số. Hình dưới đối chiếu mô hình sai với mô hình đúng.
Đang tải sơ đồ…
Cách đọc hình: hai chuỗi độc lập. Chuỗi đỏ là niềm tin sai, dẫn đến việc chờ 3 đơn. Chuỗi xanh là thực tế: toán tử ->> trả về text, nên phải ép sang số trước khi so sánh.
Xác nhận hai phép so sánh trên cùng ba đơn:
SELECT count(*) FILTER (WHERE payload->>'user_id' > '9') AS text_gt_9,
count(*) FILTER (WHERE (payload->>'user_id')::int > 9) AS int_gt_9
FROM orders;
+-----------+----------+
| text_gt_9 | int_gt_9 |
+-----------+----------+
| 0 | 3 |
+-----------+----------+
(1 row)
Kết quả [Đã chạy: PostgreSQL 16.15]: so text cho 0 đơn, so số cho 3 đơn. Khi sửa sang đúng kiểu, payload->>'paid' cũng so được bằng hai cách:
SELECT string_agg(id::text, ', ' ORDER BY id) FILTER (WHERE payload->>'paid' = 'true') AS text_compare,
string_agg(id::text, ', ' ORDER BY id) FILTER (WHERE (payload->>'paid')::boolean) AS cast_compare
FROM orders;
+--------------+--------------+
| text_compare | cast_compare |
+--------------+--------------+
| 1, 3 | 1, 3 |
+--------------+--------------+
(1 row)
Truy vấn chứa và tồn tại trả về danh sách mã đơn:
SELECT string_agg(id::text, ', ' ORDER BY id)
FILTER (WHERE payload @> '{"paid": true}') AS paid_orders,
string_agg(id::text, ', ' ORDER BY id)
FILTER (WHERE payload @> '{"items": [{"sku": "A1"}]}') AS has_sku_a1,
string_agg(id::text, ', ' ORDER BY id)
FILTER (WHERE payload ? 'coupon') AS has_coupon
FROM orders;
+-------------+------------+------------+
| paid_orders | has_sku_a1 | has_coupon |
+-------------+------------+------------+
| 1, 3 | 1, 2 | 3 |
+-------------+------------+------------+
(1 row)
Và JSON null khác với khóa vắng mặt:
SELECT id,
payload->'note' AS note_json,
payload->>'note' AS note_text,
payload->'note' IS NULL AS note_is_sql_null,
payload->'missing' IS NULL AS missing_is_sql_null
FROM orders
ORDER BY id;
+----+-----------+-----------+------------------+---------------------+
| id | note_json | note_text | note_is_sql_null | missing_is_sql_null |
+----+-----------+-----------+------------------+---------------------+
| 1 | null | NULL | f | t |
| 2 | NULL | NULL | t | t |
| 3 | NULL | NULL | t | t |
+----+-----------+-----------+------------------+---------------------+
(3 rows)
Hàng 1 có note_json là null của JSON, và note_is_sql_null bằng f. Hàng 2 và 3 không có khóa note, nên -> cho NULL của SQL. Còn ->> cho NULL của SQL ở cả ba hàng.
Tự tay làm
Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Đoán kiểu. Với đơn số 1 trong bảng orders, điền kiểu và giá trị hiện ra:
| Biểu thức | Kết quả |
|---|---|
payload->'paid' | ? |
pg_typeof(payload->>'paid') | ? |
payload->'items'->0->'sku' | ? |
payload->>'missing' | ? |
jsonb_array_length(payload->'items') | ? |
Tiêu chí đạt: điền đúng cả năm ô. Gợi ý: -> giữ kiểu jsonb nên chuỗi còn dấu nháy kép; khóa không có cho NULL.
Vận dụng
Bài 2. Khóa lặp. Đầu vào: chuỗi {"a": 1, "a": 2}. Dự đoán giá trị lưu ở json và ở jsonb, và ->>'a' trên giá trị json trả về gì. Tiêu chí đạt: ba kết quả đúng. Gợi ý: jsonb giữ khóa cuối, json giữ cả hai nhưng các toán tử thường lấy cái cuối.
Thử thách (tùy chọn)
Bài 3. Sửa tài liệu. Đầu vào: đơn số 2 chưa thanh toán. Dùng một lệnh UPDATE đặt paid thành true và thêm khóa coupon với giá trị SAVE10, rồi in paid và coupon của cả ba đơn. Tiêu chí đạt: đơn 2 có paid là true và coupon là SAVE10, hai đơn còn lại không đổi. Gợi ý: hàm jsonb_set sửa một khóa, toán tử nối hai jsonb (hai dấu gạch đứng) gộp thêm khóa mới.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài.
- (Mục tiêu 1, nhớ lại) Kể ba điều
jsonblàm khácjsonkhi lưu chuỗi{"b": 1, "a": 2}. - (Mục tiêu 2, dự đoán)
payload->'user_id'vàpayload->>'user_id'trả về kiểu gì? - (Mục tiêu 2, giải thích) Vì sao
payload->'note' IS NULLcó thể false dù tài liệu có"note": null? - (Mục tiêu 3, tìm lỗi) Một lập trình viên viết
payload->>'user_id' > '9'để lấy khách mã lớn hơn 9. Anh ấy sai ở đâu và sửa thế nào? - (Mục tiêu 4, so sánh) Chỉ mục GIN trên
payloadcó giúp truy vấnpayload->>'user_id' = '42'không? Nếu không, cần chỉ mục nào?
Điều cần nhớ
jsongiữ nguyên chữ.jsonbphân tích, bỏ dấu cách thừa, sắp lại khóa, giữ khóa cuối, và có toán tử bằng cùng chỉ mục.->trảjsonb,->>và#>>trảtext. Khóa vắng cho NULL của SQL, JSONnulllà giá trị có thật.@>hỏi chứa,?hỏi tồn tại khóa. Cả hai chạy trênjsonb.- GIN giúp
@>và?. So sánh->>cần chỉ mục biểu thức và câu truy vấn phải viết giống hệt biểu thức.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
| Giá trị dạng text | payload->>'k' | Ép sang số nếu cần so sánh theo số |
| Đi sâu vào trong | payload->'a'->0->>'b' hoặc payload #>> '{a,0,b}' | Mỗi bước một toán tử |
| Tìm đơn chứa cặp | payload @> '{"paid": true}' | Dùng được chỉ mục GIN |
| Kiểm tra khóa có mặt | payload ? 'coupon' | Chỉ ở tầng ngoài cùng |
| Sửa một khóa | jsonb_set(payload, '{paid}', 'true') | Trả tài liệu mới |
| Chỉ mục tìm chứa | CREATE INDEX ... USING gin (payload) | Không giúp ->> |
| Chỉ mục so sánh một khóa | CREATE INDEX ... ((payload->>'user_id')) | Truy vấn phải khớp biểu thức |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho ngày mai: email của khách phải theo một mẫu nhất định ở mọi bảng của tiệm. Viết điều kiện đó ở từng cột thì mệt, có cách nào đặt tên cho luật đó một lần rồi dùng lại không?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Kiểu
jsonkhông tạo được chỉ mục GIN:
CREATE TABLE json_docs (doc json);
CREATE INDEX idx_json_docs ON json_docs USING gin (doc);
ERROR: data type json has no default operator class for access method "gin"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
jsonb có hai lớp toán tử cho GIN. Lớp mặc định jsonb_ops hỗ trợ cả @> và ?, lớp jsonb_path_ops chỉ hỗ trợ @> nhưng thường nhỏ hơn, như phần so sánh kích thước ngay dưới đây.
- So sánh với lựa chọn khác. Chỉ mục giúp được truy vấn nào? Bài tạo bảng 50000 đơn, tạo chỉ mục GIN rồi xem kế hoạch của hai truy vấn:
CREATE TABLE big_orders (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
payload jsonb NOT NULL
);
INSERT INTO big_orders (payload)
SELECT jsonb_build_object(
'user_id', g % 5000,
'paid', g % 3 = 0,
'items', jsonb_build_array(jsonb_build_object('sku', 'S' || (g % 400), 'qty', 1 + g % 5)))
FROM generate_series(1, 50000) AS g;
CREATE INDEX idx_big_gin ON big_orders USING gin (payload);
ANALYZE big_orders;
EXPLAIN (COSTS OFF) SELECT id FROM big_orders WHERE payload @> '{"user_id": 42}';
EXPLAIN (COSTS OFF) SELECT id FROM big_orders WHERE payload->>'user_id' = '42';
+-----------------------------------------------------------+
| QUERY PLAN |
+-----------------------------------------------------------+
| Bitmap Heap Scan on big_orders |
| Recheck Cond: (payload @> '{"user_id": 42}'::jsonb) |
| -> Bitmap Index Scan on idx_big_gin |
| Index Cond: (payload @> '{"user_id": 42}'::jsonb) |
+-----------------------------------------------------------+
(4 rows)
+--------------------------------------------------------+
| QUERY PLAN |
+--------------------------------------------------------+
| Seq Scan on big_orders |
| Filter: ((payload ->> 'user_id'::text) = '42'::text) |
+--------------------------------------------------------+
(2 rows)
[Có thể khác nhau: kế hoạch thực thi phụ thuộc thống kê và cấu hình]. Truy vấn @> dùng Bitmap Index Scan on idx_big_gin, còn ->> bằng '42' chỉ có Seq Scan vì GIN không hỗ trợ phép so sánh đó. Thêm chỉ mục biểu thức:
CREATE INDEX idx_big_user ON big_orders ((payload->>'user_id'));
ANALYZE big_orders;
EXPLAIN (COSTS OFF) SELECT id FROM big_orders WHERE payload->>'user_id' = '42';
EXPLAIN (COSTS OFF) SELECT id FROM big_orders WHERE (payload->>'user_id')::int = 42;
+------------------------------------------------------------------+
| QUERY PLAN |
+------------------------------------------------------------------+
| Bitmap Heap Scan on big_orders |
| Recheck Cond: ((payload ->> 'user_id'::text) = '42'::text) |
| -> Bitmap Index Scan on idx_big_user |
| Index Cond: ((payload ->> 'user_id'::text) = '42'::text) |
+------------------------------------------------------------------+
(4 rows)
+-----------------------------------------------------------+
| QUERY PLAN |
+-----------------------------------------------------------+
| Seq Scan on big_orders |
| Filter: (((payload ->> 'user_id'::text))::integer = 42) |
+-----------------------------------------------------------+
(2 rows)
Truy vấn viết đúng biểu thức dùng chỉ mục B-tree mới. Truy vấn có thêm ::int quay lại Seq Scan, vì biểu thức đã đổi. Cuối cùng, kích thước hai lớp toán tử GIN trên bảng 50000 đơn đó:
CREATE INDEX idx_big_pp ON big_orders USING gin (payload jsonb_path_ops);
SELECT pg_size_pretty(pg_relation_size('idx_big_gin')) AS default_gin,
pg_size_pretty(pg_relation_size('idx_big_pp')) AS path_ops_gin;
+-------------+--------------+
| default_gin | path_ops_gin |
+-------------+--------------+
| 1312 kB | 1000 kB |
+-------------+--------------+
(1 row)
[Có thể khác nhau: kích thước phụ thuộc dữ liệu, phiên bản và trạng thái bảng].
- Hiểu lầm nâng cao. Cập nhật
jsonbluôn tạo ra tài liệu mới, hàm không sửa tại chỗ:
SELECT 'set' AS op, jsonb_set(payload, '{paid}', 'true') AS result
FROM orders WHERE id = 2
UNION ALL
SELECT 'minus', payload - 'items' FROM orders WHERE id = 2
UNION ALL
SELECT 'merge', payload || '{"status": "ok"}' FROM orders WHERE id = 2;
+-------+------------------------------------------------------------------------------------+
| op | result |
+-------+------------------------------------------------------------------------------------+
| set | {"paid": true, "items": [{"qty": 5, "sku": "A1"}], "user_id": 11} |
| minus | {"paid": false, "user_id": 11} |
| merge | {"paid": false, "items": [{"qty": 5, "sku": "A1"}], "status": "ok", "user_id": 11} |
+-------+------------------------------------------------------------------------------------+
(3 rows)
Ba dòng là kết quả của jsonb_set, của phép trừ khóa items, và của phép nối với tài liệu nhỏ. Giá trị trong bảng chỉ đổi khi bạn dùng UPDATE.
- Giới hạn của bài. Bài này chưa nói đến
jsonb_path_query,jsonb_eachhay kiểm tra cấu trúc bằngCHECK. [Suy luận] Khi một khóa luôn có mặt và hay được lọc, nhưuser_idhaypaid, nhiều đội tách nó thành cột riêng thay vì để trong JSON, nhưng bạn nên đo trên dữ liệu thật. - Đọc thêm. PostgreSQL 16 Documentation, mục JSON Types: https://www.postgresql.org/docs/16/datatype-json.html và mục JSON Functions and Operators: https://www.postgresql.org/docs/16/functions-json.html
Bài tiếp theo
Bài sau là CREATE DOMAIN: đặt tên cho một luật dữ liệu rồi dùng lại ở nhiều cột. Mini-dự án nối tiếp: định nghĩa kiểu positive_money và email_address dùng chung cho các bảng của Tạp Hóa Cô Tấm.
Lời giải và đáp án
Làm cùng mình, Bước 2. Ba chỗ trống điền (payload->>'user_id')::int AS user_id, payload #>> '{items,0,sku}' AS first_sku và payload @> '{"paid": true}'.
SELECT id,
(payload->>'user_id')::int AS user_id,
payload #>> '{items,0,sku}' AS first_sku
FROM orders
WHERE payload @> '{"paid": true}'
ORDER BY id;
+----+---------+-----------+
| id | user_id | first_sku |
+----+---------+-----------+
| 1 | 10 | A1 |
| 3 | 12 | NULL |
+----+---------+-----------+
(2 rows)
Đơn 1 (khách 10, mặt hàng đầu A1) và đơn 3 (khách 12, mảng rỗng nên first_sku là NULL) đã thanh toán.
Làm cùng mình, Bước 3. Truy vấn lỗi operator does not exist: text = boolean, như dòng cuối của mục Thử phá nó đi.
Bài 1.
SELECT payload->'paid' AS e1,
pg_typeof(payload->>'paid') AS e2,
payload->'items'->0->'sku' AS e3,
payload->>'missing' AS e4,
jsonb_array_length(payload->'items') AS e5
FROM orders
WHERE id = 1;
+------+------+------+------+----+
| e1 | e2 | e3 | e4 | e5 |
+------+------+------+------+----+
| true | text | "A1" | NULL | 2 |
+------+------+------+------+----+
(1 row)
payload->'paid' là true (kiểu jsonb), pg_typeof(payload->>'paid') là text, payload->'items'->0->'sku' là "A1" kèm nháy kép vì vẫn là JSON, payload->>'missing' là NULL, và mảng có 2 phần tử.
Bài 2.
SELECT '{"a": 1, "a": 2}'::json AS as_json,
'{"a": 1, "a": 2}'::jsonb AS as_jsonb,
('{"a": 1, "a": 2}'::json)->>'a' AS json_pick;
+------------------+----------+-----------+
| as_json | as_jsonb | json_pick |
+------------------+----------+-----------+
| {"a": 1, "a": 2} | {"a": 2} | 2 |
+------------------+----------+-----------+
(1 row)
json giữ nguyên {"a": 1, "a": 2}, jsonb ra {"a": 2}, và ->>'a' trên json trả về 2, giá trị cuối.
Bài 3.
UPDATE orders
SET payload = jsonb_set(payload, '{paid}', 'true') || '{"coupon": "SAVE10"}'
WHERE id = 2;
SELECT id, payload->>'paid' AS paid, payload->>'coupon' AS coupon
FROM orders
ORDER BY id;
+----+------+--------+
| id | paid | coupon |
+----+------+--------+
| 1 | true | NULL |
| 2 | true | SAVE10 |
| 3 | true | SAVE5 |
+----+------+--------+
(3 rows)
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
jsonb_set(payload, '{paid}', 'true') | Trả tài liệu mới với paid bằng true | Hàm không sửa tại chỗ, nên gán lại vào cột |
'{"coupon": "SAVE10"}' ở vế sau của toán tử nối | Gộp thêm khóa coupon vào tài liệu | Toán tử nối jsonb ghép hai tài liệu thành một |
WHERE id = 2 | Chỉ đổi đơn 2 | Hai đơn còn lại giữ nguyên |
Tự kiểm tra.
- Bỏ dấu cách thừa, sắp lại khóa (a trước b), và nếu khóa lặp thì giữ giá trị cuối.
jsonbvàtext.- Tài liệu có khóa
notevới giá trị JSONnull, nên->trả về JSONnull, một giá trị có thật, không phải NULL của SQL. ->>trảtextnên so sánh theo ký tự và'10' < '9'. Sửa bằng(payload->>'user_id')::int > 9.- Không. GIN hỗ trợ
@>và?chứ không hỗ trợ so sánh->>. Cần chỉ mục biểu thức B-tree trên(payload->>'user_id').