跳到主要内容

UNION、CASE 和相关结构

当一条 SQL 需要把多个表达式放到同一列位置时,数据库必须为它们找到一个公共类型。这个问题出现在 UNION、CASE、COALESCE、NULLIF、GREATEST、LEAST、VALUES 和数组构造器中。

如果公共类型选择不符合业务预期,结果可能变成文本、丢失精度,或在运行时才因某一行无法转换而失败。理解共同规则,比逐个记忆函数行为更有效。

集合操作的列对齐​

UNION、INTERSECT 和 EXCEPT 会按位置合并两个查询的结果:

SELECT customer_id, 'active' AS status
FROM active_customers
UNION ALL
SELECT customer_id, 'archived' AS status
FROM archived_customers;

两边必须有相同数量的列;第 1 列与第 1 列对齐,第 2 列与第 2 列对齐。列名通常取自左侧查询,但每个位置的类型需要兼容。UNION 还会去重,UNION ALL 保留全部行;去重方式不改变公共类型规则。

下面的列顺序虽然能通过语法解析,却可能把业务含义弄反:

-- 不推荐:列位置相同但语义相反
SELECT customer_id, customer_name FROM customers
UNION ALL
SELECT customer_name, customer_id FROM archived_customers;

始终显式列出相同语义和相同类型的列,不要依赖数据库把文本转换成数字来“凑齐”结构。

公共类型推导的常见思路​

不同数据库的细节不同,但可以用下面的思路理解多数场景:

  1. 如果所有表达式类型相同,直接使用该类型。
  2. 域类型通常先按其基础类型参与判断,除非所有输入都保持同一域。
  3. 忽略未知类型的字符串字面量,直到出现已知类型;如果全是未知字符串,通常选择文本类型。
  4. 如果已知类型属于不同类别(例如数字和日期),通常无法自动找到公共类型。
  5. 在同一类别内,根据隐式转换方向和首选类型逐步选择一个能接收其他输入的类型。
  6. 选择完成后,所有分支都转换到该类型;某个值无法转换时,整个表达式失败。

“能找到公共类型”不等于“结果就是你想要的类型”。金额、标识符和日期建议在每个分支中显式转换。

UNION 中的类型转换​

SELECT CAST(order_id AS BIGINT) AS id
FROM current_orders
UNION ALL
SELECT CAST(order_id AS BIGINT) AS id
FROM archived_orders;

如果左侧是 INTEGER、右侧是 BIGINT,数据库可能自动提升到更宽的整数;但如果一侧是文本,结果可能被推导为文本,导致后续排序变成字典序:

SELECT 2 AS code
UNION ALL
SELECT '10' AS code;

不要假设上例一定得到数值列。需要数值语义时,两侧都写成数值类型:

SELECT CAST(2 AS INTEGER) AS code
UNION ALL
SELECT CAST('10' AS INTEGER) AS code;

ORDER BY 应放在整个集合表达式最后;若对结果排序,确认公共类型已经满足排序语义。

CASE 的结果分支​

CASE 的各个 THEN 和 ELSE 表达式共享一个结果列,因此需要共同类型:

SELECT CASE
WHEN quantity = 0 THEN '无库存'
WHEN quantity < 10 THEN '库存偏低'
ELSE '库存充足'
END AS stock_label
FROM products;

上例所有分支都是文本。如果混合文本和数字,结果可能被提升为文本,或者直接因为无法转换而失败:

-- 不推荐:同一列混合两种业务语义
CASE WHEN is_valid THEN amount ELSE '无效' END

更好的做法是让结果类型保持一致,把状态和金额拆成两列,或统一用 NULL 表示没有金额:

CASE WHEN is_valid
THEN CAST(amount AS DECIMAL(12, 2))
ELSE CAST(NULL AS DECIMAL(12, 2))
END AS valid_amount

CASE 不等于过程式短路​

数据库通常不会执行未选中的 THEN 分支,但常量表达式可能在计划阶段被折叠,聚合和子查询也可能提前计算。因此不要依赖 CASE 保护所有危险表达式:

-- 不要把“不会走到”当作除零保护的唯一手段
CASE WHEN divisor <> 0 THEN numerator / divisor ELSE NULL END

更明确的写法是使用 NULLIF:

numerator / NULLIF(divisor, 0)

对于可能失败的文本转换,使用目标数据库提供的安全转换,或先在数据清洗阶段隔离非法值。CASE 的类型推导也可能要求数据库检查每个分支的可转换性,即使某个分支按数据分布很少执行。

COALESCE 和 NULLIF​

COALESCE(a, b, c) 返回第一个非 NULL 值;所有参数需要兼容的共同类型:

SELECT COALESCE(discount, CAST(0 AS DECIMAL(12, 2))) AS discount
FROM products;

NULLIF(a, b) 在两者相等时返回 NULL,否则返回第一个参数,结果类型通常跟随 a:

SELECT total / NULLIF(item_count, 0) AS average
FROM daily_summary;

不要写 COALESCE(text_amount, 0) 并期待它自动保持数值类型;先把文本解析成数值,再使用同类型的默认值:

COALESCE(CAST(text_amount AS DECIMAL(12, 2)), CAST(0 AS DECIMAL(12, 2)))

GREATEST 和 LEAST​

这两个结构分别返回最大值和最小值,也需要参数之间可以转换:

SELECT GREATEST(started_at, completed_at) AS latest_time
FROM jobs;

日期、时间戳和带时区时间不能随意混用。字符串参数可能按文本比较,而不是按数字或日期比较:

-- 可能按字符顺序比较,结果不代表数值最大值
SELECT GREATEST('2', '10');

先转换成明确的数值或日期类型,再调用这些结构。不同数据库对其中包含 NULL 时是返回 NULL 还是忽略空值,也可能不同,请查目标版本文档。

VALUES 行值列表​

VALUES 既可作为 INSERT 的数据源,也可作为查询中的临时表:

SELECT *
FROM (VALUES
(1, 'Alice'),
(2, 'Bob')
) AS v(id, name);

同一列位置的值会推导出共同类型。混合 NULL 与字符串时,通常由非空字符串决定类型;只有 NULL 时应显式标注:

VALUES
(CAST(NULL AS BIGINT), CAST(NULL AS DATE)),
(1001, CAST('2025-01-01' AS DATE));

在批量 INSERT 中,每一列还要再次转换到目标表类型。提前固定 VALUES 的类型可以让错误在更靠近数据源的位置出现。

数组构造器和复合值​

数组元素也需要共同类型:

SELECT ARRAY[
CAST(1 AS BIGINT),
CAST(2 AS BIGINT)
] AS ids;

ARRAY[1, 2.5] 可能提升为精确小数或近似数值,具体规则依数据库而异。混合文本和数字通常不适合作为数组元素,因为后续查询会失去明确的比较语义。

支持复合类型的数据库还会对每个字段分别做赋值转换。构造复合值时,字段顺序和字段类型必须同时匹配,不能只依赖显示出来的文本格式。

域类型在公共类型中的处理​

域类型包含基础类型和额外约束。集合或条件结构混合域值与基础类型值时,域约束可能不再作为结果类型的一部分;如果业务必须保留域约束,应让所有分支都显式转换为该域,并在最终写入/使用处再次验证。

-- PostgreSQL 风格示例
CASE
WHEN amount >= 0 THEN CAST(amount AS positive_amount)
ELSE CAST(NULL AS positive_amount)
END

域约束不是公共类型算法的替代品;结果类型正确也不代表值满足业务范围。

集合操作的括号和类型位置​

多个集合操作组合时,括号会影响哪一部分先形成公共类型:

(SELECT code FROM a
UNION ALL
SELECT code FROM b)
UNION ALL
SELECT code FROM c;

建议在每个子查询中先把同一列转换成统一类型,再组合集合。这样比依赖多层 UNION 的逐步推导更易读,也能避免左结合顺序造成的意外结果。

性能与可维护性​

公共类型转换通常对每行执行。若集合两侧都把大文本转换成数字后再去重或排序,CPU 和临时空间开销可能明显增加。更好的方式是从源表开始保存正确类型,或在导入层建立规范化列。

同时,UNION 的去重需要比较整行;如果业务允许重复,使用 UNION ALL 可以避免不必要的排序或哈希。性能优化不能改变公共类型和 NULL 语义,仍应先确认结果正确。

检查清单​

  1. 集合两侧列数、顺序和业务含义是否一致?
  2. 每个 CASE、COALESCE 或 VALUES 列是否有明确的目标类型?
  3. 是否无意中把数字/日期提升成文本?
  4. NULL、非法文本、舍入和时区边界是否测试?
  5. 是否依赖未选分支或条件短路来避免错误?
  6. 结果是否还要写入表列、排序、聚合或交给应用程序?

小结​

  • UNION 等集合结构按列位置合并,CASE 等条件结构按分支合并;每个位置都需要共同类型。
  • 未知字符串、NULL、域类型和不同数值类别是最常见的推导难点。
  • 对金额、日期、标识符和公共接口,优先在每个分支显式转换。
  • 不要把 CASE 当作绝对的执行顺序保证;危险转换应使用安全函数或数据清洗流程。