PostgreSQL: PostgreSQL JSON与JSONB数据处理

最后更新:2026-08-26

1. 你将学到


2. 故事

Bob 是一家 SaaS 平台的后端工程师。平台需要存储用户配置和产品属性,但这些字段每个客户都不一样:

如果用传统关系模型,每加一个字段就要 ALTER TABLE,Bob 选择用 JSONB 在一张表中存储这些动态字段,既灵活又高效。


3. Concept:JSON vs JSONB

(1) 两种 JSON 类型对比

维度 JSON JSONB
存储方式 文本存储,原样保留 二进制存储,解析后存储
写入速度 较快(无需解析) 较慢(需要解析和转换)
查询速度 较慢(每次查询需解析) 很快(已解析为树结构)
索引支持 无原生索引 支持 GIN 索引
空格/顺序 保留原文空格和键序 不保留,键按字母排序
重复键 保留所有重复键 只保留最后一个值
推荐场景 仅存储不需要查询 绝大多数场景

▶ 示例:JSON 保留空格,JSONB 不保留

SQL
SELECT '{"name": "Alice", "age": 30}'::json;
-- {"name": "Alice", "age": 30}

SELECT '{"name": "Alice", "age": 30}'::jsonb;
-- {"age": 30, "name": "Alice"}

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:创建带 JSONB 列的表

SQL
CREATE TABLE users (
  user_id SERIAL PRIMARY KEY,
  username TEXT NOT NULL,
  profile JSONB NOT NULL DEFAULT '{}'
);

INSERT INTO users (username, profile) VALUES
  ('alice', '{"theme": "dark", "language": "en", "notifications": true}'),
  ('bob', '{"timezone": "UTC-5", "currency": "USD", "dashboard_layout": "grid"}');

输出:

TEXT 📖 仅展示
INSERT 0 1

4. Concept:JSON 操作符

(1) 基础提取操作符

操作符 右操作数 返回类型 说明 示例
-> int JSON/JSONB 按索引取数组元素 '[1,2,3]'::jsonb -> 12
-> text JSON/JSONB 按键取对象值 '{"a":1}'::jsonb -> 'a'1
->> int text 按索引取数组元素(文本) '[1,2,3]'::jsonb ->> 1"2"
->> text text 按键取对象值(文本) '{"a":1}'::jsonb ->> 'a'"1"

▶ 示例:提取嵌套字段

SQL
SELECT profile -> 'theme' AS theme_json,
       profile ->> 'theme' AS theme_text
FROM users
WHERE username = 'alice';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) 路径提取操作符

操作符 右操作数 返回类型 说明
#> text[] JSON/JSONB 按路径取值(JSON 格式)
#>> text[] text 按路径取值(文本格式)

▶ 示例:路径提取

SQL
SELECT profile #> '{address,city}' AS city_json,
       profile #>> '{address,city}' AS city_text
FROM users
WHERE profile ? 'address';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(3) 包含与存在操作符(仅 JSONB)

操作符 说明 示例
@> 左侧是否包含右侧 '{"a":1,"b":2}'::jsonb @> '{"a":1}' → true
<@ 左侧是否被右侧包含 '{"a":1}'::jsonb <@ '{"a":1,"b":2}' → true
? 键是否存在 '{"a":1}'::jsonb ? 'a' → true
`? ` 任一键是否存在
?& 所有键是否都存在 '{"a":1}'::jsonb ?& array['a','b'] → false

▶ 示例:包含查询——找出所有启用了暗色主题的用户

SQL
SELECT username, profile
FROM users
WHERE profile @> '{"theme": "dark"}';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:键存在查询——找出设置了 timezone 的用户

SQL
SELECT username, profile ->> 'timezone' AS tz
FROM users
WHERE profile ? 'timezone';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:多键查询——找出设置了 timezone 或 currency 的用户

SQL
SELECT username
FROM users
WHERE profile ?| array['timezone', 'currency'];

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

5. Concept:JSONB 函数

(1) 查询与提取函数

函数 返回类型 说明
jsonb_path_query(data, path) setof jsonb 按 JSONPATH 查询,返回所有匹配
jsonb_array_elements(data) setof jsonb 展开数组为行集合
jsonb_each(data) setof (key, value) 展开对象为键值对集合
jsonb_object_keys(data) setof text 返回所有顶层键
jsonb_typeof(data) text 返回 JSON 值的类型

