跳到主要内容

调用函数

SQL 函数把输入值交给数据库中的一段预定义逻辑,并返回计算结果。最常见的调用形式是:

function_name(argument_1, argument_2, ...)

例如:

SELECT UPPER(name), ROUND(total_amount, 2)
FROM customers;

调用函数时,不仅要知道函数名,还要确认它接收哪些参数、参数类型和顺序、返回什么类型,以及遇到 NULL 时的行为。本文以通用 SQL 概念为主;具体函数名、命名参数语法、日期处理和字符串行为应以目标数据库及版本文档为准。

函数调用的组成​

一个普通函数调用包含以下部分:

ROUND(total_amount * exchange_rate, 2)
部分示例作用
函数名ROUND指定要执行的函数
左右括号(...)标记参数列表
参数total_amount * exchange_rate、2向函数提供输入
返回值舍入后的数值作为新的值继续参与 SQL

参数本身是值表达式,因此可以使用列、字面量、参数占位符、运算结果、CASE 或另一个函数调用:

SELECT UPPER(TRIM(COALESCE(display_name, username)))
FROM users;

阅读嵌套调用时应从最内层开始:先选择非空名称,再去除首尾空白,最后转换大小写。

标量函数​

标量函数通常针对一行的输入返回一个值。常见用途包括:

类别常见示例用途
字符串UPPER、LOWER、TRIM、SUBSTRING处理文本
数值ABS、ROUND、CEILING、FLOOR计算或舍入数值
日期时间CURRENT_TIMESTAMP、日期加减函数获取或处理时间
空值处理COALESCE、NULLIF选择非空值或构造空值
类型转换CAST转换数据类型

这里的函数名只是常见形态,并非所有数据库都提供完全相同的参数和结果。例如字符串长度可能按字符、字节或特定编码单元计算,日期加减也可能使用运算符、专用函数或 INTERVAL。

返回值可以继续组合​

函数返回值仍是值表达式,可以继续参与运算、比较、排序或另一个函数调用:

SELECT ROUND(unit_price * quantity, 2) AS line_amount
FROM order_items
WHERE LOWER(status) = 'paid'
ORDER BY ABS(balance) DESC;

对列套用函数可能使普通索引无法直接支持筛选,例如 LOWER(email) = ...。是否可以使用表达式索引、函数索引或其他优化方式取决于数据库,应通过执行计划验证。

按位置传递参数​

最通用的方式是按照函数签名规定的顺序传参:

SELECT SUBSTRING(product_code FROM 1 FOR 3)
FROM products;

有些数据库也提供逗号分隔形式:

SELECT SUBSTRING(product_code, 1, 3)
FROM products;

虽然两种形式意图相近,但可移植性和边界行为可能不同。调用前需要确认:

  1. 参数总数是否正确;
  2. 每个位置需要什么类型;
  3. 下标从 0 还是 1 开始;
  4. 长度、精度或时区等参数如何解释;
  5. 超出范围、负数或空值会返回什么。

命名参数与混合写法​

部分数据库允许按参数名称传值,使可选参数较多的调用更清楚:

-- 示意语法;参数名和 => 支持情况取决于数据库
SELECT format_report(
report_id => 42,
include_details => TRUE
);

命名参数通常可以改变参数书写顺序,并跳过有默认值的可选参数。部分系统使用 =>,另一些可能使用 := 或完全不支持。

有的数据库还允许先写位置参数、再写命名参数:

SELECT format_report(42, include_details => TRUE);

一般不能在命名参数之后继续添加位置参数,否则难以确定对应关系。混合写法、重复参数、大小写处理及参数名是否属于公开接口,都必须按目标数据库规则确认。

默认参数与可变参数​

自定义函数可能给末尾参数设置默认值:

-- 假设 format_report 的 include_details 默认是 FALSE
SELECT format_report(42);

省略参数和显式传入 NULL 并不相同:省略参数会使用默认值,而传入 NULL 是把空值交给函数处理。

某些数据库还支持可变参数函数,即最后一个形参可以接收数量不定的实参:

