首页 / PostgreSQL 入门教程 / JSON 与 JSONB 类型

PostgreSQL 入门教程

JSON 与 JSONB 类型

本教程共 50 篇 · 第 16 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

PostgreSQLPostgreSQL 入门教程JSONJSONBGIN 索引操作符

16. JSON 与 JSONB 类型

本节目标:学完能用 jsonb 存半结构化数据,会用 -> / ->> / #> 取字段,用 @> 做包含查询,并用 GIN 索引加速。

有时候字段不固定,比如用户的标签、配置项、第三方回传的复杂报文。硬拆成多列很别扭,加一列又一列又跟不上变化。这时用 JSON 类型最合适——把整段半结构化数据塞进一个字段,又能按里面的键查询。

json 与 jsonb 的区别

PostgreSQL 提供两种:jsonjsonb

  • json:原样存文本,每次查询都重新解析,保留空格和键的顺序。
  • jsonb:以二进制格式存储,写入时解析一次,查询快,支持索引和丰富的包含运算。
SELECT '{"a":1,"b":2}'::json  = '{"b":2,"a":1}'::json;   -- false(顺序不同)
SELECT '{"a":1,"b":2}'::jsonb = '{"b":2,"a":1}'::jsonb;  -- true(jsonb 不关心顺序)

jsonb 还会去掉重复的键、忽略空白,所以它「相等判断更自然」。两者的输入写法完全一样,差别只在存储格式。

Tip

日常几乎都用 jsonb。除非你要原样保留键顺序、保留空格这种特殊需求,否则别用 jsonjsonb 更快、能建索引、操作符更多,是事实上的标准选择。

建表与插入

users 加一个 profile 字段存扩展信息:

CREATE TABLE users (
  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  email VARCHAR(120),
  age SMALLINT,
  profile JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);

INSERT INTO users (username, profile)
VALUES ('alice', '{"city":"北京", "tags":["vip","new"], "age": 20}');

注意插入的 JSON 字符串必须符合 JSON 语法:键和字符串值都要用双引号,单引号不行。下面这种写法会报错,因为 JSON 要求键和字符串都必须用双引号:

INSERT INTO users (username, profile) VALUES ('bob', '{city:"上海"}');
-- ERROR: invalid input syntax for type json
-- DETAIL: Token "city" is invalid.

查询字段:-> 与 ->>

-> 取出来还是 json 对象(带引号);->> 取出来是纯文本(不带引号)。这是最常用的一对操作符,务必分清。

SELECT username, profile -> 'city'  FROM users;  -- 结果带引号:"北京"
SELECT username, profile ->> 'city' FROM users;  -- 结果纯文本:北京

->> 拿到的是文本,所以可以做条件过滤、字符串比较:

SELECT * FROM users WHERE profile ->> 'city' = '北京';
Note

->> 取出来的值是 text 类型。要做数字比较(比如年龄大于 18),记得转换:(profile ->> 'age')::int > 18,否则会按文本比较出错(文本里 “9” 比 “10” 大)。

数组元素用数字下标(从 0 开始):

SELECT username, profile -> 'tags' -> 0 FROM users;  -- 取第一个标签
SELECT username, profile -> 'tags' ->> 0 FROM users; -- 第一个标签的文本

嵌套对象继续用 -> 往下取:

SELECT profile -> 'address' ->> 'city' FROM users;

还有 #> 用路径数组取深层字段,#>> 取文本:

SELECT profile #> '{address, city}' FROM users;     -- jsonb 对象
SELECT profile #>> '{address, city}' FROM users;    -- 纯文本

包含判断(@>)

jsonb 支持「包含」运算符 @>,判断左边是否包含右边的结构。这是 jsonb 的杀手锏。

SELECT * FROM users WHERE profile @> '{"city":"北京"}';

意思是「profile 里存在 city 等于北京这一项」。右边可以写任意子结构,不要求完全相等。查某个标签是否存在:

SELECT * FROM users WHERE profile @> '{"tags":["vip"]}';

键存在性用 ?,判断某个顶层键在不在:

SELECT * FROM users WHERE profile ? 'tags';

