必须使用Django的JSONField而非TextField存储JSON,因其映射PostgreSQL的jsonb类型并支持GIN索引和原生JSON操作符;需配置GinIndex,注意__key与__contains查询的索引差异,避免raw查询字段名不匹配问题。

PostgreSQL JSONField 必须用 JSONField,不能用 TextField 存 JSON 字符串
直接用 TextField 存 JSON 字符串看似简单,但会彻底失去 PostgreSQL 的原生 JSON 操作能力(比如 ->、->>、@>、? 等操作符),也无法建 GIN 索引加速查询。Django 的 JSONField(来自 django.contrib.postgres.fields)底层映射为 PostgreSQL 的 jsonb 类型,这是唯一支持高效检索和索引的类型。
常见错误:在模型里写 data = models.TextField(),然后手动 json.dumps/json.loads —— 这样查一个嵌套字段就得全表扫描 + Python 解析,性能断崖式下跌。
- 必须导入:
from django.contrib.postgres.fields import JSONField - Django 3.1+ 已将
JSONField移入django.db.models,但仅限 PostgreSQL 后端;若混用 SQLite/MySQL,仍需显式从postgres.fields导入 - 字段定义示例:
config = JSONField(default=dict, blank=True)——default必须是可调用对象,不能写default={}
GIN 索引是 JSONField 高效检索的核心,不是可选项
没有索引的 JSONField 查询(如 filter(config__name="admin") 或 filter(config__contains={"role": "admin"}))默认走全表扫描。PostgreSQL 的 jsonb 类型必须配 GIN 索引才能加速键存在、键值匹配、路径查询等操作。
两种常用 GIN 索引方式:
-
GinIndex(fields=['config']):标准 GIN 索引,支持contains、has_key、has_keys、has_any_keys等查找,也支持config__name这类带路径的查询(依赖 PostgreSQL 的泛型 GIN 支持) -
GinIndex(fields=['config'], opclasses=['jsonb_path_ops']):更轻量、更快的索引,但只支持contains和contained_by,不支持路径查询(如config__user__id)
建索引示例(写在模型 Meta 中):
class UserSetting(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
config = JSONField(default=dict)
<pre class="brush:php;toolbar:false;">class Meta:
indexes = [
models.GinIndex(fields=['config']),
]
注意:GinIndex 只在 PostgreSQL 上生效,Django 迁移时会自动跳过其他数据库。
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
config__key 和 config__contains 的行为差异直接影响索引是否命中
看似相似的查询,底层 SQL 和索引使用完全不同:
-
UserSetting.objects.filter(config__theme="dark")→ 转为config ->> 'theme' = 'dark',走 GIN 索引(前提是用了默认 GIN,不是jsonb_path_ops) -
UserSetting.objects.filter(config__contains={"theme": "dark"})→ 转为config @> '{"theme": "dark"}',也走 GIN 索引 -
UserSetting.objects.filter(config__theme__startswith="dar")→ 转为(config ->> 'theme') LIKE 'dar%',**不走 GIN 索引**,除非你额外对(config ->> 'theme')建函数索引
所以,如果高频按某个固定 key 查,又需要前缀匹配,不要依赖 __key__startswith,而是拆出字段冗余存储(如加个 theme_name = models.CharField()),或单独建函数索引:
# migrations/0002_add_theme_index.py
from django.db import migrations
<p>class Migration(migrations.Migration):
dependencies = [("myapp", "0001_initial")]</p><pre class="brush:php;toolbar:false;">operations = [
migrations.RunSQL(
"CREATE INDEX CONCURRENTLY idx_usersetting_theme ON myapp_usersetting USING BTREE ((config ->> 'theme'));",
reverse_sql="DROP INDEX CONCURRENTLY idx_usersetting_theme;",
)
]
raw() 执行原生 JSON 查询时,列名必须严格匹配模型字段(含 _id 后缀)
用 Model.objects.raw() 写带 jsonb 操作的复杂查询(如递归展开 JSON 数组、聚合统计)时,Django ORM 会尝试把结果集映射回模型字段。如果查询里返回了 config->>'status',但模型没对应字段,Django 会静默丢弃该列;更危险的是,若你返回了 parent 列但模型中是 parent_id 外键,就会触发 ValueError: Cannot assign "...": "Model.parent" must be a "Model" instance。
安全做法:
- 只 select 模型已定义的字段 + 显式别名(如
config->>'status' AS status),且确保别名与模型字段名一致 - 避免在 raw 查询中返回外键字段名(如
parent),改用parent_id别名 - 若需返回计算字段,用
extra(select={...})或自定义 Manager 方法封装,比 raw 更可控
真正复杂的 JSON 处理(如多层嵌套聚合、窗口函数配合 JSON 路径),建议先在 psql 里验证 SQL 正确性,再套进 raw(),并始终检查 cursor.description 输出的列名是否与模型对齐。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










