Mục lục
- Mục Tiêu Bài Học
- JSONB vs JSON — Khác Biệt Cốt Lõi
- 6 JSONB Operator Cơ Bản
- GIN Index Cho JSONB — Default vs
jsonb_path_ops - JSONB Path Expression — Query Sâu
- Array Operator PostgreSQL
- Migration
metadataJSONB + Index - Search Metadata + Payment Query Pattern
- Tổng Kết Group 6 + Roadmap Group 7
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Phân biệt JSONB vs JSON Postgres về storage, query speed và index khả dụng.
- Hiểu 6 JSONB operator core:
->,->>,@>,<@,?,?|,?&. - Dùng JSONB path expression với
#>,#>>vàjsonb_path_query. - Implement GIN index cho JSONB: phân biệt default
jsonb_opsvsjsonb_path_ops. - Hiểu 4 array operator Postgres:
= ANY,@>,&&,||. - Viết migration mới thêm cột
metadata JSONBchoproducts+ 2 index. - Implement metadata search endpoint + payment_payload query pattern Shop API.
- Hoàn thành Group 6 PostgreSQL + sqlx (10/10 bài) → sẵn sàng sang Group 7 CRUD đầy đủ.
JSONB vs JSON — Khác Biệt Cốt Lõi
PostgreSQL hỗ trợ 2 kiểu dữ liệu JSON với chiến lược lưu trữ khác nhau:
- JSON (kiểu text): lưu nguyên văn bản gốc, parse lại mỗi lần truy vấn. Insert nhanh vì chỉ validate cú pháp. Query chậm vì parse lặp + không có index hiệu quả. Preserve thứ tự key + khoảng trắng nguyên bản.
- JSONB (kiểu binary): parse một lần lúc
INSERT, lưu dưới dạng cây nhị phân. Insert chậm hơn một chút, nhưng query nhanh, hỗ trợ index GIN, so sánh giá trị nhanh. KHÔNG preserve thứ tự key + KHÔNG giữ khoảng trắng dư.
Mô hình storage rút gọn:
JSON → TEXT internal, parse on every read
JSONB → Binary tree, parse once at write
Lock decision Shop API: dùng JSONB cho mọi flexible schema column (metadata sản phẩm, payment_payload, audit log, user preferences). Lý do: query pattern (filter, sort, contains) là yêu cầu chính của API, đánh đổi vài microsecond insert lấy index GIN xứng đáng. JSON kiểu text chỉ phù hợp khi cần archive nguyên format gốc (rất hiếm — gần như không xảy ra trong Shop API).
So sánh chi tiết:
Tiêu chí | JSON | JSONB
Storage | Text raw | Binary tree
Insert | Nhanh | Chậm hơn ~5-10%
Read/Query | Chậm (parse) | Nhanh
Index GIN | KHÔNG | CÓ
Operator @>, ?, -> | KHÔNG hiệu quả | Native + nhanh
Preserve key order | CÓ | KHÔNG
Preserve whitespace | CÓ | KHÔNG
Duplicate key | Giữ lại | Chỉ giữ key cuối
Khi nào dùng JSON kiểu text: log raw từ third-party, audit JSON nguyên bản phải so sánh byte-by-byte (vd webhook signature verify cần giữ nguyên payload gốc). Trong Shop API, webhook signature verify lưu raw bytes (BYTEA) thay JSON text, nên cũng không cần kiểu JSON. Kết luận: JSONB MANDATORY cho mọi cột flexible schema từ B60 trở đi.
6 JSONB Operator Cơ Bản
JSONB Postgres cung cấp 7 operator chính (gọi tròn là "6 core" do <@ đối xứng @>). Dữ liệu mẫu: cột payment_payload bảng payments (lock B43+B54) chứa {"card_brand": "visa", "card": {"last4": "4242", "exp_month": 12}}.
-> field/index access (trả JSONB) — truy cập key trong object hoặc index trong array, kết quả vẫn là JSONB nên có thể chain tiếp:
-- Truy cập key object → trả JSONB
SELECT payment_payload->'card' FROM payments;
-- {"last4": "4242", "exp_month": 12} (JSONB)
-- Chain tiếp key con
SELECT payment_payload->'card'->'last4' FROM payments;
-- "4242" (JSONB string — vẫn có dấu nháy ngoài)
-- Array index (zero-based)
SELECT '["a", "b", "c"]'::jsonb->1;
-- "b" (JSONB)
->> field access (trả TEXT) — giống -> nhưng kết quả là TEXT thuần, đã strip dấu nháy. Pattern dùng khi cần so sánh với chuỗi Rust hoặc convert sang kiểu khác:
SELECT payment_payload->>'card_brand' FROM payments;
-- visa (TEXT — KHÔNG có dấu nháy)
-- Cast sang INT
SELECT (payment_payload->'card'->>'exp_month')::int FROM payments;
-- 12
@> contains (left chứa right) — kiểm tra JSONB bên trái có chứa cấu trúc JSONB bên phải không. Đây là operator quan trọng nhất cho lookup theo nested key:
SELECT * FROM payments
WHERE payment_payload @> '{"card_brand": "visa"}'::jsonb;
-- Match mọi payment có key card_brand = "visa"
-- Match nested deep
SELECT * FROM payments
WHERE payment_payload @> '{"card": {"last4": "4242"}}'::jsonb;
<@ contained by (right chứa left) — đối xứng @>, ít dùng trong Shop API.
? key exists (top-level) — kiểm tra key tồn tại ở mức top-level của JSONB object:
SELECT * FROM products WHERE metadata ? 'warranty_months';
-- Match khi metadata có key warranty_months ở top-level
?| ANY key exists — match khi có ít nhất 1 key trong array tồn tại:
SELECT * FROM products
WHERE metadata ?| ARRAY['warranty_months', 'discount_code'];
?& ALL keys exist — match khi tất cả key trong array đều tồn tại:
SELECT * FROM products
WHERE metadata ?& ARRAY['warranty_months', 'color'];
Bảng so sánh nhanh:
Operator | Mô tả | Return type
-> | Get value at key/index | JSONB
->> | Get value at key/index | TEXT
@> | Contains | BOOL
<@ | Contained by | BOOL
? | Key exists (top-level) | BOOL
?| | Any key in array exists | BOOL
?& | All keys in array exist | BOOL
Lưu ý: ?, ?|, ?& chỉ kiểm tra key ở mức top-level của object — không recurse nested. Muốn check nested phải dùng @> hoặc JSONPath (Bước 5).
GIN Index Cho JSONB — Default vs jsonb_path_ops
GIN (Generalized Inverted Index) là kiểu index tối ưu cho dữ liệu "có nhiều phần tử nhỏ bên trong" — JSONB key/value, array element, tsvector token. Postgres cho phép 2 operator class khi tạo GIN cho JSONB:
Default — jsonb_ops: index mọi key và mọi value trong JSONB. Support đủ 5 operator (@>, <@, ?, ?|, ?&) nhưng size lớn hơn và write chậm hơn.
CREATE INDEX products_metadata_gin_idx
ON products USING GIN(metadata);
-- Mặc định jsonb_ops — support mọi operator JSONB
jsonb_path_ops: chỉ index path (chuỗi key → giá trị), chỉ support operator @>. Đổi lại size nhỏ hơn ~30% và lookup nhanh hơn.
CREATE INDEX payments_payload_path_idx
ON payments USING GIN(payment_payload jsonb_path_ops);
-- Chỉ support @> nhưng smaller + faster
Trade-off:
Operator class | Operator support | Size | Lookup
jsonb_ops | @>, <@, ?, ?|, ?& | Large | Slow hơn
jsonb_path_ops | @> chỉ (most common) | Small | Fast hơn
Lock decision Shop API:
payments.payment_payload→jsonb_path_ops. Lý do: query duy nhất là lookup webhookWHERE payment_payload @> '{"payment_intent_id": "pi_..."}'(B71 Stripe handler). Không cần check key existence.products.metadata→ defaultjsonb_ops. Lý do: cần cả@>(filter cụ thể) lẫn?(check feature flag tồn tại — vd sản phẩm nào cówarranty_monthsđể render badge UI).
Khi nào KHÔNG cần GIN: cột JSONB nhỏ + ít row + query rare → seq scan đủ nhanh, index overhead không xứng. Trong Shop API, mọi cột JSONB query thường xuyên đều có GIN — không có exception.
Verify index đã được dùng qua EXPLAIN ANALYZE:
EXPLAIN ANALYZE
SELECT * FROM products WHERE metadata @> '{"color": "black"}';
-- Expected output (rút gọn):
-- Bitmap Heap Scan on products
-- Recheck Cond: (metadata @> '{"color": "black"}'::jsonb)
-- -> Bitmap Index Scan on products_metadata_gin_idx
-- Index Cond: (metadata @> '{"color": "black"}'::jsonb)
-- Planning Time: 0.2 ms
-- Execution Time: 0.5 ms
JSONB Path Expression — Query Sâu
Postgres 12+ hỗ trợ chuẩn SQL/JSON với JSONPath — ngôn ngữ truy vấn nhỏ gọn nested deep, tương tự XPath cho XML.
-- Get value theo path
SELECT jsonb_path_query(payment_payload, '$.card.brand')
FROM payments;
-- "visa"
-- Filter array element theo điều kiện
SELECT jsonb_path_query(metadata, '$.history[*] ? (@.year > 2020)')
FROM products;
-- Trả về element nào trong array history có year > 2020
2 operator path "rút gọn" thường dùng hơn JSONPath đầy đủ:
#>— extract value tại path (text array), trả JSONB.#>>— extract value tại path (text array), trả TEXT.
-- Path là array of TEXT — mỗi phần tử là 1 key/index
SELECT metadata #> '{shipping, weight}' FROM products;
-- 1.2 (JSONB)
SELECT metadata #>> '{shipping, weight}' FROM products;
-- 1.2 (TEXT)
-- Truy cập array index trong path
SELECT metadata #>> '{history, 0, year}' FROM products;
-- 2024 (TEXT)
Pattern Shop API: dùng @> cho query đơn giản (đa số case) và #>> cho extract giá trị từ path cố định (vd partial expression index trên metadata->>'warranty_months' ở Bước 7). JSONPath đầy đủ chỉ dùng cho analytics endpoint admin (G14-G15) khi query phức tạp dạng "tìm sản phẩm có history nào year > 2020 và discount > 10%".
Array Operator PostgreSQL
Bảng products đã có cột tags TEXT[] với GIN index từ B52. Postgres array có 4 operator chính, parallel với JSONB operator nhưng cú pháp riêng:
= ANY(array) — kiểm tra value có nằm trong array (lock từ B52 + B59):
SELECT * FROM products WHERE 'apple' = ANY(tags);
-- Tags chứa "apple"
@> array contains — array bên trái chứa tất cả phần tử của array bên phải:
SELECT * FROM products WHERE tags @> ARRAY['apple', 'phone'];
-- Tags chứa CẢ "apple" VÀ "phone"
&& array overlap — 2 array có ít nhất 1 phần tử chung:
SELECT * FROM products WHERE tags && ARRAY['apple', 'samsung'];
-- Tags chứa ÍT NHẤT một trong "apple" hoặc "samsung"
|| array concat — nối 2 array hoặc append phần tử:
UPDATE products
SET tags = tags || ARRAY['new-tag']::TEXT[]
WHERE id = 1;
GIN index cho array (lock B52 continued):
CREATE INDEX products_tags_gin_idx
ON products USING GIN(tags);
-- Tăng tốc = ANY, @>, &&
Quy tắc Array vs JSONB Shop API:
Use case | Recommend
1D string list (tag, category) | Array TEXT[]
Hierarchical / nested object | JSONB
Variable schema per row | JSONB
Cần sort/order theo vị trí | Array (preserve index)
Full-text search inside | JSONB + GIN
Audit log raw event payload | JSONB
Lock decision Shop API:
- Tags 1D string (products) →
TEXT[]array (B52 lock). - Metadata variable schema (products) → JSONB (B60 lock).
- Settings hierarchical (future users.preferences) → JSONB.
- category_ids primary/secondary order → array
BIGINT[]giữ vị trí thứ tự.
Migration metadata JSONB + Index
Bảng products đến B59 đã có 9 cột (id, name, slug, price, stock, description, tags, created_at, updated_at). Bước cuối Group 6 thêm cột metadata JSONB cho flexible schema (warranty_months, color, dimensions, custom_attribute_* ...).
Tạo migration mới qua sqlx-cli (lock B58 workflow):
cargo sqlx migrate add \
--source crates/shop-db/migrations \
add_products_metadata
# Tạo file:
# crates/shop-db/migrations/20260615150000_add_products_metadata.sql
Edit nội dung migration:
-- File: crates/shop-db/migrations/20260615150000_add_products_metadata.sql
-- 1. Thêm cột metadata JSONB, NOT NULL DEFAULT '{}' (empty object)
ALTER TABLE products
ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}'::jsonb;
-- 2. GIN index default jsonb_ops — support đủ @>, ?, ?|, ?&
CREATE INDEX products_metadata_gin_idx
ON products USING GIN(metadata);
-- 3. Partial expression index cho query phổ biến warranty_months
-- Chỉ index row có key warranty_months → size nhỏ
CREATE INDEX products_metadata_warranty_idx
ON products ((metadata->>'warranty_months'))
WHERE metadata ? 'warranty_months';
Apply migration:
cargo sqlx migrate run --source crates/shop-db/migrations
# Output:
# Applied 20260615150000/migrate add products metadata (12.3ms)
Sau migration, Shop API có 5 migration applied tổng cộng: create_products (B51), add_products_tags (B52), create_orders (B54), create_payments (B54), add_products_metadata (B60).
Cập nhật ProductRow ở DB layer thêm field metadata:
// File: crates/shop-db/src/products.rs
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct ProductRow {
pub id: i64,
pub name: String,
pub slug: String,
pub price: rust_decimal::Decimal,
pub stock: i32,
pub description: Option<String>,
pub tags: Vec<String>,
pub metadata: JsonValue, // NEW B60 — JSONB → serde_json::Value
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
Cập nhật CreateProductDto wire format cho input client gửi (lock B47 BTreeMap stable):
// File: crates/shop-common/src/dto/product.rs
use std::collections::BTreeMap;
use serde_json::Value;
#[derive(Debug, Clone, Deserialize, Validate)]
pub struct CreateProductDto {
// ... existing field (name, slug, price, stock, description, tags)
#[serde(default)]
pub metadata: BTreeMap<String, Value>,
}
Update mọi SELECT, INSERT hiện tại để thêm cột metadata. Với sqlx::query_as! macro cần regenerate cache:
cargo sqlx prepare --workspace
# Cập nhật .sqlx/ cache cho mọi query_as! đã thay đổi schema
Search Metadata + Payment Query Pattern
Extend ProductFilter (B59 lock) thêm 2 filter JSONB:
// File: crates/shop-db/src/products.rs
use serde_json::Value as JsonValue;
#[derive(Debug, Clone)]
pub struct ProductFilter {
pub name: Option<String>,
pub tag: Option<String>,
pub min_price: Option<rust_decimal::Decimal>,
pub max_price: Option<rust_decimal::Decimal>,
pub sort: String,
pub page: u32,
pub per_page: u32,
// NEW B60
pub has_metadata_key: Option<String>, // ? operator
pub metadata_contains: Option<JsonValue>, // @> operator
}
Cập nhật apply_filter (helper reuse SELECT + COUNT, lock B59):
// File: crates/shop-db/src/products.rs
fn apply_filter(
qb: &mut QueryBuilder<'_, Postgres>,
filter: &ProductFilter,
) {
if let Some(name) = &filter.name {
qb.push(" AND name ILIKE ");
qb.push_bind(format!("%{}%", name));
}
if let Some(min) = filter.min_price {
qb.push(" AND price >= ").push_bind(min);
}
if let Some(max) = filter.max_price {
qb.push(" AND price <= ").push_bind(max);
}
if let Some(tag) = &filter.tag {
qb.push(" AND ").push_bind(tag.clone()).push(" = ANY(tags)");
}
// NEW B60 — JSONB ? operator (key exists top-level)
if let Some(key) = &filter.has_metadata_key {
qb.push(" AND metadata ? ").push_bind(key.clone());
}
// NEW B60 — JSONB @> operator (contains)
if let Some(contains) = &filter.metadata_contains {
qb.push(" AND metadata @> ").push_bind(contains.clone());
}
}
Extend ProductSearchQuery DTO (lock B59) thêm 2 query param:
// File: crates/shop-common/src/dto/product.rs
#[derive(Debug, Clone, Deserialize, Validate)]
pub struct ProductSearchQuery {
// ... 7 field existing B59 (name, tag, min_price, max_price, sort, page, per_page)
#[serde(default)]
pub has_metadata_key: Option<String>,
#[serde(default)]
pub metadata_contains: Option<serde_json::Value>,
}
Verify endpoint mới qua curl:
# 1. Search product có key warranty_months trong metadata
curl 'http://localhost:3000/api/v1/products?has_metadata_key=warranty_months' | jq
# 2. Search product metadata contains specific value
curl 'http://localhost:3000/api/v1/products?metadata_contains=%7B%22color%22%3A%22black%22%7D' | jq
# URL-encoded: {"color":"black"}
# 3. Combo filter — name + metadata
curl 'http://localhost:3000/api/v1/products?name=iphone&has_metadata_key=warranty_months' | jq
Test data nhanh bằng psql để verify GIN index hoạt động:
-- Insert test data
INSERT INTO products (name, slug, price, stock, tags, metadata)
VALUES
('iPhone 15', 'iphone-15', 25000000, 10,
ARRAY['apple', 'phone']::TEXT[],
'{"warranty_months": 12, "color": "black"}'::jsonb),
('Galaxy S24', 'galaxy-s24', 22000000, 5,
ARRAY['samsung', 'phone']::TEXT[],
'{"color": "black"}'::jsonb);
-- Extract warranty value qua ->> (text)
SELECT name, metadata->>'warranty_months' AS warranty
FROM products
WHERE metadata ? 'warranty_months';
-- iPhone 15 | 12
-- @> contains pattern
SELECT name FROM products
WHERE metadata @> '{"color": "black"}'::jsonb;
-- iPhone 15
-- Galaxy S24
Pattern thứ 2 — payment query theo payment_intent_id. File crates/shop-db/src/payments.rs đến B54 mới là skeleton, B60 thêm PaymentRow + function lookup chuẩn bị cho B71 Stripe webhook:
// File: crates/shop-db/src/payments.rs
use sqlx::PgPool;
use chrono::{DateTime, Utc};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct PaymentRow {
pub id: i64,
pub order_id: i64,
pub payment_type: String,
pub payment_payload: JsonValue,
pub status: String,
pub created_at: DateTime<Utc>,
}
pub async fn find_payments_by_stripe_intent(
pool: &PgPool,
intent_id: &str,
) -> Result<Vec<PaymentRow>, sqlx::Error> {
sqlx::query_as::<_, PaymentRow>(
r#"
SELECT id, order_id, payment_type, payment_payload,
status, created_at
FROM payments
WHERE payment_type = 'stripe'
AND payment_payload @> $1
"#,
)
.bind(serde_json::json!({"payment_intent_id": intent_id}))
.fetch_all(pool)
.await
}
Pattern payment_payload @> $1 dùng GIN jsonb_path_ops (lock từ migration create_payments B54). Webhook Stripe nhận event payment_intent.succeeded với id = "pi_3OZx..." sẽ lookup payment record bằng đúng pattern này — KHÔNG cần extract payment_intent_id ra cột riêng, JSONB query đủ nhanh nhờ GIN index.
Suggested commit: B60: products.metadata JSONB column + GIN index + ProductFilter has_metadata_key/metadata_contains + payments find_by_stripe_intent. Nhớ chạy cargo sqlx prepare --workspace trước commit để cập nhật .sqlx/ cache (lock B58).
Tổng Kết Group 6 + Roadmap Group 7
Group 6 PostgreSQL + sqlx đã cover trọn vẹn 10 bài (B51-B60):
- B51 — PostgreSQL 17 Docker Compose + sqlx-cli setup + crate
shop-db+ migrationcreate_products+ wire PgPool vào AppState. - B52 — sqlx migration system + clap CLI subcommand
db migrate+ migrationadd_products_tagsvới GIN index array. - B53 — Macro
query_as!sâu +FromRowderive + nullable Option mapping + override type annotation. - B54 — Transaction + savepoint + isolation level + ATOMIC order creation + migration
create_orders/create_payments. - B55 — Error mapping SQLSTATE → AppError variant +
map_db_errorhelper + retry strategy cho deadlock. - B56 — Pool config multi-env + AppConfig + Environment enum +
PgPoolOptionstuning chi tiết. - B57 — Pool benchmark + Prometheus metrics +
/health/live+/health/readysplit. - B58 — Offline cache
.sqlx/+ GitHub Actions CI workflow + integration test với testcontainers. - B59 — Dynamic query
QueryBuilder+ ORDER BY whitelist + search endpoint hoàn chỉnh 6 filter. - B60 — JSONB operator + GIN index 2 ops + Array operator + metadata search + payment query pattern.
Foundation ready cho Group 7 và xa hơn:
- Database schema: 4 bảng (
products,orders,order_items,payments) + 9+ index (B-tree + GIN array + GIN JSONB + partial expression). - Error handling: 19
AppErrorvariant + SQLSTATE mapping + retry helper. - Pool: config env-driven + offline cache + CI workflow ready.
- Search endpoint: 8 filter (name, tag, min_price, max_price, sort, page, per_page, has_metadata_key, metadata_contains) + apply_filter reuse pattern.
- 5 migration applied: create_products, add_products_tags, create_orders, create_payments, add_products_metadata.
Group 7 (B61-B75) sẽ cover CRUD HTTP Endpoint Đầy Đủ:
- B61 — Resource modeling REST + idempotency PUT/DELETE + ETag + pagination cursor.
- B66 — POST
/api/v1/ordersvớicreate_order_atomic+ HTTP handler đầy đủ. - B67-B69 — Cart endpoints (GET cart, POST item, PATCH/DELETE item).
- B70-B71 — User registration + profile + Stripe webhook integration.
- B72-B73 — Service layer abstraction + dependency injection pattern.
- B74 — Domain error handling pattern thay vì trộn HTTP error.
- B75 — End-to-end CRUD reuse pattern cho mọi resource Shop API.
Tổng Kết
- JSONB binary format thay JSON text — query nhanh, index GIN khả dụng.
- 6 JSONB operator core:
->,->>,@>,<@,?,?|,?&. - GIN index 2 ops: default
jsonb_ops(full operator) vsjsonb_path_ops(chỉ@>, smaller + faster). - Decision:
payment_payload→jsonb_path_ops;products.metadata→ defaultjsonb_ops. - JSONPath SQL/JSON cho query sâu — operator
#>(JSONB),#>>(TEXT). - 4 array operator:
= ANY,@>,&&,||+ GIN array index lock B52. - Array vs JSONB decision: 1D string → array; variable schema → JSONB; sortable → array; full-text inside → JSONB GIN.
- Migration 5 mới:
add_products_metadatavới cột JSONB + 2 index (GIN default + partial expression). ProductFilterextend 2 filter:has_metadata_key(?),metadata_contains(@>).- Payment query pattern:
payment_payload @> $1cho lookup theopayment_intent_id(B71 Stripe). - HOÀN THÀNH Group 6 (10/10 bài) — foundation ready cho G7 CRUD đầy đủ B61-B75.
- File path lock: migration 5
add_products_metadata.sql, extenddto/product.rs, extendshop-db/src/products.rs, extendshop-db/src/payments.rs.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- JSONB vs JSON Postgres — khác nhau ra sao về storage và query performance? Khi nào chọn JSON kiểu text?
- Phân biệt
->vs->>vs@>JSONB. Cho ví dụ mỗi operator vớipayment_payload. - GIN index default
jsonb_opsvsjsonb_path_ops— trade-off và khi nào chọn cái nào trong Shop API? - Array
@>vs&&khác nhau ra sao? Cho ví dụ cụ thể vớitagsproducts. - Quy tắc chọn Array vs JSONB Shop API — cho 3 case khác nhau (tags, metadata, hierarchical setting).
Đáp án
- JSONB vs JSON Postgres — storage + query performance + khi nào chọn JSON: kiểu
JSONPostgres lưu nguyên văn bản gốc (TEXT internal) cùng với khoảng trắng và thứ tự key — INSERT chỉ validate cú pháp xong là lưu, nhanh; nhưng mỗi lần SELECT hoặc query bằng operator phải parse lại từ đầu, chậm + không có index hiệu quả (GIN không cover được). KiểuJSONBparse JSON 1 lần lúc INSERT thành binary tree (key/value đã decode), lưu dạng nhị phân — INSERT chậm hơn ~5-10% nhưng query nhanh hơn nhiều, hỗ trợ GIN index, comparison giá trị bằng binary compare. JSONB KHÔNG preserve thứ tự key gốc, khoảng trắng, duplicate key (chỉ giữ key cuối nếu input có duplicate). Shop API lock JSONB MANDATORY cho mọi cột flexible schema (metadata, payment_payload, audit log, preferences) — query speed quan trọng hơn insert speed cho workload API. Khi nào dùng JSON text: cần archive nguyên format gốc byte-by-byte (vd webhook signature verify cần raw payload), nhưng case này thường lưuBYTEAraw bytes thay vì JSON text vì JSON text vẫn re-encode khi serialize ra (escape sequence Unicode, normalize số float). Trong Shop API gần như không có case dùng JSON kiểu text — JSONB cho mọi nơi. ->vs->>vs@>JSONB + ví dụ với payment_payload: cả 3 đều là operator JSONB nhưng mục đích khác. (a)->field/index access trả về JSONB — kết quả vẫn là JSONB nên có thể chain tiếp->hoặc->>. Ví dụ:SELECT payment_payload->'card' FROM payments→ trả{"last4": "4242", "brand": "visa"}(JSONB object); chain tiếppayment_payload->'card'->'brand'→"visa"(JSONB string vẫn có dấu nháy). (b)->>field access trả về TEXT — đã strip dấu nháy, KHÔNG chain tiếp được (TEXT không có operator JSONB). Ví dụ:SELECT payment_payload->>'card_brand' FROM payments→ trảvisa(TEXT, không có dấu nháy). Dùng khi cần so sánh với chuỗi Rust qua.bind()hoặc cast sang INT:(payment_payload->'card'->>'exp_month')::int. (c)@>contains — JSONB bên trái có chứa cấu trúc bên phải không, trả BOOL. Ví dụ:SELECT * FROM payments WHERE payment_payload @> '{"card_brand": "visa"}'::jsonbmatch mọi row có keycard_brand = "visa";WHERE payment_payload @> '{"card": {"last4": "4242"}}'::jsonbmatch nested deep.@>là operator quan trọng nhất cho lookup nested key vì có index GIN tăng tốc (B71 Stripe webhook:payment_payload @> '{"payment_intent_id": "pi_..."}'). Tóm tắt:->chain JSONB cây sâu,->>extract value cuối thành TEXT,@>filter row theo cấu trúc match.- GIN index default
jsonb_opsvsjsonb_path_ops— trade-off + khi nào chọn Shop API: Postgres cho phép 2 operator class khi tạo GIN cho JSONB. (a) Defaultjsonb_ops: index mọi key và mọi value trong JSONB → support đủ 5 operator@>,<@,?,?|,?&nhưng size index lớn (~50-100% so với data) + write chậm hơn vì cập nhật nhiều entry. (b)jsonb_path_ops: chỉ index path (chuỗi key → value) → chỉ support operator@>(KHÔNG support?series) nhưng size nhỏ hơn ~30% + lookup nhanh hơn vì cấu trúc đơn giản hơn. Trade-off ngắn gọn: default = full feature chậm to, path_ops = chỉ@>nhanh nhỏ. Lock decision Shop API: (i)payments.payment_payloaddùngjsonb_path_opsvì query duy nhất trong production là lookup webhook StripeWHERE payment_payload @> '{"payment_intent_id": "..."}'(B71) — không bao giờ check key existence riêng lẻ, chỉ@>. (ii)products.metadatadùng defaultjsonb_opsvì query có 2 pattern:@>(filter cụ thể: metadata contains color black) +?(check key existence: sản phẩm nào có warranty để render UI badge). Cần?nên phải default. Generalize: cột query nhiều pattern → default jsonb_ops; cột chỉ lookup@>nested key cố định → jsonb_path_ops. Verify đúng index dùng quaEXPLAIN ANALYZEthấy "Bitmap Index Scan on ..." là OK. - Array
@>vs&&khác nhau ra sao + ví dụ với tags products: cả 2 đều là Postgres array operator nhận 2 array bên trái + bên phải, trả BOOL. (a)@>contains — array bên trái chứa TẤT CẢ phần tử của array bên phải (left ⊇ right). Ví dụtags @> ARRAY['apple', 'phone']match row có tags chứa CẢ "apple" VÀ "phone" (vd['apple', 'phone', 'iphone']match;['apple', 'tablet']KHÔNG match vì thiếu 'phone'). Logic AND. (b)&&overlap — 2 array có ÍT NHẤT 1 phần tử chung (left ∩ right ≠ ∅). Ví dụtags && ARRAY['apple', 'samsung']match row có tags chứa "apple" HOẶC "samsung" (vd['apple', 'phone']match;['samsung', 'tablet']match;['google', 'pixel']KHÔNG match). Logic OR. Use case khác nhau: (i)@>dùng cho filter "phải đủ hết các tag" — vd UI cho phép chọn nhiều tag và muốn lọc sản phẩm match đủ; (ii)&&dùng cho filter "thuộc về 1 trong số nhóm" — vd category browse "Phone hoặc Tablet" hiển thị mọi sản phẩm thuộc bất kỳ category nào. Ở B59, Shop API dùng= ANY(tags)cho filter 1 tag single value (đơn giản hơn vì client chỉ gửi 1 string); nếu mở rộng cho multi-tag tương lai sẽ dùng@>(AND nhiều tag) hoặc&&(OR nhiều tag) tùy semantic UI. Cả 2 operator đều dùng GIN array index B52 nên scan rất nhanh. - Quy tắc Array vs JSONB Shop API — 3 case: dùng matrix quyết định lock từ B60. (a) Case tags products (
tags TEXT[]) — list 1D chuỗi đơn giản, mỗi phần tử cùng kiểu (string), không có nested object, cần sort/order theo vị trí preserve (admin nhập tags theo thứ tự ưu tiên), cần= ANY+@>+&&operator hiệu quả → chọn ArrayTEXT[]với GIN index lock B52. JSONB array overkill cho case này (overhead binary tree, không tận dụng được path_ops). (b) Case metadata products (metadata JSONB) — variable schema mỗi row có set key khác nhau (warranty_months, color, dimensions, custom_attribute_*), nested object có thể (vdshipping: {weight, length, width}), không cần preserve order key, cần query@>(contains nested) +?(key exists) — quy tắc kinh điển variable schema dùng JSONB → chọn JSONB với GIN defaultjsonb_opssupport cả@>+?. Array TEXT[] không phù hợp vì không thể nested object. (c) Case hierarchical settings users.preferences (tương lai G18) — tree nested deep (notification.email.order_confirm, notification.email.shipping_update, ui.theme, ui.language, ...), schema cố định partial nhưng có user extend custom field, cần extract giá trị từ path cố định qua#>>→ chọn JSONB với GIN default jsonb_ops. Array dù multi-dimensionTEXT[][]không model được tree, chỉ matrix vuông. Tổng matrix quyết định: 1D string list + cần sort + operator= ANY/@>/&&→ Array; variable schema + nested object + cần@>/?→ JSONB; hierarchical tree → JSONB; sortable theo vị trí cố định → Array; full-text search trong field → JSONB + GIN; raw event payload audit → JSONB; ID list FK 1D → Array (vdcategory_ids BIGINT[]giữ thứ tự primary/secondary lock B47).
Bài Tiếp Theo
Bài 61: CRUD HTTP Endpoint Overview — Resource Modeling — mở Group 7 CRUD đầy đủ: resource modeling REST, idempotency cho PUT/DELETE, ETag header, pagination strategy (offset vs cursor), versioning chiến lược, áp Shop API full CRUD pattern cho products/orders/users/carts.
