PostgreSQL中JSONB的使用与踩坑指南

 更新时间:2025年12月22日 09:29:52   作者:山沐与山  
文章介绍了PostgreSQL中JSONB的使用,包括JSONB的基础操作、索引策略、数组操作和批量更新,文章通过实例详细讲解了JSONB的使用方法,帮助读者解决实际工作中的问题,感兴趣的朋友跟随小编一起看看吧

PostgreSQL中JSONB的使用与踩坑记录

前言

之前接到一个数据迁移的需求,要批量修改表里 JSONB 数组中的某个字段。本以为很简单,结果折腾了大半天,踩了不少坑。这篇文章把 JSONB 的常用操作和我踩过的坑都整理出来,希望能帮到遇到类似问题的朋友。

一、JSONB是什么

1.1 JSON vs JSONB

PostgreSQL 提供了两种 JSON 类型:

类型存储方式查询性能写入性能支持索引
JSON原始文本慢(每次解析)不支持
JSONB二进制格式稍慢(需要转换)支持

简单理解

  • JSON 就像把 JSON 字符串原封不动存进去,每次查询都要重新解析
  • JSONB 会把 JSON 转成二进制格式存储,查询时直接读取,不需要解析

99% 的场景都应该用 JSONB,除非你只是存储不查询。

1.2 JSONB的优势

相比传统的 EAV(Entity-Attribute-Value)模式,JSONB 有明显优势:

传统 EAV 模式

-- 商品表
CREATE TABLE products (id INT, name VARCHAR(100));
-- 属性表(每个属性一行)
CREATE TABLE product_attributes (
    product_id INT,
    attr_key VARCHAR(50),
    attr_value VARCHAR(200)
);
-- 查询某商品的所有属性,需要 JOIN
SELECT p.name, pa.attr_key, pa.attr_value
FROM products p
JOIN product_attributes pa ON p.id = pa.product_id
WHERE p.id = 1;

JSONB 模式

-- 一张表搞定
CREATE TABLE products (
    id INT,
    name VARCHAR(100),
    attributes JSONB  -- 所有扩展属性都在这里
);
-- 直接查询,不需要 JOIN
SELECT name, attributes FROM products WHERE id = 1;
-- 还能直接查询 JSON 内部的字段
SELECT name FROM products WHERE attributes->>'color' = 'red';

JSONB 的核心优势

  • 灵活:不同商品可以有不同的属性,不需要改表结构
  • 高效:支持 GIN 索引,查询性能有保障
  • 简洁:减少表的数量,降低 JOIN 复杂度

二、JSONB基础操作

2.1 创建和插入

-- 创建包含 JSONB 列的表
CREATE TABLE user_profiles (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    profile JSONB DEFAULT '{}'::jsonb,
    created_at TIMESTAMP DEFAULT NOW()
);
-- 插入数据
INSERT INTO user_profiles (user_id, profile) VALUES
(1, '{"name": "张三", "age": 28, "tags": ["开发", "后端"], "settings": {"theme": "dark", "notify": true}}'),
(2, '{"name": "李四", "age": 32, "tags": ["产品", "管理"], "settings": {"theme": "light", "notify": false}}'),
(3, '{"name": "王五", "age": 25, "tags": ["前端", "全栈"], "settings": {"theme": "dark", "notify": true}}');

2.2 查询操作符

JSONB 提供了丰富的操作符:

操作符说明返回类型示例
->获取 JSON 对象字段JSONBprofile->'name'"张三"
->>获取 JSON 对象字段TEXTprofile->>'name'张三
->获取数组元素(按索引)JSONBprofile->'tags'->0"开发"
->>获取数组元素TEXTprofile->'tags'->>0开发
#>按路径获取JSONBprofile#>'{settings,theme}'"dark"
#>>按路径获取TEXTprofile#>>'{settings,theme}'dark

实际使用示例

