我用单个 Postgres 表替换了 MongoDB。
听着,我需要给你看一些东西,可能会让你伤心:
CREATE TABLE MongoDB (
_id UUID PRIMARY KEY,
data JSONB
);
在你评论区@我之前,请听我说完。如果我告诉你,你90%的NoSQL用例都可以用一个数据库管理员不想让你知道的、不起眼的Postgres技巧来解决,你会相信吗?
问题:为什么我们认为我们需要 NoSQL
我们都经历过这种情况。凌晨两点,你正埋头于数据迁移,开始怀疑自己人生中每一个导致你走到这一步的选择。你的产品经理刚刚说“再加一个字段”,现在你却像回到了2009年一样编写迁移脚本。
“或许我应该直接用MongoDB,”你低声自语道。“模式灵活!无需迁移!文档型存储!”
但关键在于:你可能不需要 MongoDB,你需要的是 JSONB。
隆重介绍 JSONB:我们不配拥有的英雄
JSONB 可不是简单地把 JSON 数据塞进 Postgres 数据库的列里。不,我的朋友。它是 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 路径进行索引(等等,什么?)
接下来才是精彩的部分。你可以在 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 可以处理”。
JSON 内部全文搜索🤯
抓紧你的键盘:
-- 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 (
'john@example.com',
'{
"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 = 'john@example.com';
事件日志模式(完美)
这正是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 的绝对优势(用例)
以下情况绝对应该使用 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 的依赖,那我就不知道什么才能让你重新考虑了。
附赠:JSONB 终极速查表
-- 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 使用案例。我会在这里回答问题,可能还会讲一些关于数据库的冷笑话。
PS:开头那个 MongoDB 表?它其实能用。我不是说你应该用它,但是……你可以试试。😈
附注:如果您正在收集用户反馈,并且想要比 JSONB 列更好的方案(虽然说实话,JSONB 也行),不妨试试UserJot。我们底层运用了大量的 JSONB 技术来构建它。
文章来源:https://dev.to/shayy/i-replaced-mongodb-with-a-single-postgres-table-p0d