我用一个 Postgres 表替换了 MongoDB

1_vC_o512iCQ1NsAEv95bkfw.png 看,我需要向你展示一些可能触动你情绪的内容:

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:

  1. 用户偏好设置:每位用户的需求各不相同。无需为每个属性创建50个布尔列。
  2. 事件日志:不同的事件对应不同的数据结构。JSONB 在处理此类场景时表现出色。
  3. 产品目录:如书籍有 ISBN、鞋子有尺寸、手机有屏幕分辨率等,JSONB 可以统一管理各种数据类型。
  4. API 响应缓存:可直接存储第三方 API 的响应数据,无需额外解析。
  5. 表单提交:特别是在构建类似 UserJot 这样的系统时,用户反馈可能包含自定义字段。
  6. 功能标志与配置
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)

还没有评论,来说两句吧!

文章摘要

本文探讨了在Postgres中使用JSONB数据类型替代NoSQL数据库(如MongoDB)的方案。JSONB是Postgres中一种高效的JSON数据存储格式,支持二进制存储、GIN索引、原生操作符以及全文搜索等功能,结合SQL的强大功能和NoSQL的灵活性。文章通过实际代码示例,展示了JSONB在用户偏好设置、事件日志、产品目录等场景下的应用,并提供了索引特定JSON路径、执行全文搜索等高级技巧。同时,文章也指出了JSONB的局限性,如不支持外键约束、聚合操作性能较差等,并建议采用混合方法,结合传统列和JSONB列的优势。最后,文章还提供了从MongoDB迁移到Postgres/JSONB的伪代码示例,以及JSONB操作符和函数的速查表。文章旨在说明,对于许多NoSQL用例,Postgres的JSONB提供了一种更强大和灵活的替代方案。

文章目录