PostgreSQL SELECT DISTINCT 报 could not identify an equality operator for type json
普通 DISTINCT 比较所有输出列,json 缺少相等运算符。确认去重口径后在查询中生成或转换为 jsonb,并核对重复键、文本形式及转换边界。
查询同时返回日期和 json_build_object(...) 生成的对象,加上 DISTINCT 后报:
ERROR: could not identify an equality operator for type json
即使写成 SELECT DISTINCT(day), payload,也不是只对 day 去重。普通 DISTINCT 作用于整行输出,日期相同以后还要判断 payload 是否相同;PostgreSQL 的 json 类型没有这里需要的相等比较,而 jsonb 支持比较。
本文处理包含 json 输出表达式的普通 SELECT DISTINCT,示例使用当前受支持的 PostgreSQL 版本。若错误中的类型是其他自定义类型,或者失败发生在 GROUP BY、聚合函数内部,需要对照实际 SQL 定位,不能仅凭报错相似就改所有字段。
确认真正参与去重的输出列
先查看应用或 ORM 最终发给数据库的查询,特别是 SELECT * 展开的列。基础表列即使是 jsonb,某个 json_build_object 表达式仍会生成 json。
下面是不需要业务表的故障结构示例:
SELECT DISTINCT v.day, json_build_object('score', v.score) AS payload
FROM (VALUES
(DATE '2026-01-01', 5),
(DATE '2026-01-01', 5)
) AS v(day, score);
可以在本机测试库暂时去掉 DISTINCT,用 pg_typeof(json_build_object('score', v.score)) 查看生成列的类型。去掉去重只是缩小范围的办法;它改变了结果行数,不能在业务要求去重时当成最终修复。
这个例子没有排序和复杂聚合,仍需要比较 json。因此,给日期增加括号、修改 ORDER BY 或添加索引,都不能补上类型缺失的相等运算。
按 JSON 数据内容去重,生成 jsonb 输出
如果需要比较日期和 JSON 对象内容的组合,可以直接采用返回 jsonb 的构造函数:
SELECT DISTINCT v.day, jsonb_build_object('score', v.score) AS payload
FROM (VALUES
(DATE '2026-01-01', 5),
(DATE '2026-01-01', 5),
(DATE '2026-01-01', 6)
) AS v(day, score);
按这个去重规则,预期保留两行:同一天的分数 5 和分数 6 各一行。它不会强行压成“每天一行”。如果业务只允许每天一行,还必须决定应该保留哪个对象或如何汇总,不能用类型转换代替这个选择。
已有列或表达式返回 json 时,也可以只转换查询输出:
SELECT DISTINCT v.day, v.payload::jsonb AS payload
FROM (VALUES
(DATE '2026-01-01', '{"score":5}'::json),
(DATE '2026-01-01', '{ "score" : 5 }'::json)
) AS v(day, payload);
这个例子中的空白差异不会让两个 jsonb 对象变成不同值,预期得到一行。转换放在查询表达式中即可,不需要为消除这条报错直接执行整表字段类型变更。
转换会改变哪些比较细节
json 保留输入文本的空白、对象键顺序以及重复键;jsonb 不保留这些文本形式,对象内重复键只保留最后一个值。如果上下游要求保存原始 JSON 文本,或依赖重复键、键顺序,不能把转换视作无损替换。原始存储仍可保留,但查询输出和去重依据需要经过业务确认。
现有 json 数据也不保证都能转成 jsonb。后者对部分 Unicode 转义和数值范围有更严格的要求,例如不能表示的零字符转义或超出 PostgreSQL numeric 范围的数字可能导致转换失败。先在有代表性的测试数据上验证转换,不要遇到错误就删除原记录。
把对象转成 text 也可能避开运算符错误,但那会改成文本比较。在通常的文本比较条件下,空白和键顺序不同的两段 JSON 仍可被视为不同文本;这不等于按 JSON 内容去重。只有业务明确要比较序列化文本时,才考虑这种口径。
同样,不要仅为让查询通过就创建一个自定义 json 相等运算符。它需要定义完整且一致的比较语义,也不能保证自动满足所有去重和索引机制。
验证输出契约后替换查询
至少检查完全相同的对象、仅空白或键顺序不同的对象、同一日期但内容不同的对象,以及 SQL 空值。需要区分 SQL NULL 与 JSON 值 null,不要把两者在客户端显示得相似当成相等依据。
对原查询涉及的每个 json 输出都要核对类型;只修改一个构造函数,另一个 json 列仍可能使去重失败。确认结果行数、对象内容和客户端类型映射符合要求后,再替换应用 SQL。
保留原查询及返回样例,便于下游不兼容时回退。这里的处理不修改原始行,也不需要删除重复数据;如果必须保持原始文本格式而又需要按某些业务字段唯一,应重新设计去重键和输出步骤。


