聽著,我需要給你看一些可能會傷害你感情的東西:
CREATE TABLE MongoDB (
_id UUID PRIMARY KEY,
data JSONB
);
在你評論區@我之前,先聽我說完。如果我告訴你,90% 的 NoSQL 用例都可以用這個資料庫管理員不想讓你知道的 Postgres 詭異技巧來處理,你會怎麼想?
我們都經歷過這種情況。凌晨兩點,你正忙於資料庫遷移,質疑人生中所有讓你走到這一步的選擇。你的產品經理只是要求“再加一個字段”,你就像回到 2009 年一樣,不停地寫遷移腳本。
「也許我應該只用 MongoDB,」你低聲自言自語。 “模式靈活!無需遷移!文件儲存!”
但問題是:你可能不需要 MongoDB。你需要 JSONB。
JSONB 可不是隨便塞進 Postgres 欄位裡的 JSON。喔不,我的朋友。它是 JSON 更酷、更快、更吸引人的“老大哥”,它可是去健身房學習瞭如何使用索引的。
以下是 JSONB 的特別之處:
二進位儲存格式(B 代表二進位,而不是🐝)
GIN 索引讓查詢變得非常快
讓 JavaScript 開發者喜極而泣的原生運算符
完整的 SQL 功能與 NoSQL 靈活性結合
這就像 MongoDB 和 Postgres 生了一個孩子,而這個孩子長大後成為了超級英雄。
-- The containment operator @>
-- "Does this JSON contain this structure?"
SELECT * FROM users
WHERE preferences @> '{"theme": "dark"}';
-- The existence operator ?
-- "Does this key exist?"
SELECT * FROM products
WHERE attributes ? 'wireless';
-- The arrow operators -> and ->>
-- -> returns JSON, ->> returns text
SELECT
data->>'name' AS name,
data->'address'->>'city' AS city
FROM users;
-- The path operator #>
-- Navigate deep into nested JSON
SELECT * FROM events
WHERE data #> '{user,settings,notifications}' = 'true';
事情開始變得棘手了。你可以在 JSON 中的特定路徑上建立索引:
-- Index a specific field
CREATE INDEX idx_user_email ON users ((data->>'email'));
-- Index for existence queries
CREATE INDEX idx_attributes ON products USING GIN (attributes);
-- Index for containment queries
CREATE INDEX idx_preferences ON users USING GIN (preferences);
現在,您的 JSON 查詢比您的同事聲稱的「不需要索引,因為 MongoDB 可以處理」要快。
抓住你的鍵盤:
-- Add full-text search to JSON fields
CREATE INDEX idx_content_search ON articles
USING GIN (to_tsvector('english', data->>'content'));
-- Search like a boss
SELECT * FROM articles
WHERE to_tsvector('english', data->>'content') @@ plainto_tsquery('postgres jsonb amazing');
讓我們從實際的事情開始。假設你正在建立一個 SaaS 產品(例如UserJot——我的回饋管理工具的無恥宣傳),並且需要儲存使用者偏好設定:
-- The hybrid approach: structured + flexible
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
preferences JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}'
);
-- Insert a user with preferences
INSERT INTO users (email, preferences) VALUES (
'[email protected]',
'{
"theme": "dark",
"notifications": {
"email": true,
"push": false,
"frequency": "daily"
},
"features": {
"beta": true,
"advancedAnalytics": false
}
}'
);
-- Query users who have dark theme AND email notifications
SELECT email FROM users
WHERE preferences @> '{"theme": "dark", "notifications": {"email": true}}';
-- Update nested preferences
UPDATE users
SET preferences = jsonb_set(
preferences,
'{notifications,push}',
'true'
)
WHERE email = '[email protected]';
這是 JSONB 絕對閃耀的地方:
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type TEXT NOT NULL,
user_id UUID,
occurred_at TIMESTAMPTZ DEFAULT NOW(),
data JSONB NOT NULL
);
-- Index for fast event type + data queries
CREATE INDEX idx_events_type_data ON events (event_type)
WHERE event_type IN ('purchase', 'signup', 'feedback');
CREATE INDEX idx_events_data ON events USING GIN (data);
-- Insert different event types with different schemas
INSERT INTO events (event_type, user_id, data) VALUES
('signup', 'user-123', '{
"source": "google",
"campaign": "summer-2024",
"referrer": "blog-post"
}'),
('purchase', 'user-123', '{
"items": [
{"sku": "PROD-1", "quantity": 2, "price": 49.99},
{"sku": "PROD-2", "quantity": 1, "price": 19.99}
],
"discount": "SUMMER20",
"total": 99.97
}'),
('feedback', 'user-123', '{
"type": "feature_request",
"title": "Add dark mode",
"priority": "high",
"tags": ["ui", "accessibility"]
}');
-- Find all purchases with a specific discount
SELECT * FROM events
WHERE event_type = 'purchase'
AND data @> '{"discount": "SUMMER20"}';
-- Calculate total revenue from events
SELECT SUM((data->>'total')::NUMERIC) AS total_revenue
FROM events
WHERE event_type = 'purchase'
AND occurred_at >= NOW() - INTERVAL '30 days';
這個範例讓 MongoDB 開發人員質疑一切:
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL,
attributes JSONB DEFAULT '{}'
);
-- Insert products with completely different attributes
INSERT INTO products (name, price, attributes) VALUES
('iPhone 15', 999.00, '{
"brand": "Apple",
"storage": "256GB",
"color": "Blue",
"5g": true,
"screen": {
"size": "6.1 inches",
"type": "OLED",
"resolution": "2532x1170"
}
}'),
('Nike Air Max', 120.00, '{
"brand": "Nike",
"size": "10",
"color": "Black/White",
"material": "Mesh",
"style": "Running"
}'),
('The Pragmatic Programmer', 39.99, '{
"author": "David Thomas",
"isbn": "978-0135957059",
"pages": 352,
"publisher": "Addison-Wesley",
"edition": "2nd"
}');
-- Find all products with 5G
SELECT name, price FROM products WHERE attributes @> '{"5g": true}';
-- Find products by brand
SELECT * FROM products WHERE attributes->>'brand' = 'Apple';
-- Complex query: Find all products with screens larger than 6 inches
SELECT name, attributes->'screen'->>'size' AS screen_size
FROM products
WHERE (attributes->'screen'->>'size')::FLOAT > 6.0;
這裡是你絕對應該使用 JSONB 的地方:
使用者偏好/設定:每個使用者的需求都不一樣。不要建立 50 個布林值列。
事件日誌:不同的事件 = 不同的資料。 JSONB 處理起來非常得心應手。
產品目錄:書籍有 ISBN,鞋子有尺碼,手機有螢幕解析度。一個架構統領一切。
API 回應快取:儲存第三方 API 回應而不對其進行解析。
表單提交:特別是當您建立像 UserJot 這樣的東西時,使用者回饋可以有自訂欄位。
功能標誌和配置:
CREATE TABLE feature_flags (
key TEXT PRIMARY KEY,
config JSONB
);
INSERT INTO feature_flags VALUES
('new_dashboard', '{
"enabled": true,
"rollout_percentage": 25,
"whitelist_users": ["user-123", "user-456"],
"blacklist_countries": ["XX"],
"start_date": "2024-01-01",
"end_date": null
}');
讓我們先來現實一下。 JSONB 並不總是最佳選擇。以下情況應該使用常規列:
外鍵:不能在外鍵約束中引用 JSONB 字段
重度聚合:JSONB 欄位上的 SUM、AVG、COUNT 速度較慢
頻繁更新:更新單一 JSONB 欄位會重寫整個 JSON
類型安全:當你確實需要資料為整數時
秘訣?混合方法:
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id), -- Real FK
total NUMERIC(10,2) NOT NULL, -- For fast aggregations
status TEXT NOT NULL, -- For indexed lookups
created_at TIMESTAMPTZ DEFAULT NOW(),
line_items JSONB, -- Flexible item details
metadata JSONB -- Everything else
);
以下是從 MongoDB 遷移到 Postgres/JSONB 的方法:
# Pseudo-code for the brave
import psycopg2
from pymongo import MongoClient
# Connect to both
mongo = MongoClient('mongodb://localhost:27017/')
postgres = psycopg2.connect("postgresql://...")
# Migrate with style
for doc in mongo.mydb.mycollection.find():
postgres.execute(
"INSERT INTO my_table (id, data) VALUES (%s, %s)",
(str(doc['_id']), Json(doc))
)
這是你的作業。建立此表並執行此查詢:
-- Create a table
CREATE TABLE magic (
id SERIAL PRIMARY KEY,
data JSONB
);
-- Insert nested, complex data
INSERT INTO magic (data) VALUES
('{"user": {"name": "Alice", "scores": [10, 20, 30], "preferences": {"level": "expert"}}}'),
('{"user": {"name": "Bob", "scores": [5, 15, 25], "preferences": {"level": "beginner"}}}');
-- Mind-blowing query: Find users with average score > 15 AND expert level
SELECT
data->'user'->>'name' AS name,
(SELECT AVG(value::INT) FROM jsonb_array_elements_text(data->'user'->'scores') AS value) AS avg_score
FROM magic
WHERE data @> '{"user": {"preferences": {"level": "expert"}}}'
AND (
SELECT AVG(value::INT)
FROM jsonb_array_elements_text(data->'user'->'scores') AS value
) > 15;
如果這都不能讓你重新考慮你對 MongoDB 的依賴,我不知道還有什麼能做到。
-- Operators
@> -- Contains
<@ -- Is contained by
? -- Key exists
?| -- Any key exists
?& -- All keys exist
|| -- Concatenate
- -- Delete key/element
#- -- Delete at path
-- Functions
jsonb_set() -- Update value at path
jsonb_insert() -- Insert value at path
jsonb_strip_nulls() -- Remove null values
jsonb_pretty() -- Format for humans
jsonb_agg() -- Aggregate into array
jsonb_object_agg() -- Aggregate into object
-- Performance tips
1. Use GIN indexes for @> and ? operators
2. Use btree indexes for ->> on specific fields
3. Partial indexes for common queries
4. Don't nest more than 3-4 levels deep
5. Keep JSONB documents under 1MB
聽著,我不是說 MongoDB 不好。它有自己的用武之地。但在你選擇單獨的 NoSQL 資料庫之前,先問問自己:JSONB 能做到嗎?
十有八九,答案是肯定的。而且,你還可以保留:
ACID事務
當您需要時加入
您現有的 Postgres 知識
減少一個需要管理的資料庫
口袋裡的錢(Postgres 是免費的!)
在UserJot ,我們廣泛使用 JSONB 來儲存使用者回饋元資料、自訂欄位和整合配置。它既能提供類似 MongoDB 的彈性,還能提供 Postgres 的可靠性。兩者兼得。
現在開始@>
所有東西吧!留言分享你最瘋狂的 JSONB 用例。我會在這裡回答你的問題,或許還會講更多資料庫相關的冷笑話。
附註:開頭那個 MongoDB 表?它真的很管用。我不是說你應該用它,但是…你可以用。 😈
P.S:如果你正在收集使用者回饋,並且想要比 JSONB 欄位更好的東西(雖然說實話,JSONB 也可以),請查看UserJot 。我們在底層使用了大量 JSONB 魔法來建造它。
原文出處:https://dev.to/shayy/i-replaced-mongodb-with-a-single-postgres-table-p0d