跳到主要内容

函数

调用函数时,数据库不仅要执行函数体,还要先确定“调用的是哪个函数”。同名函数可能有不同参数类型或不同返回类型,字符串字面量和 NULL 又可能没有明确类型,因此函数调用是类型转换最常见的发生点之一。

本文以通用 SQL 为主,并在需要时标注 PostgreSQL 风格写法。函数名称、重载数量、可用的多态类型和失败处理函数都可能因数据库而异。

函数调用的基本结构​

SELECT ROUND(total_amount, 2) AS rounded_amount,
LOWER(customer_name) AS normalized_name
FROM orders;

数据库通常按以下步骤解析:

  1. 根据函数名和参数个数找候选函数。
  2. 确定每个参数当前的类型。
  3. 优先选择参数类型精确匹配的函数。
  4. 必要时尝试允许的隐式转换或多态匹配。
  5. 确定函数返回类型,并把它交给外层表达式继续解析。

如果仍有多个候选函数同样匹配,调用会因为歧义失败;如果没有候选函数,通常会看到“函数不存在”或“参数类型不匹配”的错误。

字面量决定不了函数时​

列通常有明确类型,字面量却可能需要上下文:

SELECT LENGTH('hello'),
ABS(-10),
COALESCE(NULL, 'unknown');

字符字面量 'hello' 一般可以转换为文本,因此 LENGTH 容易解析;NULL 没有值,COALESCE(NULL, 'unknown') 需要由另一个参数提供类型。只有 NULL 时,数据库可能无法确定应该使用哪一个重载:

-- 某些数据库中会因为类型未知而失败
SELECT some_overloaded_function(NULL);

-- 明确意图
SELECT some_overloaded_function(CAST(NULL AS INTEGER));

接口参数也建议在绑定阶段提供类型,而不是把所有值都作为文本发送。

显式转换函数参数​

当函数要求的类型与列类型不同,显式转换能让行为和错误位置更清楚:

SELECT DATE_TRUNC('month', CAST(created_at AS TIMESTAMP))
FROM events;

也可以使用数据库的类型转换语法。PostgreSQL 常见写法是 expression::type:

SELECT raw_id::BIGINT
FROM staging_rows;

:: 简短但不可移植;跨数据库的 SQL 优先使用标准 CAST(expression AS type)。不要为了让函数“能接收”而随意把金额转成浮点或把日期转成文本,这可能改变精度、时区和排序语义。

函数重载与类型优先级​

数据库可能同时提供:

format(INTEGER)
format(NUMERIC)
format(TEXT)

调用 format(1) 时,整数精确匹配通常优先;调用 format('1') 时,数据库可能把未知字符串解析为文本,也可能根据其他规则选择数字版本。调用 format(NULL) 则更容易产生歧义。

不要用“当前数据库恰好选中了某个版本”作为契约。对重载函数使用显式转换,尤其是公共视图、存储过程和跨版本迁移脚本:

SELECT format(CAST(:value AS NUMERIC(12, 2)));

添加一个新的重载函数也可能改变旧 SQL 的解析结果,因此扩展数据库功能时应回归测试依赖这些函数的查询。

返回类型会继续参与转换​

函数的返回值不是类型转换的终点,它还会作为外层操作符、函数或 CASE 的输入:

SELECT ROUND(price, 2) * quantity AS amount
FROM order_items;

这里 ROUND 的返回类型决定后续乘法采用精确数值还是近似数值。如果需要固定输出类型,可以在最外层显式转换:

SELECT CAST(ROUND(price * quantity, 2) AS DECIMAL(14, 2)) AS amount
FROM order_items;

但要注意:最外层转换无法恢复已经在函数内部丢失的精度。应先选择合适的输入类型,再决定显示格式。

字符串、数字和日期函数​

字符串格式化函数通常返回文本,不是“看起来像数字”就仍然是数字:

SELECT TO_CHAR(total_amount, 'FM999999990.00') AS display_amount
FROM orders;

display_amount 适合展示,不应再拿来做加法。需要计算时保留原始数值列,最后一步才格式化。

日期解析也应明确格式和时区:

SELECT CAST(:start_date AS DATE) AS start_date;

如果使用方言格式函数,应确认格式标记、语言环境和会话时区。不要把用户输入的 01/02/2025 当作跨地区稳定的日期格式。

聚合函数与输入类型​

聚合函数的结果类型通常与输入类型相关:

SELECT COUNT(*) AS row_count,
SUM(quantity) AS total_quantity,
AVG(unit_price) AS average_price
FROM order_items;

COUNT 常返回整数或大整数,AVG 可能返回精确小数或近似类型,SUM 还可能提升到更宽的数值类型。不要只根据客户端显示的数字判断结果类型;需要稳定接口时,用 CAST 固定返回类型,并测试空表和全 NULL 输入:

SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(14, 2)) AS total_amount
FROM payments;

这里先处理聚合结果为 NULL 的情况,再固定输出类型。COALESCE 中的 0 也应与金额类型兼容。

窗口函数与参数类型​

窗口函数保留每一行,但其参数仍会参与类型解析:

SELECT order_id,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY created_at
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS seven_row_total
FROM payments;

窗口边界中的数字是语法要求的整数概念,不要把它们当作可随意转换的业务值。日期窗口通常需要明确使用时间间隔类型或数据库方言。

多态函数和“兼容类型”​

部分数据库支持多态函数,例如一个函数可以接收任意数组并返回相同元素类型。这类函数会根据调用处的参数推导类型:

SELECT ARRAY[1, 2, 3];

如果数组元素来自不同类型,数据库必须寻找共同类型;无法找到时会失败。显式指定数组类型可以消除歧义:

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

多态函数的具体名称和类型约束是方言能力。迁移前应查目标数据库是否支持相同的泛型规则。

安全转换函数​

直接 CAST 遇到非法输入通常会使整个语句失败:

SELECT CAST(raw_quantity AS INTEGER)
FROM import_rows;

部分数据库提供失败时返回 NULL 的函数,例如 SQL Server 的 TRY_CAST、BigQuery 的 SAFE_CAST。它们便于隔离坏数据,但不是通用 SQL:

-- 仅在支持 SAFE_CAST 的数据库中使用
SELECT SAFE_CAST(raw_quantity AS INT64) AS quantity
FROM import_rows;

安全转换不会告诉你失败原因,也不会自动把空值和非法值区分开。生产导入应同时记录原始文本、失败原因和数据来源,避免把所有问题静默变成 NULL。

条件函数的共同类型​

COALESCE、NULLIF、GREATEST 和 LEAST 都会比较或合并多个参数,参数通常需要转换成共同类型:

SELECT COALESCE(discount, CAST(0 AS DECIMAL(12, 2))) AS discount,
NULLIF(status, '') AS normalized_status
FROM products;

如果分支混合文本和数字,数据库可能选择文本类型,后续数值运算就会失败。把参数先转换到业务类型,比等函数内部自动推断更安全。

函数嵌套与转换位置​

转换放在不同层次,结果可能完全不同:

-- 先相加再保留两位小数
SELECT CAST(price * quantity AS DECIMAL(14, 2))
FROM order_items;

-- 先把单价舍入,再相加
SELECT CAST(ROUND(price, 2) * quantity AS DECIMAL(14, 2))
FROM order_items;

金额、税费和比例计算应明确舍入时机。不要把“最后显示两位小数”误认为“整个计算过程都只有两位小数”。

与索引和执行计划的关系​

函数套在索引列上,常常会让普通索引无法直接使用:

-- 可能对每一行调用 LOWER
WHERE LOWER(email) = LOWER(:email)

改进方案可能是保存规范化列、使用数据库支持的函数索引,或在参数侧完成规范化:

WHERE email_normalized = :normalized_email

是否真的使用索引,应通过目标数据库的执行计划确认;不要仅凭 SQL 表面形式判断性能。

调试函数类型问题​

  1. 先查看函数签名和参数实际类型。
  2. 把复杂表达式拆成多列,逐层检查中间结果类型。
  3. 对 NULL、空字符串、非法文本和边界数值分别测试。
  4. 对重载函数和日期/数值格式使用显式 CAST。
  5. 固定对外返回类型,避免客户端根据一次查询结果猜测类型。

小结​

  • 函数调用首先是“函数解析”,然后才是执行;重载和未知字面量可能造成歧义。
  • 输入类型决定函数版本和返回类型,返回值还会继续参与外层表达式的转换。
  • 聚合、窗口和条件函数都可能改变类型或传播 NULL,空输入必须单独测试。
  • 需要容忍脏数据时使用数据库提供的安全转换,并记录失败,而不是静默吞错。
  • 函数套在索引列上可能影响性能;必要时调整数据模型或使用专用索引。