调用函数
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;
虽然两种形式意图相近,但可移植性和边界行为可能不同。调用前需要确认:
- 参数总数是否正确;
- 每个位置需要什么类型;
- 下标从
0还是1开始; - 长度、精度或时区等参数如何解释;
- 超出范围、负数或空值会返回什么。
命名参数与混合写法
部分数据库允许按参数名称传值,使可选参数较多的调用更清楚:
-- 示意语法;参数名和 => 支持情况取决于数据库
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 时没有统一结果,需要查看函数契约。常见情况包括:
- 任一参数为
NULL就返回NULL; - 忽略空值,例如许多聚合函数;
- 专门判断或替换空值,例如
COALESCE; - 把
NULL当作独立输入,由函数自行处理; - 自定义函数在参数为
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 早于窗口计算。
假设函数只执行一次
数据库可能重排或重复调用。不要通过普通查询表达式依赖副作用,也不要用短路假设保护可能报错的函数。
选择和调用函数的步骤
- 明确需要标量、聚合、窗口还是表值结果。
- 查找目标数据库当前版本中的准确函数签名。
- 核对每个参数的位置、名称、类型、默认值和允许范围。
- 明确
NULL、空输入、越界值和错误输入的处理方式。 - 检查返回类型、精度、排序规则和时区语义。
- 动态数据使用参数绑定,动态函数名使用可信白名单。
- 对方言函数、边界值和性能敏感调用编写测试,并查看执行计划。
学习检查清单
读完本节后,应能够:
- 识别函数名、参数列表、实参表达式和返回值;
- 区分位置参数、命名参数、默认参数与显式
NULL; - 用显式类型转换解决重载歧义,并理解模式限定的作用;
- 区分标量函数、聚合函数、窗口函数和表值函数;
- 解释
COUNT(*)、COUNT(column)与COUNT(DISTINCT column)的区别; - 使用
OVER定义窗口分区、排序和框架; - 不依赖函数调用顺序、调用次数或未经保证的短路行为;
- 在调用函数前核对
NULL、返回类型、安全权限与方言差异。
调用函数的核心不是记住大量名称,而是把函数当作有明确输入、输出和行为边界的接口。先读签名,再检查类型与 NULL,最后验证目标数据库中的实际行为,才能让函数调用保持正确、清晰且可维护。