PostgreSQL实战:详解如何用Python优雅地从PG中存取处理JSON

文章目录

    • [一、PostgreSQL 中的 JSON 类型基础](#一、PostgreSQL 中的 JSON 类型基础)
      • [1.1 JSON vs JSONB:关键区别](#1.1 JSON vs JSONB:关键区别)
      • [1.2 常用操作符与函数](#1.2 常用操作符与函数)
    • [二、Python 驱动选择与配置](#二、Python 驱动选择与配置)
      • [2.1 主流驱动对比](#2.1 主流驱动对比)
      • [2.2 PostgreSQL 的 JSONB 与 Python 的结合注意点](#2.2 PostgreSQL 的 JSONB 与 Python 的结合注意点)
    • [三、使用 psycopg2 处理 JSON](#三、使用 psycopg2 处理 JSON)
      • [3.1 基础连接与自动转换](#3.1 基础连接与自动转换)
      • [3.2 插入 JSON 数据](#3.2 插入 JSON 数据)
      • [3.3 查询并自动反序列化](#3.3 查询并自动反序列化)
      • [3.4 复杂查询示例](#3.4 复杂查询示例)
    • [四、使用 asyncpg 处理 JSON(异步场景)](#四、使用 asyncpg 处理 JSON(异步场景))
      • [4.1 基础用法](#4.1 基础用法)
      • [4.2 复杂查询](#4.2 复杂查询)
    • [五、使用 SQLAlchemy ORM 处理 JSON](#五、使用 SQLAlchemy ORM 处理 JSON)
      • [5.1 模型定义](#5.1 模型定义)
      • [5.2 CRUD 操作](#5.2 CRUD 操作)
      • [5.3 使用专用函数(SQLAlchemy 1.4+)](#5.3 使用专用函数(SQLAlchemy 1.4+))
    • 六、高级技巧与最佳实践
      • [6.1 动态构建 JSON 查询条件](#6.1 动态构建 JSON 查询条件)
      • [6.2 更新 JSON 字段](#6.2 更新 JSON 字段)
      • [6.3 索引优化](#6.3 索引优化)
    • 七、性能考量
      • [7.1 存储效率](#7.1 存储效率)
      • [7.2 查询性能](#7.2 查询性能)
      • [7.3 批量操作](#7.3 批量操作)
    • 八、典型应用场景
      • [8.1 用户配置存储](#8.1 用户配置存储)
      • [8.2 日志与事件追踪](#8.2 日志与事件追踪)
      • [8.3 动态表单/问卷](#8.3 动态表单/问卷)
    • 九、常见陷阱与解决方案
      • [9.1 时区与日期处理](#9.1 时区与日期处理)
      • [9.2 精度丢失(浮点数)](#9.2 精度丢失(浮点数))
      • [9.3 键名大小写敏感](#9.3 键名大小写敏感)

PostgreSQL 自 9.2 版本起原生支持 JSON 数据类型,并在后续版本中不断增强其功能,现已提供 JSONJSONB 两种类型、丰富的操作符、索引支持及函数体系。与此同时,Python 作为数据处理的主流语言,与 PostgreSQL 的结合日益紧密。

本文将系统讲解如何在 Python 中高效、安全、可维护地与 PostgreSQL 的 JSON 数据交互,涵盖:

  • JSON 与 JSONB 的区别与选型
  • 使用 psycopg2asyncpg 等主流驱动
  • 自动序列化/反序列化 Python 字典与 JSON
  • 复杂查询:路径提取、条件过滤、更新操作
  • 性能优化与索引策略
  • 实战案例:配置存储、日志分析、动态表单等

一、PostgreSQL 中的 JSON 类型基础

参考

1.1 JSON vs JSONB:关键区别

特性 JSON JSONB
存储格式 文本(保留原始格式) 二进制(解析后存储)
是否去重 否(保留重复键) 是(仅保留最后一个键值)
是否保留顺序 否(对象键无序)
索引支持 不支持(需表达式索引) 支持 GIN、GiST 索引
查询性能 较慢(每次需解析) 快(已解析为内部结构)
存储空间 较大 较小(无空白、无重复)

推荐 :除非必须保留原始 JSON 格式(如审计日志),否则一律使用 JSONB

1.2 常用操作符与函数

1、路径提取

  • ->:返回 JSON 对象(仍为 JSONB)

    sql 复制代码
    SELECT data->'user'->'name' FROM logs;
  • ->>:返回文本(TEXT)

    sql 复制代码
    SELECT data->>'status' FROM orders;

2、条件查询

  • 检查键是否存在:

    sql 复制代码
    SELECT * FROM events WHERE data ? 'error_code';
  • 检查嵌套路径:

    sql 复制代码
    SELECT * FROM configs WHERE data @> '{"feature": {"enabled": true}}';

3、更新操作

  • 设置字段(PostgreSQL 9.5+):

    sql 复制代码
    UPDATE users SET profile = jsonb_set(profile, '{settings,theme}', '"dark"');

二、Python 驱动选择与配置

2.1 主流驱动对比

驱动 异步支持 JSON 自动转换 成熟度 适用场景
psycopg2 需手动注册适配器 同步应用、Django、Flask
psycopg2-binary 同上 快速原型、无需编译
asyncpg 自动转换 dict ↔ JSONB 异步框架(FastAPI, aiohttp)
SQLAlchemy + psycopg2 通过 JSON / JSONB 类型自动处理 极高 ORM 场景

2.2 PostgreSQL 的 JSONB 与 Python 的结合注意点

PostgreSQL 的 JSONB 与 Python 的结合,为处理半结构化数据提供了强大而灵活的方案。通过合理选择驱动、配置自动转换、利用索引和操作符,可实现:

  • 开发效率高:无需预定义 schema
  • 查询能力强:支持复杂嵌套查询
  • 性能可优化:GIN 索引、路径索引保障效率

但也要谨记:

  • 不要滥用 JSON:结构化数据仍应使用关系模型
  • 保持查询简单:避免过度嵌套导致维护困难
  • 监控性能:定期分析执行计划

三、使用 psycopg2 处理 JSON

3.1 基础连接与自动转换

默认情况下,psycopg2JSONB 列返回为字符串。需注册适配器实现自动转换:

python 复制代码
import json
import psycopg2
from psycopg2.extras import Json

# 注册自动转换:DB → Python
def _json_decode(data, cur):
    if data is None:
        return None
    return json.loads(data)

# 注册适配器
psycopg2.extensions.register_type(
    psycopg2.extensions.new_type(
        (3802,), "JSONB", _json_decode  # 3802 是 JSONB 的 OID
    )
)

# 连接数据库
conn = psycopg2.connect(
    host="localhost",
    database="mydb",
    user="user",
    password="pass"
)

3.2 插入 JSON 数据

python 复制代码
data = {
    "user_id": 123,
    "action": "login",
    "metadata": {
        "ip": "192.168.1.1",
        "device": "mobile"
    }
}

cur = conn.cursor()
cur.execute(
    "INSERT INTO events (event_data) VALUES (%s)",
    (Json(data),)  # 使用 Json 包装器
)
conn.commit()

注意:必须使用 psycopg2.extras.Json,否则会被当作字符串插入。

3.3 查询并自动反序列化

python 复制代码
cur.execute("SELECT id, event_data FROM events WHERE id = %s", (1,))
row = cur.fetchone()
print(type(row[1]))  # <class 'dict'>
print(row[1]['metadata']['ip'])  # 192.168.1.1

得益于前面的适配器注册,event_data 自动转为 dict

3.4 复杂查询示例

1、提取嵌套字段

python 复制代码
cur.execute("""
    SELECT 
        id,
        event_data->>'action' AS action,
        event_data->'metadata'->>'ip' AS ip
    FROM events
    WHERE (event_data->'metadata'->>'device') = %s
""", ("mobile",))

for row in cur:
    print(f"Action: {row[1]}, IP: {row[2]}")

2、条件过滤(使用 @>)

python 复制代码
# 查找包含特定子结构的记录
filter_condition = {"metadata": {"device": "mobile"}}
cur.execute(
    "SELECT * FROM events WHERE event_data @> %s",
    (Json(filter_condition),)
)

四、使用 asyncpg 处理 JSON(异步场景)

asyncpg 对 JSONB 支持更友好,默认自动转换。

4.1 基础用法

python 复制代码
import asyncio
import asyncpg

async def main():
    conn = await asyncpg.connect(
        host='localhost',
        database='mydb',
        user='user',
        password='pass'
    )

    # 插入:直接传 dict
    data = {"user_id": 123, "tags": ["a", "b"]}
    await conn.execute(
        "INSERT INTO items (payload) VALUES ($1)",
        data  # asyncpg 自动序列化为 JSONB
    )

    # 查询:自动反序列化为 dict/list
    row = await conn.fetchrow("SELECT payload FROM items LIMIT 1")
    print(type(row['payload']))  # <class 'dict'>
    print(row['payload']['tags'])  # ['a', 'b']

    await conn.close()

asyncio.run(main())

优势:无需额外配置,开箱即用。

4.2 复杂查询

python 复制代码
# 使用路径操作符
rows = await conn.fetch("""
    SELECT 
        id,
        payload->'user'->>'name' AS name
    FROM profiles
    WHERE payload @> $1
""", {"settings": {"visible": True}})

for r in rows:
    print(r['name'])

五、使用 SQLAlchemy ORM 处理 JSON

SQLAlchemy 通过 JSONJSONB 类型提供 ORM 支持。

5.1 模型定义

python 复制代码
from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class Event(Base):
    __tablename__ = 'events'
    id = Column(Integer, primary_key=True)
    data = Column(JSONB)  # 使用 JSONB

engine = create_engine('postgresql://user:pass@localhost/mydb')
Session = sessionmaker(bind=engine)

5.2 CRUD 操作

python 复制代码
session = Session()

# 插入
event = Event(data={
    "type": "click",
    "element": {"id": "btn1", "class": "primary"}
})
session.add(event)
session.commit()

# 查询(自动转为 dict)
e = session.query(Event).first()
print(e.data['element']['id'])  # btn1

# 条件查询(使用 .op() 调用操作符)
from sqlalchemy import text
results = session.query(Event).filter(
    Event.data.op('@>')({'type': 'click'})
).all()

5.3 使用专用函数(SQLAlchemy 1.4+)

python 复制代码
from sqlalchemy.dialects.postgresql import JSONB

# 提取字段
session.query(
    Event.data['user']['name'].astext.label('username')
).all()

六、高级技巧与最佳实践

6.1 动态构建 JSON 查询条件

避免拼接 SQL,使用参数化:

python 复制代码
def find_by_metadata(conn, **kwargs):
    # 构建嵌套 dict
    condition = {"metadata": kwargs}
    cur = conn.cursor()
    cur.execute(
        "SELECT * FROM events WHERE event_data @> %s",
        (Json(condition),)
    )
    return cur.fetchall()

# 调用
results = find_by_metadata(conn, device="mobile", os="iOS")

6.2 更新 JSON 字段

python 复制代码
# 使用 jsonb_set 更新嵌套字段
cur.execute("""
    UPDATE users 
    SET profile = jsonb_set(profile, %s, %s, true)
    WHERE id = %s
""", (
    ['settings', 'notifications'],  # 路径(数组)
    Json({"email": True, "push": False}),  # 新值
    user_id
))

第四个参数 true 表示:若路径不存在则创建。

6.3 索引优化

对高频查询字段建立 GIN 索引:

sql 复制代码
-- 全文索引(适用于任意键查询)
CREATE INDEX idx_event_data ON events USING GIN (event_data);

-- 特定路径索引(更高效)
CREATE INDEX idx_event_action ON events ((event_data->>'action'));

在 Python 中可通过 Alembic 或原生 SQL 创建。


七、性能考量

7.1 存储效率

  • JSONBJSON 节省 10%~30% 空间
  • 避免在 JSON 中存储大文本(如 base64 图片),应存 URL

7.2 查询性能

  • 避免在 WHERE 中对 JSON 字段使用函数(如 length(data->>'name')),会导致索引失效
  • 优先使用 @>, ?, ->> 等支持索引的操作符

7.3 批量操作

  • 使用 execute_values(psycopg2)或 copy_records_to_table(asyncpg)提升批量插入性能

  • 示例(psycopg2):

    python 复制代码
    from psycopg2.extras import execute_values
    data_list = [{"id": i, "tags": ["x"]} for i in range(1000)]
    execute_values(
        cur,
        "INSERT INTO items (payload) VALUES %s",
        [(Json(d),) for d in data_list]
    )

八、典型应用场景

8.1 用户配置存储

sql 复制代码
CREATE TABLE user_settings (
    user_id INT PRIMARY KEY,
    preferences JSONB NOT NULL DEFAULT '{}'
);
  • 优势:无需 ALTER TABLE 即可新增配置项
  • 查询:SELECT preferences->'theme' FROM user_settings

8.2 日志与事件追踪

  • 存储非结构化日志,支持按任意字段过滤
  • 结合 BRIN 索引按时间分区,GIN 索引按内容查询

8.3 动态表单/问卷

  • 表单结构存为 JSON,回答存为另一 JSON
  • 避免 EAV(Entity-Attribute-Value)反模式

九、常见陷阱与解决方案

9.1 时区与日期处理

JSON 不支持日期类型,通常存为 ISO 字符串:

python 复制代码
data = {"created_at": datetime.utcnow().isoformat()}

查询时用 to_timestamp() 转换:

sql 复制代码
SELECT to_timestamp(data->>'created_at', 'YYYY-MM-DD"T"HH24:MI:SS.US') 
FROM logs;

9.2 精度丢失(浮点数)

PostgreSQL 的 JSONB 使用 IEEE 754 双精度,与 Python 一致,一般无问题。

但若需高精度(如金融),应存为字符串或使用 NUMERIC 字段。

9.3 键名大小写敏感

JSON 对象键区分大小写:

sql 复制代码
-- 以下不等价
data->'UserId'  vs  data->'userid'

建议统一使用小写命名。


相关推荐
ZH15455891312 小时前
Flutter for OpenHarmony Python学习助手实战:面向对象编程实战的实现
python·学习·flutter
玄同7652 小时前
SQLite + LLM:大模型应用落地的轻量级数据存储方案
jvm·数据库·人工智能·python·语言模型·sqlite·知识图谱
HoneyMoose2 小时前
PostgreSQL 创建用户表的时候提示 user 错误
postgresql
吾日三省吾码2 小时前
别只会“加索引”了!这 3 个 PostgreSQL 反常识优化,能把性能和成本一起打下来
数据库·postgresql
User_芊芊君子2 小时前
CANN010:PyASC Python编程接口—简化AI算子开发的Python框架
开发语言·人工智能·python
白日做梦Q2 小时前
Anchor-free检测器全解析:CenterNet vs FCOS
python·深度学习·神经网络·目标检测·机器学习
喵手2 小时前
Python爬虫实战:公共自行车站点智能采集系统 - 从零构建生产级爬虫的完整实战(附CSV导出 + SQLite持久化存储)!
爬虫·python·爬虫实战·零基础python爬虫教学·采集公共自行车站点·公共自行车站点智能采集系统·采集公共自行车站点导出csv
喵手2 小时前
Python爬虫实战:地图 POI + 行政区反查实战 - 商圈热力数据准备完整方案(附CSV导出 + SQLite持久化存储)!
爬虫·python·爬虫实战·零基础python爬虫教学·地区poi·行政区反查·商圈热力数据采集
熊猫_豆豆3 小时前
YOLOP车道检测
人工智能·python·算法