-- CONCAT 的可用性与 NULL 行为因数据库而异
SELECT CONCAT(first_name, ' ', last_name);

可变参数的声明、数组展开方式和空参数列表规则都属于方言能力。调用自定义函数时,应把默认值和可变参数视为函数接口的一部分,不要仅凭函数名猜测。

无参数调用与特殊语法​

普通的零参数函数常写成:

SELECT random_value();

但 SQL 中有一些看起来像函数、却使用特殊语法的结构:

SELECT CURRENT_DATE;
SELECT CURRENT_TIMESTAMP;
SELECT CAST(total_amount AS DECIMAL(12, 2));
SELECT EXTRACT(YEAR FROM created_at);

它们可能不使用普通的逗号参数列表,甚至不带括号。不要为了“统一风格”擅自添加或删除括号;应按该数据库规定的语法书写。

函数签名、类型与重载​

函数签名通常由函数名、参数数量和参数类型共同决定。数据库可能允许同名函数处理不同类型,这称为重载:

calculate_tax(DECIMAL, DECIMAL)
calculate_tax(DECIMAL, INTEGER)

调用时,数据库会根据实参类型选择最合适的版本。以下情况容易产生歧义:

  • 字符串字面量尚未确定具体字符类型;
  • NULL 没有足够的类型上下文;
  • 参数可以隐式转换成多个候选类型;
  • 同名函数存在于多个模式中;
  • 新增一个重载后,旧调用匹配了不同版本。

可以用显式转换消除歧义:

SELECT calculate_tax(
CAST(:amount AS DECIMAL(12, 2)),
CAST(:rate AS DECIMAL(7, 4))
);

显式转换也可能造成精度损失,因此目标类型应依据函数签名和业务范围选择,而不是只为消除报错随意指定。

模式限定与名称解析​

自定义函数可能位于某个模式中,可以使用限定名明确调用目标:

SELECT billing.calculate_tax(total_amount, tax_rate)
FROM orders;

未限定函数名时,数据库可能按照搜索路径或当前模式查找。若用户能够在搜索路径靠前的位置创建同名函数,就可能改变名称解析结果。对安全敏感、跨团队共享或长期维护的 SQL,使用受信任模式并明确权限边界通常更稳妥。

模式限定规则、内置函数优先级和重载解析方式因数据库而异。不要把数据库 A 的搜索路径安全结论直接套用到数据库 B。

NULL 如何影响函数​

函数遇到 NULL 时没有统一结果,需要查看函数契约。常见情况包括:

  1. 任一参数为 NULL 就返回 NULL;
  2. 忽略空值,例如许多聚合函数;
  3. 专门判断或替换空值,例如 COALESCE;
  4. 把 NULL 当作独立输入,由函数自行处理;
  5. 自定义函数在参数为 NULL 时根本不执行函数体。

例如:

SELECT UPPER(nickname) FROM users;

UPPER(NULL) 在常见数据库中会返回 NULL,但不能据此推断所有函数。尤其是字符串连接、JSON 构造和自定义函数,它们对空值的处理常有差异。

不要未经业务确认就统一写成 COALESCE(value, '')。未知值、空字符串和零通常具有不同含义。

聚合函数​

聚合函数把一组行汇总成一个结果:

SELECT
customer_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_amount,
AVG(total_amount) AS average_amount
FROM orders
GROUP BY customer_id;

COUNT(*) 与 COUNT(expression)​

  • COUNT(*) 统计输入行数;
  • COUNT(column) 统计该表达式结果不为 NULL 的行数;
  • COUNT(DISTINCT column) 统计不同的非空值数量。
SELECT
COUNT(*) AS all_rows,
COUNT(shipped_at) AS shipped_rows,
COUNT(DISTINCT customer_id) AS customer_count
FROM orders;

SUM、AVG、MIN 和 MAX 通常忽略 NULL。当没有任何可聚合值时,除 COUNT 通常返回 0 外,其他聚合函数一般返回 NULL。

聚合中的 DISTINCT、排序与过滤​

部分聚合函数支持对参数去重:

