跳到主要内容

值表达式

值表达式是能够产生一个值的 SQL 片段。这个值既可以直接来自字面量或列,也可以由运算、函数、条件判断、类型转换或子查询计算得到。

SELECT
unit_price * quantity AS line_amount,
CASE WHEN stock > 0 THEN '有货' ELSE '缺货' END AS stock_status
FROM products;

这里的 unit_price * quantity 和整个 CASE 都是值表达式。理解值表达式,关键是每次看到一个 SQL 片段时都问三个问题:

  1. 它会产生什么值?
  2. 结果是什么数据类型?
  3. 输入包含 NULL 时,结果会怎样?

本文以通用 SQL 为主。数组、JSON、日期运算、类型转换简写等能力具有较多方言差异,使用时应核对目标数据库及版本的文档。

值表达式出现在哪里​

SQL 中凡是需要“一个值”或“一个条件”的位置,通常都可以使用值表达式:

位置作用示例
SELECT计算结果列price * quantity AS amount
WHERE判断一行是否保留status = 'paid'
JOIN ... ON判断两行是否匹配o.customer_id = c.id
HAVING判断分组是否保留COUNT(*) >= 3
ORDER BY计算排序键COALESCE(priority, 0) DESC
UPDATE ... SET计算新值stock = stock - 1
VALUES构造待写入的行VALUES (1, 'Alice')
CHECK限制允许写入的数据CHECK (price >= 0)

不同位置对表达式还有额外要求。例如 WHERE、ON 和 HAVING 需要条件结果;CHECK 中通常不能任意访问其他表;生成列、索引表达式等位置可能限制非确定函数或子查询。

从简单值组合成复杂表达式​

值表达式可以逐层组合。下面的表达式由列引用、字面量、运算符和函数调用构成:

ROUND(unit_price * quantity * (1 - discount_rate), 2)

可以从内向外理解:

  1. 读取 unit_price、quantity 和 discount_rate 三列;
  2. 用 1 - discount_rate 算出折扣系数;
  3. 依次相乘得到折后金额;
  4. 调用 ROUND 将结果处理为两位小数。

复杂查询的阅读方法也是如此:先找最小表达式,再根据括号、运算符和函数调用向外展开。

原子表达式​

原子表达式是继续组合其他表达式的基础。

字面量​

字面量把值直接写在 SQL 中:

SELECT 100, 19.95, 'paid', TRUE, DATE '2026-09-16';

常见类型包括数值、字符串、布尔值、日期和时间。字符串使用单引号;日期、时间字面量和布尔值的具体支持程度因数据库而异。

NULL 也可以写在表达式中,但它表示缺失或未知,不是字符串 'NULL',也不是数字 0。

列引用​

列引用从当前行读取一个值:

SELECT product_name, price
FROM products;

查询涉及多张表时,应使用表名或表别名限定列,既避免同名冲突,也让来源更清楚:

SELECT c.name, o.total_amount
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id;

c.name 可以理解为“当前组合行中,别名 c 对应记录的 name 值”。如果两张表都有 id 列,只写 id 通常会产生“列名不明确”的错误。

参数​

参数由应用程序在执行 SQL 时绑定,是动态数据值的占位符:

SELECT order_id, total_amount
FROM orders
WHERE customer_id = :customer_id
AND created_at >= :start_time;

占位符也可能写作 ?、$1 或 @name,具体取决于数据库和驱动。参数具有实际数据类型,应让驱动完成绑定,不要把值拼接进 SQL 文本。

参数通常只能代替数据值,不能代替表名、列名、关键字或排序方向。动态 SQL 结构必须从可信白名单中选择。

带括号的表达式​

括号可以改变计算顺序,也可以明确表达作者意图:

SELECT (price + shipping_fee) * quantity AS total_cost
FROM order_items;

即使默认优先级已经符合预期,复杂表达式仍建议使用括号分组,减少读者记忆优先级的负担。

运算符表达式​

运算符接收一个或多个操作数并返回结果:

SELECT
-balance AS opposite_balance,
price * quantity AS amount,
first_name || last_name AS full_name
FROM accounts;
  • -balance 是一元运算;
  • price * quantity 是二元运算;
  • 字符串连接符可能是标准 SQL 的 ||,也可能由数据库提供其他写法。

常见运算符类别包括算术、比较、字符串连接和逻辑运算。数据库通常按“括号 → 一元运算 → 乘除 → 加减 → 比较 → NOT → AND → OR”一类的顺序处理,但完整优先级及自定义运算符规则可能不同。表达式稍复杂时,应主动加括号。

WHERE (status = 'paid' OR status = 'shipped')
AND total_amount >= 100

函数调用​

函数接收零个或多个参数,并产生一个值或一组结果:

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

函数参数本身也可以是任意合法的值表达式,因此可以嵌套:

SELECT UPPER(TRIM(display_name))
FROM users;