-- 获取用户名(返回 JSONB 类型,带引号)
SELECT profile->'name' FROM user_profiles WHERE user_id = 1;
-- 结果:"张三"
-- 获取用户名(返回 TEXT 类型,不带引号)
SELECT profile->>'name' FROM user_profiles WHERE user_id = 1;
-- 结果:张三
-- 获取嵌套字段
SELECT profile->'settings'->>'theme' FROM user_profiles WHERE user_id = 1;
-- 结果:dark
-- 使用路径操作符(更简洁)
SELECT profile#>>'{settings,theme}' FROM user_profiles WHERE user_id = 1;
-- 结果:dark
-- 获取数组第一个元素
SELECT profile->'tags'->>0 FROM user_profiles WHERE user_id = 1;
-- 结果:开发

2.3 条件查询

-- 查询年龄大于 30 的用户
SELECT * FROM user_profiles 
WHERE (profile->>'age')::int > 30;
-- 查询使用深色主题的用户
SELECT * FROM user_profiles 
WHERE profile#>>'{settings,theme}' = 'dark';
-- 查询标签包含"后端"的用户
SELECT * FROM user_profiles 
WHERE profile->'tags' ? '后端';
-- 查询同时包含多个标签的用户
SELECT * FROM user_profiles 
WHERE profile->'tags' ?& array['开发', '后端'];
-- 查询包含任意一个标签的用户
SELECT * FROM user_profiles 
WHERE profile->'tags' ?| array['前端', '后端'];

2.4 包含查询

@> 操作符用于判断左边的 JSONB 是否包含右边的 JSONB:

-- 查询 settings 中 theme 为 dark 的用户
SELECT * FROM user_profiles 
WHERE profile @> '{"settings": {"theme": "dark"}}';
-- 查询标签包含"开发"的用户
SELECT * FROM user_profiles 
WHERE profile @> '{"tags": ["开发"]}';

注意@> 可以利用 GIN 索引,性能很好。

三、JSONB索引详解

3.1 GIN索引基础

GIN(Generalized Inverted Index)是 JSONB 最常用的索引类型:

-- 创建默认的 GIN 索引
CREATE INDEX idx_profile_gin ON user_profiles USING gin(profile);

这个索引支持以下操作符:

  • @> 包含
  • ? 键存在
  • ?& 所有键存在
  • ?| 任意键存在

3.2 jsonb_path_ops

如果你只需要 @> 操作符,可以使用更高效的 jsonb_path_ops

-- 创建 jsonb_path_ops 索引
CREATE INDEX idx_profile_path ON user_profiles USING gin(profile jsonb_path_ops);

对比