▶ 示例:展开 JSON 数组为行

SQL
CREATE TABLE products (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  attributes JSONB NOT NULL DEFAULT '{}'
);

INSERT INTO products (name, attributes) VALUES
  ('T-Shirt', '{"colors": ["red", "blue", "green"], "sizes": ["S", "M", "L"]}'),
  ('Laptop', '{"colors": ["silver", "black"], "warranty_years": 2}');

SELECT product_id, name,
       jsonb_array_elements_text(attributes -> 'colors') AS color
FROM products;

输出:

TEXT 📖 仅展示
INSERT 0 1

▶ 示例:展开对象为键值对

SQL
SELECT username,
       (jsonb_each(profile)).key AS config_key,
       (jsonb_each(profile)).value AS config_value
FROM users;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) 修改函数

函数 说明
jsonb_set(target, path, new_value) 设置指定路径的值
jsonb_insert(target, path, new_value [, before]) 在指定路径插入新值
target - key 删除顶层键
target - path_array 删除指定路径
jsonb_pretty(data) 格式化输出

▶ 示例:修改用户配置

SQL
-- Add or update a field
UPDATE users
SET profile = jsonb_set(profile, '{language}', '"zh"')
WHERE username = 'alice';

-- Add a nested field
UPDATE users
SET profile = jsonb_set(profile, '{address,city}', '"New York"')
WHERE username = 'alice';

-- Delete a field
UPDATE users
SET profile = profile - 'notifications'
WHERE username = 'alice';

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:向 JSON 数组追加元素

SQL
-- Append to end (path must point to existing array, insert after last element)
UPDATE products
SET attributes = jsonb_set(
  attributes, '{colors}',
  (attributes -> 'colors') || '"yellow"'
)
WHERE name = 'T-Shirt';

输出:

TEXT 📖 仅展示
INSERT 0 1

▶ 示例:格式化输出 JSON

SQL
SELECT jsonb_pretty(profile) FROM users WHERE username = 'alice';
TEXT 📖 仅展示
{
    "theme": "dark",
    "language": "zh",
    "address": {
        "city": "New York"
    }
}

6. Concept:JSONB 索引

(1) GIN 索引加速 JSONB 查询

GIN 索引类型 支持操作符 说明
jsonb_ops(默认) @> ? `? ?&`
jsonb_path_ops @> 更小更快的索引,仅支持包含查询

▶ 示例:创建 GIN 索引

SQL
-- Default GIN index (supports @>, ?, ?|, ?&)
CREATE INDEX idx_users_profile ON users USING gin (profile);

-- Path ops GIN index (smaller, faster for @> only)
CREATE INDEX idx_users_profile_path ON users USING gin (profile jsonb_path_ops);

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:对比有无索引的查询性能

SQL
-- Without index: sequential scan
EXPLAIN ANALYZE
SELECT * FROM users WHERE profile @> '{"theme": "dark"}';

-- After creating GIN index: bitmap index scan
CREATE INDEX idx_users_profile ON users USING gin (profile);
EXPLAIN ANALYZE
SELECT * FROM users WHERE profile @> '{"theme": "dark"}';

输出:

TEXT 📖 仅展示
CREATE TABLE
查询方式 能否用 GIN 索引 说明
profile @> '{"theme":"dark"}' 包含查询,GIN 最佳场景
profile ->> 'theme' = 'dark' 不能 提取后比较,需 B-tree 表达式索引
profile ? 'theme' 键存在查询

▶ 示例:B-tree 表达式索引加速提取查询

SQL
-- For queries using ->> operator
CREATE INDEX idx_users_theme ON users ((profile ->> 'theme'));
SELECT * FROM users WHERE profile ->> 'theme' = 'dark'; -- Uses index

输出:

TEXT 📖 仅展示
CREATE TABLE

7. Concept:JSONPATH(SQL/JSON 标准)

(1) JSONPATH 语法

PostgreSQL 12+ 支持 SQL/JSON 标准的 JSONPATH,类似 XPath,用于复杂 JSON 查询。

语法 说明 示例
$.key 根对象键 $.theme
$.array[*] 遍历数组 $.colors[*]
$.nested.key 嵌套访问 $.address.city
? (condition) 过滤器 $.items[*] ? (@.price > 100)
@ 当前元素 @.name

▶ 示例:使用 jsonb_path_query 查询