不同数据库的函数名称、参数顺序和返回类型差异很大。函数的参数形式、聚合函数、窗口函数及可移植性会在调用函数中详细讲解。

条件表达式​

条件表达式根据条件选择结果,常用于分类、兜底和处理空值。

搜索式 CASE​

搜索式 CASE 依次检查各个条件,返回第一个结果为真的分支:

SELECT
order_id,
CASE
WHEN total_amount >= 1000 THEN '大额订单'
WHEN total_amount >= 100 THEN '普通订单'
ELSE '小额订单'
END AS order_level
FROM orders;

分支顺序会影响结果,因此通常先写范围更窄或优先级更高的条件。没有任何 WHEN 匹配且省略 ELSE 时,结果通常为 NULL。

简单式 CASE​

简单式 CASE 把一个表达式依次与多个值比较:

SELECT
status,
CASE status
WHEN 'pending' THEN '待处理'
WHEN 'paid' THEN '已支付'
WHEN 'cancelled' THEN '已取消'
ELSE '未知状态'
END AS status_text
FROM orders;

它适合等值映射。涉及范围、多个条件或 NULL 判断时,应使用搜索式 CASE。

COALESCE 与 NULLIF​

COALESCE 返回参数中第一个非 NULL 的值:

SELECT COALESCE(nickname, username, '匿名用户') AS display_name
FROM users;

NULLIF(a, b) 在 a 与 b 相等时返回 NULL,否则返回 a。它常用于避免除零:

SELECT total_amount / NULLIF(item_count, 0) AS average_amount
FROM order_summary;

CASE、COALESCE 和 NULLIF 在语法上看起来像函数或表达式,但数据库可能对它们采用专门的类型推断和求值规则。

类型转换表达式​

当输入类型不符合运算或函数要求时,可以显式转换类型:

SELECT CAST(quantity AS DECIMAL(12, 2)) / total_count AS ratio
FROM task_summary;

标准形式是 CAST(expression AS type)。部分数据库还支持 expression::type、CONVERT(...) 等方言写法。

显式转换适合以下场景:

  • 避免整数除法截断小数;
  • 为参数或 NULL 指定明确类型;
  • 统一 CASE 各分支的类型;
  • 控制数值精度和小数位;
  • 把文本转换成日期或数值。

转换并不保证成功。文本格式非法、数值超出范围或目标精度不足时,数据库可能报错或按方言规则舍入、截断。应用层传值时,优先绑定正确类型,不要依赖数据库猜测字符串含义。

标量子查询​

用圆括号包围、并且只返回一列至多一行的查询,可以作为单个值使用:

SELECT
c.customer_id,
c.name,
(
SELECT MAX(o.created_at)
FROM orders AS o
WHERE o.customer_id = c.customer_id
) AS last_order_time
FROM customers AS c;

这种子查询称为标量子查询:

  • 返回一行一列时,结果就是该值;
  • 没有返回行时,结果通常是 NULL;
  • 返回多行时会报错,因为单个表达式位置无法容纳多个值。

相关子查询会引用外层查询的列。它的实际执行方式由优化器决定,但大量逐行相关计算可能影响性能,应结合执行计划判断是否需要改写为连接或预聚合。

行值表达式​

行值表达式把多个标量组合成一个有顺序的行:

SELECT order_id, created_at
FROM orders
WHERE (created_at, order_id) < (:last_created_at, :last_order_id)
ORDER BY created_at DESC, order_id DESC;

它适合表达复合键比较、复合赋值或多列匹配。也可以用 ROW(...) 显式构造:

SELECT ROW(customer_id, order_id)
FROM orders;

行构造器、行比较以及其中的 NULL 处理并非所有数据库都一致。为追求可移植性,可以展开成多个明确条件,并仔细处理边界与空值。

数组、JSON 与字段访问​

现代数据库通常还允许从复合值中取出一部分:

-- 仅展示常见形态,具体语法取决于数据库
SELECT tags[1], profile.address, payload->>'status'
FROM events;

数组下标的起点、复合字段访问方式、JSON 运算符和返回类型具有明显方言差异。它们仍遵循同一个思路:先由左侧表达式产生复合值,再通过下标、字段名或路径得到新的值。

表达式的类型​

每个值表达式都有数据类型。数据库会根据输入、运算符、函数签名和上下文推导结果类型:

SELECT price * quantity AS amount
FROM order_items;

结果类型不一定简单等于某个输入类型。例如整数相乘可能溢出,整数相除可能截断,小数运算可能改变精度,字符串与日期的隐式转换也可能失败。

编写表达式时应关注:

  1. 输入类型是否兼容;
  2. 中间结果范围是否足够;
  3. 是否发生隐式转换;
  4. 最终类型是否符合使用位置;
  5. 不同数据库是否会推导出不同类型。

不要用试出来的显示格式代替类型判断。应查看表定义、函数签名或数据库提供的结果元数据。

NULL 的传播与三值逻辑​

