看,我需要向你展示一些可能触动你情绪的内容:
CREATE TABLE MongoDB (
_id UUID PRIMARY KEY,
data JSONB
);
在你评论区@我之前,请先听完我的解释。如果你愿意听,我会告诉你,90%的NoSQL使用场景其实都可以通过Postgres中这个独特的JSONB特性来实现,而这个特性,数据库管理员可能并不希望你发现。
问题:为什么我们需要NoSQL?
我们都曾经历过这样的时刻。深夜两点,你正深陷于一次模式迁移,开始怀疑每一个让你走到这一步的生活决定。产品经理刚刚提出“再加一个字段”,而你却像在2009年一样编写迁移脚本。
“也许我应该直接使用MongoDB吧,”你低声自语,“模式灵活!不需要迁移!文档存储!”
但这里有个现实:你可能根本不需要MongoDB。你需要的,是JSONB。
JSONB 并不只是将 JSON 数据存储在 Postgres 的一个列中。不,我朋友,它更像是 JSON 的一个更酷、更快、更强大的兄弟,他不仅去健身房锻炼过,还掌握了如何高效地使用索引。
JSONB 的独特之处在于:
- 二进制存储格式(B 表示 Binary,不是 🐝)
- GIN 索引,使查询速度大幅提升
- 原生操作符,会让 JavaScript 开发者感到兴奋不已
- 完整的 SQL 功能与 NoSQL 的灵活性相结合
它就像是 MongoDB 和 Postgres 的结合体,孕育出了一位功能强大的超级英雄。
大多数开发者都不知道的惊人功能
能改变你开发方式的操作符
索引特定 JSON 路径(你可能会问,这是什么?)
这就是事情变得有趣的地方。你可以在 JSON 数据的特定路径上创建索引:
-- 索引一个特定字段
CREATE INDEX idx_user_email ON users ((data->>'email'));
-- 用于存在性查询的索引
CREATE INDEX idx_attributes ON products USING GIN (attributes);
-- 用于包含性查询的索引
CREATE INDEX idx_preferences ON users USING GIN (preferences);
现在你的JSON查询速度会比那些认为“不需要索引,因为MongoDB可以处理”的同事快上很多。
JSON中的全文搜索 🤯
请紧握你的键盘:
-- 在JSON字段中添加全文搜索索引
CREATE INDEX idx_content_search ON articles
USING GIN (to_tsvector('english', data->>'content')));
-- 进行像专家一样的搜索
SELECT * FROM articles
WHERE to_tsvector('english', data->>'content') @@ plainto_tsquery('postgres jsonb amazing');
实际代码示例(核心内容)
让我们从一些实用的场景入手。假设你正在开发一个SaaS产品(例如 UserJot - 我的反馈管理工具的自我宣传),需要存储用户的偏好设置:
-- 混合方法:结构化与灵活结合
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 INTO users (email, preferences) VALUES (
'john@example.com',
'{
"theme": "dark",
"notifications": {
"email": true,
"push": false,
"frequency": "daily"
},
"features": {
"beta": true,
"advancedAnalytics": false
}
}'
);
-- 查询使用深色主题且启用电子邮件通知的用户
SELECT email FROM users
WHERE preferences @> '{"theme": "dark", "notifications": {"email": true}}';
-- 更新嵌套偏好
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
);
-- 为快速查询事件类型和事件数据创建索引
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 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"]
}');
-- 查询所有包含特定折扣的购买事件
SELECT * FROM events
WHERE event_type = 'purchase'
AND data @> '{"discount": "SUMMER20"}';
-- 计算所有事件的总收入
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 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"
}');
-- 查询所有支持5G的产品
SELECT name, price FROM products WHERE attributes @> '{"5g": true}';
-- 按品牌查询产品
SELECT * FROM products WHERE attributes->>'brand' = 'Apple';
-- 复杂查询:查找所有屏幕尺寸大于6英寸的产品
SELECT name, attributes->'screen'->>'size' AS screen_size
FROM products
WHERE (attributes->'screen'->>'size')::FLOAT > 6.0;
JSONB 绝对适用的场景(Use Cases)
以下情况建议优先使用 JSONB:
- 用户偏好设置:每位用户的需求各不相同。无需为每个属性创建50个布尔列。
- 事件日志:不同的事件对应不同的数据结构。JSONB 在处理此类场景时表现出色。
- 产品目录:如书籍有 ISBN、鞋子有尺寸、手机有屏幕分辨率等,JSONB 可以统一管理各种数据类型。
- 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), -- 实际外键
total NUMERIC(10,2) NOT NULL, -- 用于快速聚合计算
status TEXT NOT NULL, -- 用于索引查询
created_at TIMESTAMPTZ DEFAULT NOW(),
line_items JSONB, -- 灵活存储商品详情
metadata JSONB -- 存储其他附加信息
);
迁移策略:终极方案
以下是将MongoDB迁移到Postgres/JSONB的迁移方法:
# 勇敢者的伪代码示例
import psycopg2
from pymongo import MongoClient
# 连接MongoDB与PostgreSQL
mongo = MongoClient('mongodb://localhost:27017/')
postgres = psycopg2.connect("postgresql://...")
# 有条理地迁移数据
for doc in mongo.mydb.mycollection.find():
postgres.execute(
"INSERT INTO my_table (id, data) VALUES (%s, %s)",
(str(doc['_id']), json.dumps(doc))
)
尝试这个查询,告诉我它不是魔法
这是你的任务。创建该表并运行以下查询:
-- 创建一个表
CREATE TABLE magic (
id SERIAL PRIMARY KEY,
data JSONB
);
-- 插入包含嵌套和复杂结构的数据
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"}}}');
-- 精彩查询:查找平均分数大于15且为专家级别的用户
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;
If that doesn't make you reconsider your MongoDB addiction, I don't know what will.
福利:终极 JSONB 快速参考表
-- 操作符 @> -- 包含 <@ -- 被包含 ? -- 键存在 ?| -- 至少包含一个键 ?& -- 同时包含所有键 || -- 合并
- -- 删除键或元素
- -- 删除指定路径下的内容
-- 函数 jsonb_set() -- 更新指定路径的值 jsonb_insert() -- 在指定路径插入值 jsonb_strip_nulls() -- 移除空值 jsonb_pretty() -- 人性化格式化输出 jsonb_agg() -- 聚合为数组 jsonb_object_agg() -- 聚合为对象
-- 性能技巧 1. 为 @> 和 ? 运算符使用 GIN 索引 2. 为 ->> 操作符在特定字段上使用 btree 索引 3. 利用部分索引优化常见查询 4. 避免嵌套超过 3-4 层 5. 保持 JSONB 文档大小在 1MB 以内
实话实说
听着,我并不是在否定 MongoDB。它也有其适用的场景。但在你打算使用一个单独的 NoSQL 数据库之前,请先问自己:JSONB 是否能够满足这个需求?
十次中有九次,答案是肯定的。而且你还能保留:
- ACID 事务支持
- 在需要时使用连接(joins)
- 你已有的 Postgres 知识
- 更少的数据库需要维护
- 你的钱包里的钱(Postgres 是免费的!)
在 UserJot 中,我们广泛采用 JSONB 来存储用户反馈的元数据、自定义字段及集成配置。它不仅提供了类似 MongoDB 的灵活性,还融合了 Postgres 的可靠性,实现两者的完美结合。
来源:Dev.to 热门 原文地址:https://dev.to/shayy/i-replaced-mongodb-with-a-single-postgres-table-p0d
评论 (0)
还没有评论,来说两句吧!