北屋教程网

专注编程知识分享,从入门到精通的编程学习平台

告别 MongoDB?PostgreSQL JSONB 让关系型数据库玩转非结构化数据!

我得让你看看一些可能让你感到不悦的东西:

CREATE TABLE MongoDB (
  _id  UUID PRIMARY KEY,
  data JSONB
);

如果我告诉你,90% 的 NoSQL 使用场景可以通过一个数据库管理员不想让你知道的 Postgres 技巧来解决,你会怎么想?

问题:为什么我们认为我们需要 NoSQL

我们都经历过这种情况。凌晨两点,你正忙于数据库迁移,质疑着人生中所有让你走到这一步的选择。你的产品经理刚要求“再加一个字段”,你就得像回到 2009 年一样,不停地写迁移脚本。

“或许我应该直接使用 MongoDB,”你自言自语道,“灵活的模式!无需迁移!文档存储!”

但事实是:你可能并不需要 MongoDB。你需要的是 JSONB。

JSONB:我们不配拥有的英雄

JSONB 并不是简单地将 JSON 放到一个 Postgres 列中。它是JSON 更酷、更快、更有吸引力的,那个去健身房锻炼过、学会了如何使用索引的哥哥。

以下是 JSONB 的特殊之处:

  • 二进制存储格式
  • 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 产品,并且需要存储用户偏好设置:

-- 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 的地方:

  1. 用户偏好/设置:每个用户想要的东西都不同。不要创建 50 个布尔型列。
  2. 事件日志:不同的事件 = 不同的数据。JSONB 处理起来非常得心应手。
  3. 产品目录:书籍有 ISBN,鞋子有尺码,手机有屏幕分辨率。一个模式搞定一切。
  4. API 响应缓存:存储第三方 API 响应而不对其进行解析。
  5. 表单提交:尤其是在构建系统时,用户反馈可以包含自定义字段。
  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),  -- 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事务
  • 需要时的连接Join操作
  • 您现有的 Postgres 知识
  • 减少一个需要管理的数据库
  • 省下一笔开支(Postgres 是免费的!)

在我们的系统里,我们广泛使用 JSONB 来存储用户反馈元数据、自定义字段和集成配置。它既能提供类似 MongoDB 的灵活性,又能提供 Postgres 的可靠性。两者兼得。

现在去尽情使用 @> 吧!

本文为译文,英文原文地址(可能需要使用魔法访问或付费会员才可观看):
https://dev.to/shayy/i-replaced-mongodb-with-a-single-postgres-table-p0d

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言