SELECT COUNT(DISTINCT customer_id)
FROM orders;

某些数据库还支持在聚合调用内部指定顺序,或使用 FILTER 只让满足条件的行进入某个聚合:

SELECT
COUNT(*) AS all_orders,
COUNT(*) FILTER (WHERE status = 'paid') AS paid_orders
FROM orders;

FILTER、多列 DISTINCT、聚合内排序及字符串聚合函数的语法并不普遍一致。不能使用 FILTER 时,常见替代方式是条件聚合:

SELECT
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders
FROM orders;

聚合函数的使用位置​

聚合通常发生在 GROUP BY 之后,因此不能直接放在同层查询的 WHERE 中:

-- 按客户分组后筛选订单数
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 3;

WHERE 先筛选输入行,HAVING 再筛选聚合后的组。没有 GROUP BY 时,整个输入通常被视为一个组。

窗口函数​

窗口函数在保留明细行的同时,对一组相关行进行计算。它由函数调用和 OVER 子句共同构成:

SELECT
order_id,
customer_id,
total_amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY created_at, order_id
) AS order_sequence,
SUM(total_amount) OVER (
PARTITION BY customer_id
) AS customer_total
FROM orders;

OVER 中常见部分包括:

  • PARTITION BY:把结果划分为多个窗口分区;
  • ORDER BY:定义分区内部的逻辑顺序;
  • 窗口框架:限定当前行前后哪些行参与计算。
