百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

wuantov 2025-07-19 23:12 9 浏览

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

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

相关推荐

SQL关联各种JOIN傻傻分不清楚,读这一篇就够了

在关系型数据库中支持多表关联,不同场景下通过不同join方式让分布在不同表中的数据呈现在同一个结果里。熟练使用sql联合查询是日常开发的基础工作。为了方便演示讲解,假设有两个表,一张是保存学生踢足球的...

MyBatis的SQL执行流程不清楚?看完这一篇就够了

推荐学习真香警告!Alibaba珍藏版mybatis手写文档,刷起来全网独家的“MySQL高级知识”集合,骨灰级收藏,手慢则无前言MyBatis可能很多人都一直在用,但是MyBatis的SQL执行...

SQL优化这十条,面试的时候你都答对了吗?

尽量不要在要给在SQL语句的where子句中使用函数,这样会使索引失效。如果已经确定查询结果只有一条数据(当表中数据的该字段是唯一的),在查询SQL末尾增加limit1,这样MySQL的查询执行引...

SQL查询Excel结果数据还可这样输出到窗体控件ListBox和ListView

上一期作品,我们分享了通过SQL查询Excel的结果数据输出到Excel自身的工作表区域。大家估计应该感觉到了SQL查询的强大功能,它对精确或模糊查询均无畏惧,优点是查询检索效率高,将查询结果输出的形...

数据库|SQLServer数据库:模糊查询的三种情况

哈喽,你好啊,我是雷工!就是字面意思,当数据库的查询条件并不是十分具体时就用到模糊查询,比如查询姓氏为雷的人名,就需要从姓名列模糊查询。01like关键字查询当使用like关键字进行查询时,字段中的...

数据库教程-SQL Server多条件模糊查询

表单查询是以数据存储管理为基础的信息管理系统各业务功能实现的基础,也是数据库CRUD操作的重点与难点,尤其是多表连接查询、条件查询、分组查询、聚合函数等的综合应用。本文以某一比赛样式要求为基础,对数据...

如何利用教育网站源码成功搭建在线教育网站

如今是一个信息化时代,人们都想接受各种各样的教育,在线教育也就因此发展了起来,并且逐渐成为了一种趋势。而成熟的在线教育网站皆是由高质量的教育网站源码搭建而成的。如何利用教育网站源码成功搭建在线教育网站...

宝塔搭建WordPress跨境电商外贸商城模板汉化woodmart7.5.1源码

大家好啊,欢迎来到web测评。本期给大家带来一套php开发的WoodmartV7.5.1汉化主题|跨境电商|外贸商城|产品展示网站模板WordPress主题,是wordpress开发的。上次是谁要的系...

小狐狸ChatGPT付费创作系统V2.4.7全开源版 (vue全开源端)

测试环境:Nginx1.20+PHP7.4+MySQL5.7本版本为官方的最新开源包对应V2.4.7版本,包含了前后端所有开源包,是目前最新全开源版本,需要二开的这部分朋友也有选择了,如果不需要二...

php宝塔搭建部署thinkphp红色大气装修公司官网php源码

大家好啊,欢迎来到web测评。本期给大家带来一套php开发的thinkphp红色大气装修公司官网源码,上次是谁要的系统项目啊,帮你找到了,还说不会搭建,让我帮忙录制一期教程,趁着今天有空,简单的录制测...

php宝塔搭建免登录积分商城系统php源码

大家好啊,欢迎来到web测评。本期给大家带来一套php开发的免登录积分商城系统php源码,上次是谁要的系统项目啊,帮你找到了,还说不会搭建,让我帮忙录制一期教程,趁着今天有空,简单的录制测试了一下,部...

零代码搭建接口收费平台——接口大师YesApi

主流的API接口收费模式目前各大API接口平台,采用的收费模式主可以分为:免费接口、免费试用、接口流量套餐、先充值后按量计费的模式。例如,聚合数据的API收费模式是:按接口流量套餐。例如身份证二要素...

php宝塔搭建部署实战抽奖系统开源php源码

大家好啊,我是测评君,欢迎来到web测评。本期给大家带来一套抽奖系统开源php源码。感兴趣的朋友可以自行下载学习。技术架构PHP5.4+nginx+mysql5.7+JS+CSS+...

【推荐】一款开源个人与企业私有化部署使用的在线知识库管理平台

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍zyplayer-doc是一款基于Java+Vue开源、专注于个人与企业私有化部署使用的在线知识库管...

网上的付费文档无法下载?这几个方法10秒搞定,任意免费复制

工作或者学习过程中,我们很多时候需要在网上找资料,但是想要的资料却要付费或者提示无法下载怎么办?别怕,这几个方法,让你10秒就能搞定付费文档,任意复制。1.打印界面复制遇到文档需要付费或者无法复制的...