索引类型索引大小支持的操作符
默认 GIN较大@>, ?, ?&, `?
jsonb_path_ops较小(约 1/3)@>

建议:如果只用 @> 查询,优先选择 jsonb_path_ops

3.3 表达式索引

如果经常查询某个特定字段,可以创建表达式索引:

-- 为 profile->>'name' 创建 B-Tree 索引
CREATE INDEX idx_profile_name ON user_profiles ((profile->>'name'));
-- 为 age 创建索引(转换为整数)
CREATE INDEX idx_profile_age ON user_profiles (((profile->>'age')::int));
-- 查询时可以利用索引
SELECT * FROM user_profiles WHERE profile->>'name' = '张三';
SELECT * FROM user_profiles WHERE (profile->>'age')::int > 30;

3.4 索引选择策略

查询模式推荐索引
profile @> '{"key": "value"}'GIN (jsonb_path_ops)
profile ? 'key'GIN (默认)
profile->>'key' = 'value'B-Tree 表达式索引
(profile->>'num')::int > 100B-Tree 表达式索引

四、JSONB数组操作

JSONB 数组操作是实际开发中的高频需求,也是很多人踩坑的地方。

4.1 数组展开

jsonb_array_elements 函数可以将数组展开成多行:

-- 原始数据
SELECT profile->'tags' FROM user_profiles WHERE user_id = 1;
-- 结果:["开发", "后端"]
-- 展开数组
SELECT jsonb_array_elements(profile->'tags') AS tag
FROM user_profiles WHERE user_id = 1;
-- 结果:
-- "开发"
-- "后端"
-- 展开为文本(去掉引号)
SELECT jsonb_array_elements_text(profile->'tags') AS tag
FROM user_profiles WHERE user_id = 1;
-- 结果:
-- 开发
-- 后端

4.2 保留数组顺序

展开数组时,如果需要保留原始顺序,使用 WITH ORDINALITY

SELECT elem, idx
FROM user_profiles,
     jsonb_array_elements(profile->'tags') WITH ORDINALITY AS t(elem, idx)
WHERE user_id = 1;
-- 结果:
-- elem     | idx
-- "开发"   | 1
-- "后端"   | 2

这个技巧非常重要,后面批量更新时会用到。

4.3 数组聚合

jsonb_agg 函数可以将多行聚合成数组:

-- 将所有用户的名字聚合成数组
SELECT jsonb_agg(profile->>'name') FROM user_profiles;
-- 结果:["张三", "李四", "王五"]
-- 按顺序聚合
SELECT jsonb_agg(elem ORDER BY idx)
FROM user_profiles,
     jsonb_array_elements(profile->'tags') WITH ORDINALITY AS t(elem, idx)
WHERE user_id = 1;

4.4 数组修改

-- 追加元素到数组末尾
UPDATE user_profiles
SET profile = jsonb_set(profile, '
  • PostgreSQL
  • JSONB
  • ', profile->'tags' || '"运维"'::jsonb) WHERE user_id = 1; -- 删除数组中的某个元素 UPDATE user_profiles SET profile = jsonb_set( profile, '
  • PostgreSQL
  • JSONB
  • ', (SELECT jsonb_agg(elem) FROM jsonb_array_elements(profile->'tags') AS elem WHERE elem <> '"后端"') ) WHERE user_id = 1;

    五、JSONB批量更新实战

    这是本文的重点,也是实际工作中最容易出问题的地方。

    5.1 场景描述

    假设我们有一个促销配置表,每个商品可以配置多条促销规则:

    CREATE TABLE product_promo_config (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        product_id UUID NOT NULL,
        shop_id UUID NOT NULL,
        promo_rules JSONB NOT NULL,  -- 促销规则数组
        created_at TIMESTAMP DEFAULT NOW()
    );
    -- 插入测试数据
    INSERT INTO product_promo_config (product_id, shop_id, promo_rules) VALUES
    ('a1111111-1111-1111-1111-111111111111', 's1111111-1111-1111-1111-111111111111',
     '[{"code": "discount-fixed", "enabled": true, "params": {"rate": 0.8}},
       {"code": "coupon-amount", "enabled": true, "params": {"max": 50}},
       {"code": "gift-random", "enabled": false, "params": {}}]'),
    ('a2222222-2222-2222-2222-222222222222', 's1111111-1111-1111-1111-111111111111',
     '[{"code": "discount-percent", "enabled": true, "params": {"percent": 10}}]');

    现在需求来了:批量修改所有规则的 code 字段,按照映射关系:

    旧 code新 code
    discount-fixedprice-discount-fixed
    discount-percentprice-discount-percent
    coupon-amountorder-coupon-amount
    gift-randomactivity-gift-random

    5.2 错误示范

    很多人第一反应是用 jsonb_set 直接改:

    -- 错误!只能改数组第一个元素
    UPDATE product_promo_config
    SET promo_rules = jsonb_set(promo_rules, '{0,code}', '"price-discount-fixed"')
    WHERE promo_rules->0->>'code' = 'discount-fixed';
    

    这样只能改数组的第一个元素,如果一个商品配了多条规则,后面的规则就改不到。

    5.3 正确方案

    正确的做法是:展开 → 替换 → 聚合

    第一步:创建映射表

    CREATE TEMPORARY TABLE code_mapping (
        old_code VARCHAR(100) PRIMARY KEY,
        new_code VARCHAR(100) NOT NULL
    );
    INSERT INTO code_mapping (old_code, new_code) VALUES
    ('discount-fixed', 'price-discount-fixed'),
    ('discount-percent', 'price-discount-percent'),
    ('coupon-amount', 'order-coupon-amount'),
    ('coupon-percent', 'order-coupon-percent'),
    ('gift-random', 'activity-gift-random'),
    ('gift-specific', 'activity-gift-specific');

    第二步:预览更新结果

    在执行 UPDATE 之前,先用 SELECT 预览:

    SELECT 
        ppc.id,
        ppc.promo_rules AS old_rules,
        (
            SELECT jsonb_agg(
                CASE 
                    WHEN cm.new_code IS NOT NULL 
                    THEN jsonb_set(elem, '[code]', to_jsonb(cm.new_code))
                    ELSE elem
                END
                ORDER BY idx  -- 保持原始顺序
            )
            FROM jsonb_array_elements(ppc.promo_rules) WITH ORDINALITY AS t(elem, idx)
            LEFT JOIN code_mapping cm ON elem->>'code' = cm.old_code
        ) AS new_rules
    FROM product_promo_config ppc;

    解释这段 SQL

    • jsonb_array_elements(ppc.promo_rules) WITH ORDINALITY AS t(elem, idx)
    • 将 promo_rules 数组展开成多行
      • elem 是每个元素,idx 是原始位置(从 1 开始)
      • LEFT JOIN code_mapping cm ON elem->>'code' = cm.old_code
    • 和映射表关联,找到对应的新 code
      • CASE WHEN ... THEN jsonb_set(...) ELSE elem END
    • 如果找到映射就替换,找不到就保持原样
    • jsonb_agg(... ORDER BY idx)
      • 聚合回数组,按原始顺序排列

    第三步:执行更新

    确认预览结果正确后,执行更新:

    BEGIN;
    UPDATE product_promo_config ppc
    SET promo_rules = (
        SELECT COALESCE(
            jsonb_agg(
                CASE 
                    WHEN cm.new_code IS NOT NULL 
                    THEN jsonb_set(elem, '[code]', to_jsonb(cm.new_code))
                    ELSE elem
                END
                ORDER BY idx
            ),
            '[]'::jsonb  -- 处理空数组的情况
        )
        FROM jsonb_array_elements(ppc.promo_rules) WITH ORDINALITY AS t(elem, idx)
        LEFT JOIN code_mapping cm ON elem->>'code' = cm.old_code
    )
    WHERE EXISTS (
        SELECT 1 
        FROM jsonb_array_elements(ppc.promo_rules) AS e
        JOIN code_mapping cm ON e->>'code' = cm.old_code
    );
    -- 验证结果
    SELECT id, promo_rules FROM product_promo_config;
    -- 确认无误后提交
    COMMIT;

    5.4 通用模板

    这是 JSONB 数组批量更新的通用模板,可以直接套用:

    UPDATE your_table t
    SET json_column = (
        SELECT COALESCE(
            jsonb_agg(
                CASE 
                    WHEN mapping.new_val IS NOT NULL 
                    THEN jsonb_set(elem, '{field_name}', to_jsonb(mapping.new_val))
                    ELSE elem
                END
                ORDER BY idx  -- 保持原顺序
            ),
            '[]'::jsonb  -- 处理空数组
        )
        FROM jsonb_array_elements(t.json_column) WITH ORDINALITY AS x(elem, idx)
        LEFT JOIN mapping_table mapping ON elem->>'field_name' = mapping.old_val
    )
    WHERE EXISTS (
        SELECT 1 
        FROM jsonb_array_elements(t.json_column) AS e
        JOIN mapping_table mapping ON e->>'field_name' = mapping.old_val
    );

    六、JSONB性能优化

    6.1 避免全表扫描

    -- 慢:没有索引支持
    SELECT * FROM user_profiles WHERE profile->>'name' = '张三';
    -- 快:创建表达式索引
    CREATE INDEX idx_profile_name ON user_profiles ((profile->>'name'));
    SELECT * FROM user_profiles WHERE profile->>'name' = '张三';
    -- 快:使用 @> 操作符 + GIN 索引
    CREATE INDEX idx_profile_gin ON user_profiles USING gin(profile jsonb_path_ops);
    SELECT * FROM user_profiles WHERE profile @> '{"name": "张三"}';

    6.2 减少 JSONB 大小

    JSONB 越大,查询和更新越慢。建议:

    • 只存必要的字段
    • 避免深层嵌套(建议不超过 3 层)
    • 大文本考虑单独存储
    -- 不推荐:把所有东西都塞进 JSONB
    profile = '{"name": "...", "avatar_base64": "超长字符串...", "history": [...]}'
    -- 推荐:大字段单独存储
    profile = '{"name": "...", "avatar_id": "xxx"}'
    -- avatar 内容存在单独的表或对象存储

    6.3 批量操作优化

    -- 慢:逐行更新
    UPDATE user_profiles SET profile = jsonb_set(profile, '{age}', '29') WHERE user_id = 1;
    UPDATE user_profiles SET profile = jsonb_set(profile, '{age}', '33') WHERE user_id = 2;
    UPDATE user_profiles SET profile = jsonb_set(profile, '{age}', '26') WHERE user_id = 3;
    -- 快:批量更新
    UPDATE user_profiles AS up
    SET profile = jsonb_set(up.profile, '{age}', to_jsonb(v.new_age))
    FROM (VALUES (1, 29), (2, 33), (3, 26)) AS v(user_id, new_age)
    WHERE up.user_id = v.user_id;

    6.4 使用 EXPLAIN 分析

    EXPLAIN ANALYZE 
    SELECT * FROM user_profiles WHERE profile @> '{"settings": {"theme": "dark"}}';
    -- 查看是否使用了索引
    -- Index Scan using idx_profile_gin on user_profiles  (cost=...)

    七、常见问题与最佳实践

    7.1 常见问题

    问题原因解决方案
    查询慢没有合适的索引根据查询模式创建 GIN 或表达式索引
    更新数组只改了第一个用了 jsonb_set(col, '{0,field}', ...)使用展开-替换-聚合模式
    数组顺序乱了jsonb_agg 不保证顺序WITH ORDINALITY + ORDER BY
    空数组报错jsonb_agg 对空集返回 NULLCOALESCE(..., '[]'::jsonb)
    类型转换错误-> 返回 JSONB,->> 返回 TEXT注意操作符的返回类型

    7.2 最佳实践

    设计阶段

    1. 明确哪些字段放 JSONB,哪些放普通列
    2. 高频查询的字段考虑提取为普通列
    3. 设计合理的 JSON 结构,避免过深嵌套

    开发阶段

    1. 优先使用 @> 操作符(可以利用 GIN 索引)
    2. 数组操作记得保持顺序
    3. UPDATE 前先 SELECT 预览

    运维阶段

    1. 监控 JSONB 列的大小
    2. 定期 VACUUM 清理死元组
    3. 关注慢查询日志

    7.3 JSONB 操作速查表

    -- 取值
    col->'key'                    -- 返回 JSONB
    col->>'key'                   -- 返回 TEXT
    col->0                        -- 数组第一个元素(JSONB)
    col->>0                       -- 数组第一个元素(TEXT)
    col#>'{a,b,c}'               -- 路径取值(JSONB)
    col#>>'{a,b,c}'              -- 路径取值(TEXT)
    -- 修改
    jsonb_set(col, '{key}', '"value"'::jsonb)           -- 设置字段
    jsonb_set(col, '{key}', to_jsonb(variable))         -- 设置字段(变量)
    col || '{"new_key": "value"}'::jsonb                -- 合并
    col - 'key'                                          -- 删除键
    col - 0                                              -- 删除数组第一个元素
    -- 数组操作
    jsonb_array_elements(col)                           -- 展开数组
    jsonb_array_elements(col) WITH ORDINALITY           -- 展开并保留顺序
    jsonb_agg(elem)                                     -- 聚合成数组
    jsonb_agg(elem ORDER BY idx)                        -- 按顺序聚合
    jsonb_array_length(col)                             -- 数组长度
    -- 判断
    col ? 'key'                   -- 键是否存在
    col ?& array['a','b']        -- 所有键是否存在
    col ?| array['a','b']        -- 任意键是否存在
    col @> '{"key": "val"}'      -- 是否包含
    -- 类型
    jsonb_typeof(col)            -- 返回类型(object/array/string/number/boolean/null)

    八、总结

    本文从基础到进阶,系统讲解了 PostgreSQL JSONB 的使用:

    1. 基础操作->->> 的区别,条件查询的写法
    2. 索引策略:GIN 索引、jsonb_path_ops、表达式索引的选择
    3. 数组操作:展开、聚合、保持顺序的技巧
    4. 批量更新:展开-替换-聚合的通用模式
    5. 性能优化:索引选择、批量操作、EXPLAIN 分析

    JSONB 是 PostgreSQL 的杀手级特性之一,掌握它可以让你在很多场景下避免引入额外的中间件。希望这篇文章能帮你在实际工作中少踩坑。

    到此这篇关于PostgreSQL中JSONB的使用与踩坑记录的文章就介绍到这了,更多相关PostgreSQL JSONB使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    相关文章

    • docker安装Postgresql数据库及基本操作

      docker安装Postgresql数据库及基本操作

      PostgreSQL是一个强大的开源对象-关系型数据库管理系统,以其高可扩展性和标准化而著称,这篇文章主要介绍了docker安装Postgresql数据库及基本操作的相关资料,需要的朋友可以参考下
      2025-03-03
    • PostgreSQL事务回卷实战案例详析

      PostgreSQL事务回卷实战案例详析

      前段时间在公司小范围做了一个关于PG事务实现的讲座,最后总结了一个摘要性的东西,分享一下,这篇文章主要给大家介绍了关于PostgreSQL事务回卷实战案例的相关资料,需要的朋友可以参考下
      2022-03-03
    • 在postgreSQL中运行sql脚本和pg_restore命令方式

      在postgreSQL中运行sql脚本和pg_restore命令方式

      这篇文章主要介绍了在postgreSQL中运行sql脚本和pg_restore命令方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
      2021-01-01
    • PostgreSQL中扩展moddatetime的使用

      PostgreSQL中扩展moddatetime的使用

      PostgreSQL的moddatetime扩展通过触发器自动维护时间戳字段,轻量高效,适用于审计日志和多租户系统,具有一定的参考价值,感兴趣的可以了解一下
      2025-06-06
    • Postgresql数据库密码忘记的详细解决方法

      Postgresql数据库密码忘记的详细解决方法

      在使用PostgreSQL数据库时,忘记数据库密码可能会影响到正常的开发和维护工作,这篇文章主要介绍了Postgresql数据库密码忘记的详细解决方法,文中通过代码介绍的非常详细,需要的朋友可以参考下
      2025-06-06
    • postgresql 赋权语句 grant的正确使用说明

      postgresql 赋权语句 grant的正确使用说明

      这篇文章主要介绍了postgresql 赋权语句 grant的正确使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
      2021-01-01
    • Postgres copy命令导入导出数据的操作方法

      Postgres copy命令导入导出数据的操作方法

      最近有需要对数据进行迁移的需求,由于postgres性能的关系,单表3000W的数据量查询起来有一些慢,需要对大表进行切割,拆成若干个子表,涉及到原有数据要迁移到子表的需求,这篇文章主要介绍了Postgres copy命令导入导出数据的操作方法,需要的朋友可以参考下
      2024-08-08
    • PostgreSQL查询和处理JSON数据

      PostgreSQL查询和处理JSON数据

      这篇文章主要给大家介绍了关于PostgreSQL查询和处理JSON数据的相关资料,需要的朋友可以参考下
      2023-11-11
    • Ubuntu PostgreSQL安装和配置的介绍

      Ubuntu PostgreSQL安装和配置的介绍

      今天小编就为大家分享一篇关于Ubuntu PostgreSQL安装和配置的介绍,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
      2019-03-03
    • PostgreSQL数据库实现公网远程连接的操作步骤

      PostgreSQL数据库实现公网远程连接的操作步骤

      PostgreSQL是一个功能非常强大的关系型数据库管理系统(RDBMS),本文呢将简单几步通过cpolar 内网穿透工具即可现实本地postgreSQL 远程访问,需要的朋友可以参考下
      2023-09-09

    最新评论