DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

PostgreSQL 连续 UNION 的前两个分支为 NULL,报 text and integer cannot be matched

连续 UNION 的前两个分支只有无类型 NULL 时,中间结果可能先确定为 text。给占位值显式指定类型,并核对 UNION ALL 的行数与输出类型。

同一列合并一个 NULL 和一个整数时可以查询,加上第二个 NULL 分支后却报错:

SELECT NULL AS item_id
UNION ALL
SELECT NULL
UNION ALL
SELECT 42::integer;
ERROR: UNION types text and integer cannot be matched

三个分支里没有写任何文本值,错误却提到 text。原因是前两段查询先确定了自己的输出类型:两个未指定类型的 NULL 合并后成为 text,随后再与整数列合并时发生冲突。

本文针对这种连续集合查询中“前面的分支只有无类型占位值”的情况,规则适用于 PostgreSQL 的 UNION 和 UNION ALL;PostgreSQL 18 仍采用这套类型推断规则。它不意味着所有同名报错都由 NULL 引起,真实文本列与数值列混用、对应列位置写错,也会导致类型不兼容。

前两段查询已经决定了类型

没有括号改变结合方式时,连续的 UNION 从左向右结合。上面的 SQL 可以按下面的结构理解:

(
    SELECT NULL AS item_id
    UNION ALL
    SELECT NULL
)
UNION ALL
SELECT 42::integer;

每一层集合操作都要为每个输出列确定类型。这里的内层只有两个尚未指定类型的 NULL,因此把该列解析为 text。到外层时,左侧已经是确定的文本类型,不能再借助右侧的整数重新推断内层。

类型解析不按执行时先遇到的非空值决定。即使某个分支没有返回数据,查询仍然需要确定输出列的类型。只盯着结果里会不会出现空值,无法定位这一错误。

可以在本机测试数据库单独检查前两段的输出类型;下面是诊断示例,不需要创建业务表:

SELECT pg_typeof(u.item_id) AS resolved_type
FROM (
    SELECT NULL AS item_id
    UNION ALL
    SELECT NULL
) AS u;

按该规则,两行的 resolved_type 都应为 text。这一步检查的是中间查询的类型,而不是整个失败查询;把 pg_typeof 包在仍然报错的完整查询外面,并不能绕过类型解析失败。

在最早合并的位置给 NULL 指定目标类型

如果这个输出列的业务类型就是整数,可以在第一个占位值上指定类型:

SELECT NULL::integer AS item_id
UNION ALL
SELECT NULL
UNION ALL
SELECT 42::integer;

第一层合并此时已经有一个明确的整数输入,另一个无类型 NULL 可以按整数处理,后续整数分支也能匹配。这里指定的是空值的类型,不会把 NULL 变成 0。

如果 SQL 由程序动态生成,可以让每个占位分支都携带类型,避免删掉或调换某个分支后重新触发问题:

SELECT CAST(NULL AS integer) AS item_id
UNION ALL
SELECT CAST(NULL AS integer)
UNION ALL
SELECT 42::integer;

CAST(NULL AS integer) 与 NULL::integer 都可以表达这个意图。类型应按输出接口或目标列选择,不要因为示例用了 integer 就照搬;实际需要的是 bigint、日期或其他类型时,相应的占位值也应使用目标类型,并检查其余分支是否兼容。

多列查询需要逐列处理。例如报表同时输出标识和日期,缺少这两项的分支应明确保留两列及其位置:

SELECT NULL::integer AS item_id, NULL::date AS event_date
UNION ALL
SELECT NULL::integer, NULL::date
UNION ALL
SELECT 42::integer, DATE '2026-01-15';

这是查询形状的示例,日期是示例值。对应关系按列的位置建立,不会因为给列起了相同别名就自动重排。维护实际查询时,应先列出每个分支的输出列,再检查发生冲突的那一列;不要对整条结果一律转成文本。

改 ALL、换空字符串或在外层转换,都不是同一个修复

UNION 去除重复行,UNION ALL 保留重复行;两者都要求对应列的类型兼容。为了消除类型报错而切换它们,既没有处理无类型占位值,还可能改变报表行数。

把 NULL 换成 '' 也不合适。空字符串不是缺失值,若目标类型是整数,还可能产生无效整数输入错误。业务含义是缺失数据时,应保留带类型的 NULL。

在最外层写转换也太晚,例如:

SELECT u.item_id::integer
FROM (
    SELECT NULL AS item_id
    UNION ALL
    SELECT NULL
    UNION ALL
    SELECT 42::integer
) AS u;

内层集合查询仍然无法完成类型解析,外层转换没有机会修正它。需要把类型约束放回产生歧义的分支。

把整数分支挪到最前面,可能让这个小例子通过,但这会让类型依赖生成顺序,也不能保证最终结果按分支顺序返回。需要稳定类型时,优先显式标注占位值;需要稳定行序时,另行在最终查询中设计排序,不能用分支排列代替。

验收要同时看类型、空值和行数

修改查询模板前保存原版本,在本机测试数据库中先用上面的常量示例验证类型,再替换成业务查询。对修正后的示例可以这样检查:

SELECT pg_typeof(u.item_id) AS resolved_type, u.item_id
FROM (
    SELECT NULL::integer AS item_id
    UNION ALL
    SELECT NULL::integer
    UNION ALL
    SELECT 42::integer
) AS u;

预期是三行,类型均为 integer,其中两行的值为 NULL,一行为 42;未指定排序时不依赖行的排列顺序。如果原查询使用的是 UNION,不要为了套用这个三行预期而改成 UNION ALL,应按原有去重语义检查结果。

随后覆盖“前几个分支全为空”“中间分支被省略”“真实数据非空”等生成条件,并核对调用方读取到的列类型。若仍出现 text 与整数冲突,检查是否有真实文本字段参与合并,或某个嵌套子查询已经把占位列固定成文本;这种情况需要回到那一层修改,不能仅在最后增加一个带类型的分支。

修复只涉及查询表达式时,不需要改表类型、删除数据或重建数据库。回退应恢复查询模板及相关调用代码;若该查询已用于视图或应用接口,发布前同时核对其输出类型约定,避免把类型修复扩大成接口变更。