还有 ?|(任一键存在)、?&(全部键存在)、<@(被包含)等运算符,按需要取用。

修改 jsonb

jsonb_set 改某个键的值:

UPDATE users
SET profile = jsonb_set(profile, '{city}', '"上海"')
WHERE username = 'alice';

jsonb_set 的第三个参数必须是合法 jsonb 值,字符串要带引号。要新增不存在的键,第四个参数传 true

UPDATE users
SET profile = jsonb_set(profile, '{phone}', '"13800000000"', true)
WHERE username = 'alice';

|| 运算符合并、新增键值对(左边优先,键冲突时保留左边):

UPDATE users
SET profile = profile || '{"vip": true}'
WHERE username = 'alice';

删键用 - 操作符,删数组元素用 - 加下标:

UPDATE users SET profile = profile - 'phone' WHERE username = 'alice';
UPDATE users SET profile = profile - '{tags, 0}' WHERE username = 'alice';  -- 删第一个标签

用 GIN 索引加速

jsonb 的一大优势是能建 GIN 索引,加速包含查询。没有索引时,@> 要逐行扫描,大数据量很慢。

CREATE INDEX idx_users_profile ON users USING GIN (profile);

之后 WHERE profile @> ... 这类查询就能用上索引,大表尤其明显。如果只按某个固定键查询,还可以建表达式索引更省空间、更快:

CREATE INDEX idx_users_city ON users USING btree ((profile ->> 'city'));
Note

总结:半结构化数据优先 jsonb;取字段用 -> / ->>(前者带引号、后者纯文本),深层用 #>;包含判断用 @>;要快就建 GIN 索引,单键高频查询用 btree 表达式索引。jsonb 让你的表既能灵活扩展,又保持可查询、可索引。

半结构化数据为什么需要它

关系表的列是固定的,但现实里常有「结构不固定」的数据:用户的个性化配置、第三方回传的报文、商品的动态属性。硬拆成几十列既难维护又大量为空。JSON 类型让「一整段灵活结构」住进一个字段,同时 PostgreSQL 还能按里面的键做查询、建索引。

不过要清醒:jsonb 不是用来替代关系建模的。如果某个字段你经常要拿来做关联、做聚合统计,那它更该是独立列。jsonb 适合「整体读写、偶尔按个别键过滤」的扩展信息。想清楚这一点,才不会把表搞成一团乱麻。

常用 jsonb 函数

除了操作符,PostgreSQL 还提供一批 jsonb 函数。比如 jsonb_pretty 把内容格式化得好看,jsonb_object_keys 列出顶层所有键:

SELECT jsonb_pretty(profile) FROM users WHERE username='alice';
SELECT jsonb_object_keys(profile) FROM users WHERE username='alice';

判断某个键是否存在还有函数版 jsonb_exists

SELECT * FROM users WHERE jsonb_exists(profile, 'city');

json 与 jsonb 怎么选的再提醒

再强调一次取舍:json 保留原始文本(键顺序、空格),每次查询重新解析,适合「原样存档、很少按内部字段查」的场景;jsonb 写入即解析、查询快、能建 GIN 索引、操作符丰富,适合「要按内部字段搜索、过滤、聚合」的场景。日常 99% 情况用 jsonb

Tip

如果只偶尔存一段配置、从不按里面字段查询,json 也行;但只要有一处 WHERE profile @> ...profile ->> 'x',就果断 jsonb

索引怎么选

jsonb 上两种索引最常用:GIN 适合「包含查询」@>,能覆盖任意结构;btree 表达式索引 ((profile ->> 'city')) 适合「只按某个固定键等值或范围查」,更小更快。

CREATE INDEX idx_profile_gin ON users USING GIN (profile);
CREATE INDEX idx_city_btree ON users ((profile ->> 'city'));

两张索引不冲突,可以共存,让不同查询各取所需。

什么时候不该用 JSON

jsonb 虽好,但不是万能的。如果数据结构固定、需要频繁按字段做关联或聚合,老老实实拆成独立列性能更好、约束更强。jsonb 适合「结构灵活、查询简单」的扩展字段,别拿它替代正常的关系建模。