SELECT
sale_date,
amount,
SUM(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM daily_sales;

窗口函数不会像普通聚合那样把多行折叠成一行。窗口内没有稳定排序时,ROW_NUMBER 等结果可能不确定;排序键重复时,应补充唯一或稳定的第二排序键。

窗口框架的默认值会受是否存在 ORDER BY、同值行以及数据库方言影响。累计、移动平均等逻辑应显式写出 ROWS 或所需框架,并使用边界样例验证。

返回表或多行的函数​

某些函数返回的不只是一个标量,而是一张表或多行结果。这类函数通常出现在 FROM 中:

-- 示意语法;函数名与列定义取决于数据库
SELECT d.day
FROM generate_calendar(:start_date, :end_date) AS d(day);

返回表的函数可以像表一样被连接、筛选和投影。不同数据库可能称其为表值函数、集合返回函数或管道函数,其声明方式、列别名和相关引用能力各不相同。

不要默认标量函数可以放进 FROM,也不要默认表值函数能在 SELECT 中当作单个值使用;先确认函数返回的是标量、行、表还是游标等其他形式。

函数与存储过程的区别​

函数通常用于产生值,适合出现在表达式中:

SELECT calculate_tax(total_amount, tax_rate)
FROM orders;

存储过程更偏向执行操作,常使用 CALL:

CALL close_account(:account_id);

两者在事务控制、输出参数、结果集、副作用和权限模型方面可能完全不同。不能因为都接收参数,就把函数调用和 CALL 语句互换。具体边界由数据库实现决定。

求值次数、顺序与副作用​

优化器可以重排、合并或重复计算表达式。函数写在 SQL 文本中的次数,不一定等于运行时实际调用次数;条件的书写顺序也不保证函数按从左到右执行。

WHERE is_valid = TRUE
AND parse_value(raw_text) > 0

不要假设 is_valid 一定先执行,从而保护 parse_value 不接收非法输入。应改用数据库提供的安全转换能力,或让函数自己验证输入并对非法值返回受控结果;仅仅多套一层子查询或 CTE,通常仍不能保证求值顺序。

数据库通常还会按函数性质区分:

  • 相同输入始终得到相同结果;
  • 在一条语句中稳定,但不同语句可能变化;
  • 每次调用都可能变化或产生副作用。

这些分类会影响常量折叠、索引表达式、生成列、并行执行和缓存。自定义函数必须如实声明其性质;查询代码不要依赖未承诺的调用顺序或次数。

安全调用函数​

参数化动态值​

函数参数中的动态数据仍应使用绑定参数:

SELECT billing.calculate_tax(:amount, :tax_rate);

参数绑定能保留类型并避免 SQL 注入。占位符通常不能替代函数名、模式名或参数关键字;需要动态选择函数时,应使用可信白名单,而不是拼接任意输入。

注意权限与执行者身份​

自定义函数可能以调用者权限执行,也可能以定义者权限执行。后者如果搜索路径、动态 SQL 或对象权限配置不当,可能扩大访问范围。创建安全敏感函数时,应按目标数据库的安全模型:

  • 固定可信对象的模式;
  • 收紧函数和底层对象权限;
  • 对动态 SQL 使用参数绑定和标识符白名单;
  • 避免把未经验证的输入拼接成 SQL;
  • 审查函数是否会读写调用者本不应访问的数据。

普通查询使用者也应把函数视为可执行逻辑,而不是无害的文本格式化工具。

跨数据库使用函数​

函数是 SQL 方言差异最集中的部分之一。迁移时应逐项核对:

关注点可能的差异
函数名LENGTH、LEN 等名称不同
参数顺序起始位置、长度、格式模板的位置不同
返回类型整数宽度、数值精度、时区类型不同
NULL 行为忽略、传播或转换方式不同
字符规则按字符、字节或排序规则处理
日期时间时区、周起始日、月份加减规则不同
聚合语法FILTER、内部排序、字符串聚合不同
命名参数语法不同或不支持
表值函数放置位置和列定义方式不同

如果项目需要支持多个数据库,优先把方言函数隔离在少量查询、视图或数据访问层中,并为边界值、NULL、时区和字符集建立测试。

常见错误​

凭函数名猜参数顺序​

同名或相似函数在不同数据库中可能有不同签名。应查看准确版本的函数文档,并对起始位置、长度、舍入位数等边界进行测试。

混淆省略参数与传入 NULL​

默认参数只在参数被省略时生效。显式传入 NULL 后,函数会按空值规则处理,不一定采用默认值。

忽略返回类型​

函数显示结果看起来正确,不代表类型适合后续计算。日期格式化函数可能返回文本,数值函数可能扩大或缩小精度。需要时显式转换并检查元数据。

在 WHERE 中使用聚合函数​

聚合结果应在 HAVING 中筛选,或先在子查询、CTE 中计算,再由外层 WHERE 筛选。

把窗口函数当普通聚合​

窗口函数保留原始行,并且需要 OVER。若要筛选窗口计算结果,通常需要外层查询,因为同层 WHERE 早于窗口计算。

假设函数只执行一次​

数据库可能重排或重复调用。不要通过普通查询表达式依赖副作用,也不要用短路假设保护可能报错的函数。

选择和调用函数的步骤​

  1. 明确需要标量、聚合、窗口还是表值结果。
  2. 查找目标数据库当前版本中的准确函数签名。
  3. 核对每个参数的位置、名称、类型、默认值和允许范围。
  4. 明确 NULL、空输入、越界值和错误输入的处理方式。
  5. 检查返回类型、精度、排序规则和时区语义。
  6. 动态数据使用参数绑定,动态函数名使用可信白名单。
  7. 对方言函数、边界值和性能敏感调用编写测试,并查看执行计划。

学习检查清单​

读完本节后,应能够:

  • 识别函数名、参数列表、实参表达式和返回值;
  • 区分位置参数、命名参数、默认参数与显式 NULL;
  • 用显式类型转换解决重载歧义,并理解模式限定的作用;
  • 区分标量函数、聚合函数、窗口函数和表值函数;
  • 解释 COUNT(*)、COUNT(column) 与 COUNT(DISTINCT column) 的区别;
  • 使用 OVER 定义窗口分区、排序和框架;
  • 不依赖函数调用顺序、调用次数或未经保证的短路行为;
  • 在调用函数前核对 NULL、返回类型、安全权限与方言差异。

调用函数的核心不是记住大量名称,而是把函数当作有明确输入、输出和行为边界的接口。先读签名,再检查类型与 NULL,最后验证目标数据库中的实际行为,才能让函数调用保持正确、清晰且可维护。