许多普通运算在任一输入为 NULL 时也返回 NULL:

SELECT price * discount_rate
FROM products;

如果 discount_rate 为 NULL,结果通常也是 NULL。但聚合函数、COALESCE、IS NULL 等有自己的空值规则,不能一概而论。

比较表达式遇到 NULL 时通常得到 UNKNOWN,而不是 TRUE 或 FALSE:

-- 不能用这种方式判断空值
WHERE deleted_at = NULL

-- 正确
WHERE deleted_at IS NULL

WHERE 只保留结果为 TRUE 的行,结果为 FALSE 或 UNKNOWN 的行都会被过滤。设计表达式时,要明确 NULL 表示“未知”“不适用”还是“尚未填写”,再决定使用 IS NULL、COALESCE 或单独分支。

求值顺序不能想当然​

SQL 描述期望的结果,优化器可以重写表达式并调整执行方式。不要依赖条件从左到右执行,也不要把所谓“短路”当作防错机制:

-- 不应假设 amount <> 0 一定先执行
WHERE amount <> 0
AND total_amount / amount > 10

应把危险操作本身写成安全形式:

WHERE total_amount / NULLIF(amount, 0) > 10

同理,不要依赖有副作用的函数调用次数或调用顺序。数据库可能进行常量折叠、公共表达式消除、谓词下推或并行执行;具体保证应以目标数据库文档为准。

列别名不是通用变量​

别名用于命名输出列,但通常不能在同一层的其他 SELECT 表达式或 WHERE 中立即复用:

-- 多数数据库不允许 WHERE 使用同层 SELECT 别名
SELECT price * quantity AS amount
FROM order_items
WHERE amount >= 100;

原因是 WHERE 在逻辑上早于 SELECT 计算。可以重复表达式,或使用子查询、公共表表达式分层:

WITH item_amounts AS (
SELECT product_id, price * quantity AS amount
FROM order_items
)
SELECT product_id, amount
FROM item_amounts
WHERE amount >= 100;

ORDER BY 通常可以使用输出别名,但与输入列同名时的解析规则可能因数据库而异,应避免模糊命名。

完整示例​

下面的查询把多种值表达式组合在一起:

SELECT
o.order_id,
COALESCE(c.display_name, c.username) AS customer_name,
CAST(o.subtotal AS DECIMAL(14, 2))
- COALESCE(o.discount_amount, 0) AS payable_amount,
CASE
WHEN o.paid_at IS NOT NULL THEN '已支付'
WHEN o.expires_at < CURRENT_TIMESTAMP THEN '已过期'
ELSE '待支付'
END AS payment_status
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
WHERE o.created_at >= :start_time
AND (o.status <> 'cancelled' OR o.status IS NULL)
ORDER BY payable_amount DESC, o.order_id DESC;

其中包含:

  • o.subtotal:限定后的列引用;
  • :start_time:绑定参数;
  • o.subtotal - ...:算术表达式;
  • COALESCE(...):空值兜底;
  • CAST(...):显式类型转换;
  • CASE ... END:条件表达式;
  • o.paid_at IS NOT NULL:空值谓词;
  • payable_amount:供 ORDER BY 使用的输出别名。

常见错误​

忽略运算符优先级​

-- 实际范围可能大于预期
WHERE status = 'paid' OR status = 'shipped' AND amount >= 100

-- 明确表达业务规则
WHERE (status = 'paid' OR status = 'shipped')
AND amount >= 100

假设 NULL 等于零或空字符串​

NULL 表示值未知,不能自动当作 0 或 ''。只有业务语义确实允许时,才使用 COALESCE 提供默认值。

依赖隐式类型转换​

把数字、日期和布尔值作为文本传入,可能导致错误、歧义或索引失效。优先绑定正确类型,必要时显式 CAST。

让标量子查询返回多行​

标量位置只能接收一个值。应通过聚合、唯一条件或明确的查询结构保证至多一行,而不是依赖未排序的“第一行”。

把别名当作过程式变量​

SQL 的同一查询层不是从上到下执行的赋值语句。需要复用复杂结果时,用子查询或 CTE 建立清晰层次。

学习检查清单​

读完本节后,应能够:

  • 说明值表达式会产生值和类型,并判断它能出现在哪些 SQL 位置;
  • 使用字面量、列引用、参数、运算符和括号组合表达式;
  • 使用 CASE、COALESCE、NULLIF 和 CAST 表达条件与类型规则;
  • 理解标量子查询必须返回一列至多一行;
  • 认识行值、数组和 JSON 表达式的用途与方言差异;
  • 判断 NULL 对算术、比较和筛选结果的影响;
  • 不依赖表达式的书写顺序或短路求值;
  • 知道列别名不是同一查询层中的通用变量。

值表达式是 SQL 的“计算单元”。只要持续检查值、类型和 NULL 这三个维度,就能把复杂查询拆成可验证的小步骤。