SQL
SELECT jsonb_path_query(profile, '$.theme') AS theme
FROM users
WHERE username = 'alice';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:带过滤条件的 JSONPATH 查询

SQL
-- Products with warranty > 1 year
SELECT name,
       jsonb_path_query(attributes, '$.warranty_years') AS warranty
FROM products
WHERE jsonb_path_exists(attributes, '$.warranty_years ? (@ > 1)');

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:遍历数组元素

SQL
SELECT name,
       jsonb_path_query(attributes, '$.colors[*]') AS color
FROM products;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
函数 返回类型 说明
jsonb_path_query(data, path) setof jsonb 返回所有匹配
jsonb_path_query_array(data, path) jsonb 返回匹配为 JSON 数组
jsonb_path_query_first(data, path) jsonb 返回第一个匹配
jsonb_path_exists(data, path) boolean 是否有匹配

8. Concept:JSONB 与关系数据混合设计

(1) 何时用 JSONB,何时用关系列

场景 推荐方案 原因
需要频繁查询/排序/连接的字段 关系列 + B-tree 索引 性能最优
字段结构固定且参与业务逻辑 关系列 类型安全,约束完整
字段结构因客户而异 JSONB + GIN 索引 灵活,无需 ALTER TABLE
偶尔查询的附加信息 JSONB 不污染主表结构
需要精确类型约束的动态字段 JSONB + CHECK 约束 兼顾灵活与安全

▶ 示例:混合设计——产品表

SQL
CREATE TABLE products_v2 (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,              -- Fixed column, indexed
  price NUMERIC(10,2) NOT NULL,        -- Fixed column, indexed
  stock INT NOT NULL DEFAULT 0,        -- Fixed column, indexed
  attributes JSONB NOT NULL DEFAULT '{}', -- Dynamic attributes
  metadata JSONB DEFAULT '{}'          -- Rarely queried meta info
);

CREATE INDEX idx_products_category ON products_v2 (category);
CREATE INDEX idx_products_attrs ON products_v2 USING gin (attributes jsonb_path_ops);

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:JSONB CHECK 约束保证数据质量

SQL
ALTER TABLE products_v2
ADD CONSTRAINT chk_attributes_schema
CHECK (
  jsonb_typeof(attributes -> 'colors') = 'array'
  AND attributes ? 'colors'
);

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:JSONB 关联查询

SQL
-- Find orders where product has specific attribute
SELECT o.order_id, o.customer_id, p.name
FROM orders o
JOIN products_v2 p ON o.product_id = p.product_id
WHERE p.attributes @> '{"warranty_years": 2}';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. JSON 存储与查询处理流程

100%
flowchart TD
    A[JSON Text Input] --> B{Target Type?}
    B -->|json| C[Store as-is<br/>No parsing overhead]
    B -->|jsonb| D[Parse & Convert<br/>to binary tree]
    D --> E[Store as JSONB<br/>Keys sorted, no whitespace]
    E --> F{Query Type?}
    F -->|@> contains| G[GIN Index Scan<br/>Fast path]
    F -->|->> extract + compare| H[B-tree Expr Index<br/>or Seq Scan]
    F -->|jsonpath| I[JSONPATH Engine<br/>PG 12+]
    G --> J[Return Results]
    H --> J
    I --> J
    C --> K[Parse on every query<br/>Slow, no index]
    K --> J

10. 实战:SaaS 平台用户配置与产品属性系统

Bob 需要实现一个完整的 SaaS 平台数据存储方案,支持灵活的用户配置和产品属性管理。

SQL
-- Step 1: Create core tables with JSONB
CREATE TABLE saas_users (
  user_id SERIAL PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free',
  config JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMP DEFAULT now()
);

CREATE TABLE saas_products (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  price NUMERIC(10,2) NOT NULL,
  specs JSONB NOT NULL DEFAULT '{}',
  tags JSONB NOT NULL DEFAULT '[]'
);

-- Step 2: Insert sample data
INSERT INTO saas_users (email, name, plan, config) VALUES
  ('alice@corp.com', 'Alice', 'pro',
   '{"theme":"dark","language":"en","notifications":{"email":true,"sms":false},"sidebar":["dashboard","reports"]}'),
  ('bob@corp.com', 'Bob', 'enterprise',
   '{"theme":"light","language":"zh","notifications":{"email":true,"sms":true},"sidebar":["dashboard","admin","billing"]}');

