值表达式
值表达式是能够产生一个值的 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 片段时都问三个问题:
- 它会产生什么值?
- 结果是什么数据类型?
- 输入包含
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)
可以从内向外理解:
- 读取
unit_price、quantity和discount_rate三列; - 用
1 - discount_rate算出折扣系数; - 依次相乘得到折后金额;
- 调用
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;
结果类型不一定简单等于某个输入类型。例如整数相乘可能溢出,整数相除可能截断,小数运算可能改变精度,字符串与日期的隐式转换也可能失败。
编写表达式时应关注:
- 输入类型是否兼容;
- 中间结果范围是否足够;
- 是否发生隐式转换;
- 最终类型是否符合使用位置;
- 不同数据库是否会推导出不同类型。
不要用试出来的显示格式代替类型判断。应查看表定义、函数签名或数据库提供的结果元数据。
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 这三个维度,就能把复杂查询拆成可验证的小步骤。