PostgreSQL: PostgreSQL JSON与JSONB数据处理
最后更新:2026-08-26
1. 你将学到
- 理解 JSON 与 JSONB 的区别及 JSONB 的优势
- 使用 JSON 操作符提取和过滤数据
- 使用 JSONB 函数查询、修改和生成 JSON 数据
- 创建 GIN 索引加速 JSONB 查询
- 使用 JSONPATH(SQL/JSON 标准)进行复杂查询
- JSONB 与关系数据的混合设计模式
2. 故事
Bob 是一家 SaaS 平台的后端工程师。平台需要存储用户配置和产品属性,但这些字段每个客户都不一样:
- 客户 A 的用户配置有
theme、language、notifications - 客户 B 的用户配置有
timezone、currency、dashboard_layout - 产品属性更是千变万化:衣服有
size/color,电子产品有warranty/voltage
如果用传统关系模型,每加一个字段就要 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 -> 1 → 2 |
-> |
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 存储与查询处理流程
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 存低频更新的附加数据。
📖 小节
- JSONB 是二进制存储的 JSON,查询更快、支持索引,推荐作为默认选择
->返回 JSON 类型,->>返回文本类型,#>/#>>按路径提取- 包含操作符
@>配合 GIN 索引是 JSONB 查询的最佳组合 jsonb_set/jsonb_insert/-实现增删改,jsonb_array_elements/jsonb_each实现展开- JSONPATH(PG 12+)提供 SQL/JSON 标准的复杂查询能力
- 混合设计:固定字段用关系列,动态字段用 JSONB,配合 CHECK 约束保证质量
📝 作业
-
⭐ 创建一张
app_settings表,包含app_name(TEXT)和settings(JSONB)列,插入两条数据,使用->>查询某个配置项的值。 -
⭐⭐ 为
saas_products表创建 GIN 索引,编写查询:找出所有specs中warranty_years > 2的产品名称,并使用jsonb_pretty格式化输出specs。 -
⭐⭐⭐ 设计一个订单表的 JSONB 混合方案:固定列存
order_id/customer_id/total_amount/status/created_at,JSONB 列extra存储优惠券信息(coupon_code/discount_percent)和配送备注(delivery_notes)。编写:插入含 extra 的订单、用@>查找使用了特定优惠券的订单、用jsonb_set给已有订单追加gift_wrap: true。