JSONB 的创建与插入
上一节我们确定了「默认一律用 JSONB」的结论。本节进入实操:如何正确地把 JSON 数据写进 JSONB 列。你将了解 JSONB 字面量的引号陷阱、五组生成函数(to_jsonb / jsonb_build_object / jsonb_object / row_to_json / json_object)的适用边界、从查询结果批量构造文档的写法、用 IS JSON 与检查约束把脏数据挡在写入侧,以及应用层参数绑定的正确姿势。
前置知识
阅读本节前,建议先了解:JSON 与 JSONB 的对比
基础概念
JSONB 列的值可以来自三个地方:SQL 里的字面量、生成函数、应用层传入的参数。三者的类型转换路径不同,踩坑点也不同。
| 写入方式 | 类型转换路径 | 典型问题 |
|---|---|---|
字面量 '{"a":1}'::jsonb | 字符串 → jsonb 输入函数 | 单引号需转义、JSON 内部的 " 与 SQL 的 ' 混淆 |
生成函数 jsonb_build_object(...) | 直接产出 jsonb,无二次解析 | 参数个数、键的合法性 |
| 应用层参数绑定 | 驱动按 jsonb 类型发送文本,服务端解析 | 应避免拼接 SQL 字符串 |
JSONB 的存储结构决定了几个必须提前知道的约束:
| 特性 | 取值 |
|---|---|
| 单个值上限 | 1 GB(与 JSON 相同) |
| 键的类型 | 只能是字符串(JSON 规范要求) |
| 值可为 SQL NULL 吗 | 可以,SQL NULL 的键值对会被整体丢弃(见下) |
| 数字范围 | 受 numeric 约束,1e400 等会报错 |
| 字符串限制 | 禁止 \u0000 |
有一个非常容易误解的点:jsonb_build_object('a', NULL) 不会生成 {"a": null},而是生成 {}。
SELECT
jsonb_build_object('a', NULL) AS built_object,
jsonb_build_array(1, NULL, 2) AS built_array,
'{"a": null}'::jsonb AS literal_object; built_object | built_array | literal_object
--------------+------------------+----------------
{} | [1, null, 2] | {"a": null}
(1 row)差别在于:SQL 的 NULL 在对象构造时会让整个键值对被跳过,在数组构造时则会被转成 JSON 的 null(因为数组元素不能跳过,只能占位)。而 JSON 字面量里的 null 是 JSON null,与 SQL NULL 完全无关,会被原样保留。混淆这两者是最常见的 JSONB 数据错误来源。
关键术语:
- JSON 字面量(JSON Literal):以文本形式书写的 JSON,需要显式转换
::jsonb才能写入列。 - JSON null 与 SQL NULL:前者是 JSON 里的一个值(
"a": null),后者是「没有值」。jsonb_typeof('null'::jsonb)返回null字符串,而 SQL NULL 的jsonb_typeof返回 SQL NULL。 - 生成函数(Creation Functions):直接返回
json/jsonb的函数,如jsonb_build_object。好处是完全绕过字符串转义。 - 美元引用(Dollar Quoting):
$tag$...$tag$形式的字符串字面量,内部不需要转义单引号,适合书写大段 JSON。
语法与用法
-- 1. 字面量转换
'{"a": 1}'::jsonb
'[1, 2, 3]'::jsonb
'null'::jsonb -- 合法:JSON 的 null 字面量
'"hello"'::jsonb -- 合法:JSON 字符串
'123'::jsonb -- 合法:JSON 数字
-- 2. 通用转换
to_jsonb(任意 SQL 值)
to_json(任意 SQL 值)
-- 3. 可变参数构造
jsonb_build_object(键1, 值1, 键2, 值2, ...)
jsonb_build_array(值1, 值2, ...)
-- 4. 文本数组构造
jsonb_object(text[]) -- 奇数个元素按 键,值,键,值 成对解释
jsonb_object(键数组 text[], 值数组 text[])
-- 5. 行/数组转换
row_to_json(复合类型)
array_to_json(任意数组)
-- 6. SQL/JSON 标准构造函数(PostgreSQL 16+)
json_object('k' VALUE 'v', 'k2': 'v2')
json_array(1, true, NULL::text)
json_scalar(123.45)
-- 7. 校验谓词(PostgreSQL 16+)
表达式 IS JSON -- 能否解析为 JSON
表达式 IS JSON OBJECT -- 是否对象
表达式 IS JSON ARRAY WITH UNIQUE KEYS -- 是否无重复键的数组
表达式 IS NOT JSON -- 取反
-- 8. 建表与插入
CREATE TABLE t (id bigint GENERATED ALWAYS AS IDENTITY, doc jsonb NOT NULL DEFAULT '{}'::jsonb);
INSERT INTO t (doc) VALUES ('{"a": 1}'::jsonb);最小的写入示例:
DROP TABLE IF EXISTS kv_demo;
CREATE TABLE kv_demo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
doc jsonb NOT NULL DEFAULT '{}'::jsonb
);DROP TABLE
CREATE TABLEINSERT INTO kv_demo (doc) VALUES
('{"a": 1, "b": "text"}'::jsonb),
(jsonb_build_object('c', 2, 'd', jsonb_build_array(1, 2))),
('{}'::jsonb); -- 使用默认值效果相同
SELECT * FROM kv_demo ORDER BY id; id | doc
----+--------------------------------------
1 | {"a": 1, "b": "text"}
2 | {"c": 2, "d": [1, 2]}
3 | {}
(3 rows)详细说明
字面量写入:单引号陷阱与美元引用
JSON 规范要求字符串用双引号定界,而 SQL 字符串字面量用单引号定界。这两套规则叠在一起,就产生了 JSONB 入门第一坑。
-- 错误!JSON 内部误用单引号,SQL 解析器把 ' 当成字符串结束符
INSERT INTO kv_demo (doc) VALUES ('{"name": 'Acme'}');ERROR: syntax error at or near "Acme"
LINE 1: INSERT INTO kv_demo (doc) VALUES ('{"name": 'Acme'}');
^即使侥幸通过 SQL 解析,JSON 解析器也不接受单引号:
-- 错误!单引号是合法的 SQL 字符串,但不是合法的 JSON
SELECT '{''a'': 1}'::jsonb;ERROR: invalid input syntax for type json
LINE 1: SELECT '{''a'': 1}'::jsonb;
^
DETAIL: Token "'" is invalid.正确做法有三条路径,按推荐程度排序:
-- 方案 A(最推荐):用生成函数,完全不用关心转义
SELECT jsonb_build_object('name', 'O''Brien', 'age', 30) AS by_function; by_function
---------------------------------------------------
{"age": 30, "name": "O'Brien"}
(1 row)-- 方案 B:美元引用,JSON 内部可以随意写引号
SELECT $json${"name": "O'Brien", "note": "含 \"双引号\" 也没问题"}$json$::jsonb AS by_dollar_quote; by_dollar_quote
--------------------------------------------------------------------
{"name": "O'Brien", "note": "含 \"双引号\" 也没问题"}
(1 row)-- 方案 C:老老实实把 JSON 内部的 " 写在 SQL 的 '' 之间
SELECT '{"name": "O''Brien"}'::jsonb AS by_escaping; by_escaping
---------------------------
{"name": "O'Brien"}
(1 row)三种写法的选择建议:
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 值来自 SQL 变量/列 | jsonb_build_object | 自动处理转义与类型 |
| 手写固定的演示数据 | 美元引用 | 可读性最好,无需数引号 |
| 少量简单字面量(无引号) | 普通单引号 | 最简洁 |
| 多行大文档 | 美元引用 | 换行不会被误解析 |
绝对不要用字符串拼接把用户输入塞进 JSON 字面量,这既是转义灾难也是注入入口:
-- 错误!用户输入 O'Brien 会让 SQL 语法崩掉;更糟的是可能被注入
-- INSERT INTO kv_demo (doc) VALUES ('{"name": "' || user_input || '"}');生成函数对比
五组生成函数的定位差异很大,选错会导致写出冗长或语义错误的 SQL:
| 函数 | 返回类型 | 输入形态 | 适用场景 |
|---|---|---|---|
to_jsonb(anyelement) | jsonb | 任意单个 SQL 值 | 把一行、一个数组、一个标量整体转 JSON |
jsonb_build_object(VARIADIC "any") | jsonb | 交替的键值对 | 手工拼装小对象,键名是字面量 |
jsonb_build_array(VARIADIC "any") | jsonb | 任意个值 | 手工拼装小数组 |
jsonb_object(text[]) | jsonb | 一维或二维文本数组 | 键值来自数组(如解构的行) |
row_to_json(record) | json | 复合类型 | 整个表行转对象,键名即列名 |
json_object(...) | json | k VALUE v 或 k: v | 标准 SQL/JSON 语法,需放宽到 PG16+ |
注意一个不对称:jsonb_build_object 系列返回 jsonb,而 row_to_json / array_to_json / json_object / json_array 返回 json。要用 jsonb 需要再转一次。
-- row_to_json 返回 json,需要 ::jsonb 才能放进 jsonb 列
SELECT
pg_typeof(row_to_json(kv_demo)) AS row_to_json_type,
pg_typeof(to_jsonb(kv_demo)) AS to_jsonb_type,
pg_typeof(jsonb_build_object('a', 1)) AS build_object_type; row_to_json_type | to_jsonb_type | build_object_type
------------------+---------------+-------------------
json | jsonb | jsonb
(1 row)jsonb_build_object / jsonb_build_array
这是最常用的两个函数。键会被强制转成 text,值按 to_jsonb 规则转换。
SELECT jsonb_build_object(
'id', 1001, -- 数字
'name', '机械键盘 Pro', -- 字符串
'active', true, -- 布尔
'price', 349.00, -- numeric
'tags', jsonb_build_array('hot', 'new'),
'spec', jsonb_build_object('switch', 'red', 'layout', '87键')
) AS product; product
------------------------------------------------------------------------------------------
{"id": 1001, "name": "机械键盘 Pro", "spec": {"layout": "87键", "switch": "red"}, "tags": ["hot", "new"], "active": true, "price": 349.00}
(1 row)参数个数必须是偶数,否则报错:
-- 错误!键值不成对
SELECT jsonb_build_object('a', 1, 'b');ERROR: argument list must have even number of elements
HINT: The arguments of jsonb_build_object() must consist of alternating keys and values.键不能是 SQL NULL:
-- 错误!键不能为 NULL
SELECT jsonb_build_object(NULL, 1);ERROR: null value not allowed for object keyjsonb_build_object 允许重复键,后者覆盖前者(与 JSONB 存储语义一致):
SELECT jsonb_build_object('a', 1, 'a', 2) AS dup_key; dup_key
---------
{"a": 2}
(1 row)jsonb_build_array 则是把 SQL NULL 转成 JSON null,不会跳过:
SELECT
jsonb_build_array(1, NULL, 'x') AS arr_with_null,
jsonb_build_array() AS empty_arr; arr_with_null | empty_arr
-------------------+-----------
[1, null, "x"] | []
(1 row)to_jsonb 与 row_to_json:把整行转成 JSON
数据已经在表里时,用 to_jsonb(表别名) 可以一行代码把整行转成对象,键名就是列名:
DROP TABLE IF EXISTS users_demo;
CREATE TABLE users_demo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username varchar(32) NOT NULL,
email varchar(128) NOT NULL,
is_active boolean NOT NULL DEFAULT true,
profile jsonb NOT NULL DEFAULT '{}'::jsonb
);
INSERT INTO users_demo (username, email, profile) VALUES
('alice', 'alice@example.com', '{"city": "上海", "level": 3}'),
('bob', 'bob@example.com', '{"city": "北京", "level": 1}');DROP TABLE
CREATE TABLE
INSERT 0 2SELECT to_jsonb(u) AS row_as_jsonb
FROM users_demo u
WHERE username = 'alice'; row_as_jsonb
-------------------------------------------------------------------------------------------------------------
{"id": 1, "username": "alice", "email": "alice@example.com", "is_active": true, "profile": {"city": "上海", "level": 3}}
(1 row)注意 to_jsonb(u) 会把 profile 内嵌为嵌套对象而不是字符串——这正是 to_json 系列的递归转换规则:数组与复合类型递归转换,jsonb 值保持 jsonb。相比 row_to_json,to_jsonb 少了一次「先转 json 再转 jsonb」的往返。
-- array_to_json:数组转 JSON 数组,二维数组转嵌套数组
SELECT array_to_json('{{1,5},{99,100}}'::int[]) AS matrix; matrix
------------------
[[1,5],[99,100]]
(1 row)array_to_json / row_to_json 的第二个布尔参数为 true 时会在顶层元素间插入换行,纯粹为了可读性:
SELECT row_to_json(t, true)
FROM (SELECT 1 AS a, 2 AS b) t; row_to_json
----------------------
{ +
"a":1, +
"b":2 +
}
(1 row)jsonb_object:从文本数组构造
jsonb_object 与 jsonb_build_object 的差别是输入是数组而非可变参数,且所有值都会被转成 JSON 字符串。这在从 array_agg 或二维数组构造时很方便。
-- 一维数组,元素数必须为偶数
SELECT jsonb_object('{a, 1, b, def, c, 3.5}'::text[]) AS from_1d; from_1d
--------------------------------------------------------
{"a": "1", "b": "def", "c": "3.5"}
(1 row)注意所有值都成了字符串("1" 而非 1),因为 jsonb_object 的输入被声明为 text[]。需要保留数值类型时应当用 jsonb_build_object 或 jsonb_object_agg。
-- 二维数组,每个内层数组恰好两个元素
SELECT jsonb_object('{{a, 1}, {b, "def"}}'::text[]) AS from_2d;
-- 分离的键数组与值数组
SELECT jsonb_object('{a, b}'::text[], '{1, 2}'::text[]) AS from_pair; from_2d
------------------------------------
{"a": "1", "b": "def"}
(1 row)
from_pair
------------------------------------
{"a": "1", "b": "2"}
(1 row)常见的三个错误:
-- 错误!一维数组元素个数为奇数
SELECT jsonb_object('{a, 1, b}'::text[]);ERROR: array must have even number of elements-- 错误!二维数组的内层不是两个元素
SELECT jsonb_object('{{a, 1}, {b, 2, 3}}'::text[]);ERROR: array must have two columns-- 错误!键数组中存在 NULL
SELECT jsonb_object('{a, NULL}'::text[], '{1, 2}'::text[]);ERROR: null value not allowed for object keySQL/JSON 标准构造函数(PostgreSQL 16+)
PostgreSQL 16 引入了 SQL/JSON 标准的构造函数族,语法更贴近标准,且支持 RETURNING 指定输出类型。
-- PostgreSQL 16+ 才开始支持
SELECT
json_object('code' VALUE 'P123', 'title': 'Jaws') AS obj,
json_array(1, true, json '{"a": null}') AS arr,
json_scalar(123.45) AS scalar; obj | arr | scalar
------------------------------------------------+--------------------+---------
{"code" : "P123", "title" : "Jaws"} | [1, true, {"a":null}] | 123.45
(1 row)json_object 支持 NULL ON NULL / ABSENT ON NULL 与唯一性检查:
| 子句 | 含义 |
|---|---|
NULL ON NULL | 值为 NULL 时保留键,值为 JSON null(默认) |
ABSENT ON NULL | 值为 NULL 时整个键值对省略 |
WITH UNIQUE KEYS | 出现重复键时报错 |
RETURNING jsonb | 直接返回 jsonb 而非默认的 json |
-- 值为 NULL 时的两种行为
SELECT
json_object('a' VALUE NULL, ABSENT ON NULL) AS absent_on_null,
json_object('a' VALUE NULL, NULL ON NULL) AS null_on_null; absent_on_null | null_on_null
----------------+--------------
{} | {"a" : null}
(1 row)输入校验:IS JSON 与检查约束(PostgreSQL 16+)
如果列类型是 text、bytea 或 json,可以用 IS JSON 谓词在 SQL 层面做校验。jsonb 列本身已由输入函数保证合法,无需额外校验。
| 谓词 | 检查内容 |
|---|---|
x IS JSON | 能否解析为任意 JSON |
x IS JSON VALUE | 同 IS JSON(完整 JSON 值) |
x IS JSON SCALAR | 是否标量(数字/字符串/布尔/null) |
x IS JSON ARRAY | 是否数组 |
x IS JSON OBJECT | 是否对象 |
x IS JSON OBJECT WITH UNIQUE KEYS | 对象且无重复键 |
x IS NOT JSON | 取反 |
SELECT js,
js IS JSON AS "json?",
js IS JSON SCALAR AS "scalar?",
js IS JSON OBJECT AS "object?",
js IS JSON ARRAY AS "array?"
FROM (VALUES
('123'), ('"abc"'), ('{"a": "b"}'), ('[1,2]'), ('abc')
) AS foo(js); js | json? | scalar? | object? | array?
------------+-------+---------+---------+--------
123 | t | t | f | f
"abc" | t | t | f | f
{"a": "b"} | t | f | t | f
[1,2] | t | f | f | t
abc | f | f | f | f
(5 rows)对 jsonb 列做结构约束则用 jsonb_typeof:
ALTER TABLE users_demo
ADD CONSTRAINT ck_profile_is_object
CHECK (jsonb_typeof(profile) = 'object');ALTER TABLE从查询结果批量构造文档
把关系数据聚合为 JSON 文档是 JSONB 最典型的写入场景。jsonb_agg + GROUP BY 可以一次构造出嵌套结构。
DROP TABLE IF EXISTS order_items_demo;
CREATE TABLE order_items_demo (
order_no varchar(16) NOT NULL,
sku varchar(32) NOT NULL,
qty integer NOT NULL,
price numeric(10,2) NOT NULL
);
INSERT INTO order_items_demo VALUES
('SO-1001', 'SKU-1', 2, 349.00),
('SO-1001', 'SKU-2', 1, 119.00),
('SO-1002', 'SKU-3', 5, 39.00);DROP TABLE
CREATE TABLE
INSERT 0 3-- 每个订单构造一个文档,明细作为嵌套数组
SELECT jsonb_build_object(
'order_no', order_no,
'items', jsonb_agg(
jsonb_build_object('sku', sku, 'qty', qty, 'price', price)
ORDER BY sku -- 聚合内排序,保证输出稳定
),
'total', sum(qty * price)
) AS order_doc
FROM order_items_demo
GROUP BY order_no
ORDER BY order_no; order_doc
-----------------------------------------------------------------------------------------------------------------
{"total": 817.00, "items": [{"qty": 2, "sku": "SKU-1", "price": 349.00}, {"qty": 1, "sku": "SKU-2", "price": 119.00}], "order_no": "SO-1001"}
{"total": 195.00, "items": [{"qty": 5, "sku": "SKU-3", "price": 39.00}], "order_no": "SO-1002"}
(2 rows)聚合函数内部的 ORDER BY 是必须的——不加的话数组元素顺序不确定,相同输入可能产出不同文档,导致 = 比较与缓存失效:
-- 不推荐:数组顺序不确定
SELECT jsonb_agg(jsonb_build_object('sku', sku)) FROM order_items_demo;-- 推荐:显式排序
SELECT jsonb_agg(jsonb_build_object('sku', sku) ORDER BY sku) FROM order_items_demo;jsonb_agg 会保留 NULL 元素,如果要跳过 NULL 用 jsonb_agg_strict:
SELECT
jsonb_agg(v) AS keeps_null,
jsonb_agg_strict(v) AS skips_null
FROM (VALUES (1), (NULL), (3)) AS t(v); keeps_null | skips_null
---------------+------------
[1, null, 3] | [1, 3]
(1 row)应用层传入:参数绑定与 PHP 示例
生产代码里 JSON 永远应该通过参数绑定传入,而不是拼进 SQL 文本。参数绑定有两个好处:不需要关心任何转义,且驱动会按类型发送。
下面是 PHP 8 + PDO 的写法(假定已连接 demo 库):
<?php
// JSONB 写入:把 PHP 数组序列化后作为参数绑定,绝不要拼进 SQL
$pdo = new PDO('pgsql:host=127.0.0.1;dbname=demo', 'app_user', 'secret', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$profile = ['city' => "Xi'an", 'level' => 3, 'note' => '含 "引号" 与 \'单引号\''];
$stmt = $pdo->prepare(
'INSERT INTO users_demo (username, email, profile)
VALUES (:username, :email, :profile::jsonb)
RETURNING id'
);
$stmt->execute([
':username' => 'carol',
':email' => 'carol@example.com',
// 用 json_encode 保证是合法 JSON,flag 保证非 ASCII 不被转义成 \uXXXX
':profile' => json_encode($profile, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
]);
echo $stmt->fetchColumn(), PHP_EOL; // 输出新行 id查询侧同理,把 JSON 当普通参数传:
<?php
// 按 JSONB 包含查询:条件本身也走绑定
$stmt = $pdo->prepare('SELECT id, username FROM users_demo WHERE profile @> :cond');
$stmt->execute([':cond' => json_encode(['city' => '上海'])]);
var_dump($stmt->fetchAll(PDO::FETCH_ASSOC));注意事项:
json_encode遇到非法 UTF-8 会返回false,所以一定要加JSON_THROW_ON_ERROR,否则会写入空字符串。- 不要为了「省事」把 JSON 用
addslashes处理后拼进 SQL,这对\u0000、控制字符等场景无效。 - 如果列是
jsonb,PostgreSQL 会自动规范化(去键重复、重排),不要在应用层假设写入什么就能读回什么。 \u0000无法写入:json_encode会把 PHP 字符串里的 NUL 编码成\u0000,PostgreSQL 的jsonb会拒绝,必须在应用层过滤。
实战示例
下面完整演示一个「用户行为事件」表的建表、多种写入方式、批量构造、结构校验与错误处理。
第一步:建表,包含约束与默认值
DROP TABLE IF EXISTS user_events;
CREATE TABLE user_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
event_type varchar(32) NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
occurred_at timestamptz NOT NULL DEFAULT now(),
-- 约束一:payload 必须是 JSON 对象,不能是数组或标量
CONSTRAINT ck_payload_is_object CHECK (jsonb_typeof(payload) = 'object'),
-- 约束二:payload 体积上限,避免单行无限膨胀
CONSTRAINT ck_payload_size CHECK (pg_column_size(payload) < 8192)
);
COMMENT ON COLUMN user_events.payload IS '事件负载,结构随 event_type 变化';DROP TABLE
CREATE TABLE
COMMENT第二步:三种写入方式对照
-- 1) 字面量(美元引用,适合手写固定数据)
INSERT INTO user_events (user_id, event_type, payload)
VALUES (1001, 'page_view', $json${
"url": "/products/list",
"referer": null,
"duration_ms": 1200
}$json$::jsonb);
-- 2) 生成函数(推荐:值来自变量时)
INSERT INTO user_events (user_id, event_type, payload)
VALUES (1001, 'add_to_cart', jsonb_build_object(
'sku', 'SKU-1',
'qty', 2,
'price', 349.00,
'source', 'detail_page'
));
-- 3) 从查询结果聚合(把批量事件一次性写入)
INSERT INTO user_events (user_id, event_type, payload)
SELECT
1002,
'search',
jsonb_build_object('keyword', kw, 'result_count', cnt, 'page', 1)
FROM (VALUES ('键盘', 42), ('鼠标', 88)) AS t(kw, cnt);INSERT 0 1
INSERT 0 1
INSERT 0 2第三步:读回验证
SELECT id, user_id, event_type, payload
FROM user_events
ORDER BY id; id | user_id | event_type | payload
----+---------+--------------+-----------------------------------------------------------------------
1 | 1001 | page_view | {"url": "/products/list", "referer": null, "duration_ms": 1200}
2 | 1001 | add_to_cart | {"qty": 2, "sku": "SKU-1", "price": 349.00, "source": "detail_page"}
3 | 1002 | search | {"page": 1, "keyword": "键盘", "result_count": 42}
4 | 1002 | search | {"page": 1, "keyword": "鼠标", "result_count": 88}
(4 rows)注意第 1 行:JSON 里的 "referer": null 被完整保留(这是 JSON null),而不是被丢弃——再次印证「JSON null 与 SQL NULL 不同」。
第四步:验证约束确实生效
-- 错误!payload 传了数组,违反 ck_payload_is_object
INSERT INTO user_events (user_id, event_type, payload)
VALUES (1003, 'bad_event', '[1, 2, 3]'::jsonb);ERROR: new row for relation "user_events" violates check constraint "ck_payload_is_object"
DETAIL: Failing row contains (5, 1003, bad_event, [1, 2, 3], 2026-09-22 10:00:00+08).-- 错误!payload 超过 8 KB,违反 ck_payload_size
INSERT INTO user_events (user_id, event_type, payload)
VALUES (1003, 'big_event', jsonb_build_object('blob', repeat('x', 10000)));ERROR: new row for relation "user_events" violates check constraint "ck_payload_size"第五步:用 IS JSON 校验外部文本(PostgreSQL 16+)
外部系统送来的原始文本还没进表时,可以先判断合法性:
-- PostgreSQL 16+ 才支持 IS JSON 谓词
SELECT raw,
raw IS JSON OBJECT AS is_object,
raw IS JSON OBJECT WITH UNIQUE KEYS AS no_dup_keys
FROM (VALUES
('{"a": 1, "b": 2}'),
('{"a": 1, "a": 2}'),
('[1,2]')
) AS t(raw); raw | is_object | no_dup_keys
--------------------+-----------+-------------
{"a": 1, "b": 2} | t | t
{"a": 1, "a": 2} | t | f
[1,2] | f | f
(3 rows)第六步:清理
DROP TABLE user_events;
DROP TABLE users_demo;
DROP TABLE order_items_demo;
DROP TABLE kv_demo;DROP TABLE
DROP TABLE
DROP TABLE
DROP TABLE注意事项
JSON 内部必须用双引号。JSON 的字符串定界符是
",SQL 的是',两者不能互换。含单引号的字符串值是 SQL 层的转义问题,含双引号的 JSON 语法是 JSON 层的规则,要分开思考。SQL NULL 与 JSON null 是两回事。
jsonb_build_object('a', NULL)产出{},'{"a": null}'::jsonb产出{"a": null}。前者丢字段,后者留字段,语义完全不同。jsonb_object会把所有值变成字符串。它的入参是text[],所以jsonb_object('{a, 1}')得到{"a": "1"}而不是{"a": 1}。需要保留数值类型就用jsonb_build_object或jsonb_object_agg。row_to_json/array_to_json/json_object/json_array返回json而非jsonb。写入jsonb列时会隐式转换,但显式写::jsonb更清晰,也避免在表达式中意外走json的慢路径。聚合构造文档时必须显式排序。
jsonb_agg不保证元素顺序,同一份数据可能产出不同文档,破坏=比较与缓存。统一写jsonb_agg(... ORDER BY 排序键)。\u0000永远写不进jsonb。应用层要先过滤 NUL 字节,否则报unsupported Unicode escape sequence。jsonb列别忘加NOT NULL DEFAULT '{}'::jsonb。否则下游每条查询都要判空,jsonb_set之类函数遇到 SQL NULL 也只会返回 NULL,静默失败。检查约束里的
jsonb_typeof只在值为 SQL NULL 时返回 NULL,而CHECK对 NULL 结果视为通过。所以「必须为对象」的约束要配合NOT NULL,或者写成payload IS NULL OR jsonb_typeof(payload) = 'object'。不要给
jsonb列再套一层text。它已经是二进制结构,再转text存储会丧失所有索引与操作符能力。大文档的插入代价明显高于小文档。
jsonb写入需要完整解析并转二进制,几 MB 的文档单次插入可能耗时几百毫秒,能拆就拆。
最佳实践
手写固定数据用美元引用,动态数据用生成函数。这是最省心的组合:
sql-- 固定演示/初始化数据 INSERT INTO t (doc) VALUES ($json${"a": 1, "note": "O'Brien"}$json$::jsonb); -- 动态值 INSERT INTO t (doc) VALUES (jsonb_build_object('name', v_name, 'qty', v_qty));建表时给默认值与结构约束:
sqlpayload jsonb NOT NULL DEFAULT '{}'::jsonb, CONSTRAINT ck_payload_is_object CHECK (jsonb_typeof(payload) = 'object')生产代码一律用参数绑定,并在序列化时打开严格模式(PHP 的
JSON_THROW_ON_ERROR、Python 的json.dumps后再让驱动处理),不要让非法 JSON 静默变成NULL。批量写入用
INSERT ... SELECT+ 聚合函数,一次网络往返写入多行,比逐行INSERT快一个数量级:sqlINSERT INTO user_events (user_id, event_type, payload) SELECT user_id, event_type, jsonb_build_object(...) FROM staging_events;用
COPY加载大批量 JSON 数据。把每行 JSON 放一列,COPY ... FROM STDIN是 PostgreSQL 最快的导入路径,比INSERT快数倍。写入前在应用层先校验 JSON 结构。数据库约束是最后一道防线,但报错发生在事务中间会拖累整个批次;应用层先过滤能显著降低失败重试成本。
需要保留原文时用双列设计:
payload_raw json存原文用于验签,payload jsonb存规范化版本用于查询。用生成列可以自动同步:sqlpayload_raw json NOT NULL, payload_parsed jsonb GENERATED ALWAYS AS (payload_raw::jsonb) STORED给 JSONB 文档的字段名用短名。键名在每一行都重复存储,
uid比user_identifier省下大量空间。不要用 JSONB 存「本该是列」的数据。
id、created_at、status这类高频筛选字段应当独立成列,JSONB 只承载结构不定的扩展部分。
下一节
继续学习:JSONB 的查询操作