INSERT INTO saas_products (name, category, price, specs, tags) VALUES
  ('Pro Widget', 'widget', 49.99,
   '{"weight_kg":0.5,"colors":["red","blue"],"warranty_years":3}',
   '["popular","new"]'),
  ('Mega Gadget', 'gadget', 199.99,
   '{"weight_kg":2.0,"colors":["silver","black"],"voltage":"220V"}',
   '["premium","bestseller"]');

-- Step 3: Create indexes
CREATE INDEX idx_saas_users_config ON saas_users USING gin (config);
CREATE INDEX idx_saas_products_specs ON saas_products USING gin (specs jsonb_path_ops);
CREATE INDEX idx_saas_products_tags ON saas_products USING gin (tags);
CREATE INDEX idx_saas_products_category ON saas_products (category);

-- Step 4: Query examples
-- Find users with email notifications enabled
SELECT name, config ->> 'theme' AS theme
FROM saas_users
WHERE config @> '{"notifications":{"email":true}}';

-- Find products available in red
SELECT name, price
FROM saas_products
WHERE specs -> 'colors' @> '["red"]';

-- Find products with specific tags
SELECT name
FROM saas_products
WHERE tags @> '["premium"]';

-- Update user config (add new field)
UPDATE saas_users
SET config = jsonb_set(config, '{timezone}', '"America/New_York"')
WHERE email = 'alice@corp.com';

-- Remove a config field
UPDATE saas_users
SET config = config - 'language'
WHERE email = 'bob@corp.com';

-- Expand product tags for analytics
SELECT name, jsonb_array_elements_text(tags) AS tag
FROM saas_products;

-- Pretty print user config
SELECT name, jsonb_pretty(config) FROM saas_users WHERE plan = 'pro';

❓ 常见问题

Q JSON 和 JSONB 该选哪个?
A 绝大多数场景选 JSONB。JSONB 查询更快、支持索引、操作符更丰富。JSON 仅在需要保留原始文本格式(空格、键序、重复键)或仅做存储不查询时使用。
Q JSONB 能替代关系表吗?
A 不能完全替代。高频查询/排序/连接的字段应使用关系列。JSONB 适合结构可变的附加数据,混合设计是最佳实践。
Q jsonb_set 和 jsonb_insert 有什么区别?
A jsonb_set 替换已存在路径的值,如果路径不存在则创建;jsonb_insert 在数组中指定位置插入新元素(before 参数控制前/后),如果键存在则不替换。
Q GIN 索引和 B-tree 表达式索引该怎么选?
A 包含查询(@>)用 GIN,等值查询(->> 'key' = 'value')用 B-tree 表达式索引。两者可以共存,覆盖不同查询模式。
Q JSONPATH 和传统操作符该怎么选?
A 简单查询用操作符更简洁,复杂嵌套查询和过滤条件用 JSONPATH 更强大。JSONPATH 是 SQL/JSON 标准,可移植性更好。
Q JSONB 字段能加 CHECK 约束吗?
A 可以。使用 jsonb_typeof()、? 操作符等在 CHECK 约束中验证 JSONB 结构,例如 CHECK (jsonb_typeof(attributes -> 'colors') = 'array')。
Q 大量更新 JSONB 字段会有性能问题吗?
A 会有。JSONB 更新是整个值替换(MVCC 创建新版本),频繁更新大 JSONB 值会产生大量 dead tuples。建议将频繁更新的字段拆为关系列,JSONB 存低频更新的附加数据。

📖 小节


📝 作业

  1. ⭐ 创建一张 app_settings 表,包含 app_name(TEXT)和 settings(JSONB)列,插入两条数据,使用 ->> 查询某个配置项的值。

  2. ⭐⭐ 为 saas_products 表创建 GIN 索引,编写查询:找出所有 specswarranty_years > 2 的产品名称,并使用 jsonb_pretty 格式化输出 specs

  3. ⭐⭐⭐ 设计一个订单表的 JSONB 混合方案:固定列存 order_id/customer_id/total_amount/status/created_at,JSONB 列 extra 存储优惠券信息(coupon_code/discount_percent)和配送备注(delivery_notes)。编写:插入含 extra 的订单、用 @> 查找使用了特定优惠券的订单、用 jsonb_set 给已有订单追加 gift_wrap: true

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