← 中文资源

简体中文版本

关系数据库中的 JSON 列

JSON 列类型把半结构化文档存进关系模型:路径可查询、可建表达式索引,用模式灵活性换取更弱的约束与索引工程学。

阅读英文原文

概念定义

形状随生产方变化或持续演进的文档装不进固定列。JSON 类型把解析后的文档存进行里:可按路径查询、可做包含比较、可经表达式或 GIN 索引。jsonb 归一化文档,文本型 json 不可索引且无法原样往返。

工作机制

操作符负责提取与判断:-> 与 ->> 取值,@> 测包含,jsonb_path_query 执行 SQL/JSON 路径语言。GIN 加速文档包含查询;表达式索引高速服务单一路径。文档内容的统计信息弱于列统计,JSON 谓词的规划器估算常常失真。

使用场景

归档原始外部载荷、按租户扩展属性、稳定核心列加可变明细的事件信封、形状未定时的原型。访问模式一旦稳定,把热字段抽出成真正的列。

常见误区

“像文档库一样无模式”只是列级灵活:表仍是关系的,约束只能靠 CHECK 与校验代码。把频繁过滤与连接的业务键压进 JSON,等于放弃索引与约束;把金额和时间戳存成 JSON 字符串,会陷入类型转换与排序规则的泥潭。读取宽文档时按路径选取,别整块取回。

代码示例

package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
)

type Profile struct {
	ID       int64          `json:"id"`
	Nickname sql.NullString `json:"-"`
	Bio      *string        `json:"bio"`
}

type profileJSON struct {
	ID       int64   `json:"id"`
	Nickname *string `json:"nickname"`
	Bio      *string `json:"bio"`
}

func nullString(value sql.NullString) *string {
	if !value.Valid {
		return nil
	}
	return &value.String
}

func (p Profile) MarshalJSON() ([]byte, error) {
	return json.Marshal(profileJSON{
		ID:       p.ID,
		Nickname: nullString(p.Nickname),
		Bio:      p.Bio,
	})
}

func scanProfile(row *sql.Row) (Profile, error) {
	var profile Profile
	err := row.Scan(&profile.ID, &profile.Nickname, &profile.Bio)
	return profile, err
}

func main() {
	encoded, _ := json.Marshal(Profile{ID: 7})
	fmt.Println(string(encoded))
	encoded, _ = json.Marshal(Profile{
		ID:       8,
		Nickname: sql.NullString{String: "ada", Valid: true},
	})
	fmt.Println(string(encoded))
}
-- 每个邮箱保留 created_at 最新的一行,标记其余为重复
WITH ranked AS (
  SELECT
    id,
    email,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY email
      ORDER BY created_at DESC, id DESC
    ) AS rn
  FROM subscribers
)
SELECT id, email, created_at, (rn = 1) AS is_primary
FROM ranked
ORDER BY email, rn;

-- 一次性清扫:删除全部非首行
WITH ranked AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY created_at DESC, id DESC
         ) AS rn
  FROM subscribers
)
DELETE FROM subscribers s
USING ranked r
WHERE s.id = r.id
  AND r.rn > 1;

-- 上游堵漏:唯一索引阻止重复再次写入
CREATE UNIQUE INDEX CONCURRENTLY idx_subscribers_email
  ON subscribers (email);