COALESCE / NULLIF / GREATEST / LEAST
这四个函数是 SQL 中最常用的「条件值处理」工具:COALESCE 提供首个非 NULL 的兜底值,NULLIF 把「相等」转换为 NULL(经典用途是防除零),GREATEST / LEAST 在一组值中取最大或最小。它们都能嵌入任意表达式位置,是数据清洗、报表兜底与业务规则化简的基础设施。
前置知识
阅读本节前,建议先了解:CASE WHEN 表达式
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 空值兜底 | COALESCE | 返回参数列表中第一个非 NULL 的值,参数不限个数 |
| 相等转空值 | NULLIF | 两个参数相等时返回 NULL,否则返回第一个参数 |
| 取最大值 | GREATEST | 返回参数列表中的最大值 |
| 取最小值 | LEAST | 返回参数列表中的最小值 |
| 短路求值 | Short-Circuit Evaluation | COALESCE 找到首个非 NULL 后不再计算后续参数 |
| 防除零 | Divide-by-Zero Guard | a / NULLIF(b, 0) 在 b 为 0 时返回 NULL 而非报错 |
| 空值传染 | NULL Propagation | GREATEST / LEAST 中任一参数为 NULL 时结果通常为 NULL |
| 类型归一 | Type Unification | 多参数函数要求各参数能归一到同一类型 |
| 默认值填充 | Default Filling | 用 COALESCE 把 NULL 替换为业务默认值 |
| 空串归一 | Empty-String Normalization | 用 NULLIF(col, '') 把空字符串转为 NULL |
这几个函数与 CASE 的关系:
COALESCE(a, b)⟺CASE WHEN a IS NOT NULL THEN a ELSE b ENDNULLIF(a, b)⟺CASE WHEN a = b THEN NULL ELSE a ENDGREATEST/LEAST无法用简单CASE等价表达(涉及多值比较),PostgreSQL 提供了原生实现。
-- 四个函数的最小示例
SELECT
coalesce(NULL, NULL, '兜底') AS coalesce_demo, -- 兜底
nullif(10, 10) AS nullif_demo, -- NULL
greatest(3, 9, 5) AS greatest_demo, -- 9
least(3, 9, 5) AS least_demo; -- 3语法与用法
| 函数 | 签名 | 返回 |
|---|---|---|
COALESCE | coalesce(v1, v2, ...) | 第一个非 NULL 的值 |
NULLIF | nullif(v1, v2) | v1 = v2 时返回 NULL,否则返回 v1 |
GREATEST | greatest(v1, v2, ...) | 最大值 |
LEAST | least(v1, v2, ...) | 最小值 |
-- 最小可运行示例
SELECT
coalesce(NULL, 0) AS a, -- 0
nullif('', '') AS b, -- NULL
greatest(1, NULL) AS c, -- NULL(NULL 传染)
least('b', 'a', 'c') AS d; -- a(按列排序规则比较)详细说明
COALESCE:首个非 NULL 的兜底值
COALESCE 接受任意数量的参数,从左到右扫描,返回第一个非 NULL 的值。若全为 NULL,则返回 NULL。参数个数至少为 1。
SELECT
coalesce(NULL, NULL, 'C') AS pick_c, -- C
coalesce(NULL, 'B', 'C') AS pick_b, -- B
coalesce('A', 'B', 'C') AS pick_a, -- A
coalesce(NULL, NULL) AS all_null; -- NULL典型用途:
-- 1. 字段兜底:优先展示昵称,其次用户名,最后默认值
SELECT coalesce(nickname, username, '匿名用户') AS display_name FROM users;
-- 2. 聚合结果兜底:无数据时返回 0 而非 NULL
SELECT coalesce(sum(amount), 0) AS total_amount FROM orders WHERE status = 'paid';
-- 3. 多列合并:取第一个有值的联系方式
SELECT coalesce(phone, email, wechat, '无联系方式') AS contact FROM users;短路求值是 COALESCE 的重要特性:一旦找到非 NULL 值,后续参数不会求值。这可用于性能优化,也可用于避免副作用。
-- 若 c1 非 NULL,expensive_fn() 不会被调用
SELECT coalesce(c1, expensive_fn(c2)) FROM t;与 CASE 相比,COALESCE 更简洁;但需要注意:它不能像 CASE 那样做条件判断(例如「大于 0 才用」),只能判断是否为 NULL。
NULLIF:把相等转为 NULL
NULLIF(v1, v2) 在 v1 = v2 时返回 NULL,否则返回 v1。最经典的用法是防除零。
-- 危险!b 为 0 时会报 division by zero
SELECT 100 / 0; -- 错误!
-- 安全:分母为 0 时返回 NULL
SELECT 100 / NULLIF(0, 0) AS safe_div; -- NULL实际查询中:
-- 计算转化率,避免除零
SELECT
campaign,
clicks,
orders,
round(orders::numeric / NULLIF(clicks, 0), 4) AS conversion_rate
FROM campaigns;另一个常见用途是把空字符串归一为 NULL,便于统一判空逻辑:
SELECT nullif(trim(description), '') AS description_clean FROM products;注意 NULLIF 只有两个参数,多于两个需要嵌套:
-- 把多个「无效值」都转成 NULL
SELECT nullif(nullif(status, ''), 'unknown') AS status_clean FROM t;GREATEST / LEAST:多值取最大最小
两者都接受任意数量参数,参数可以是常量、列或表达式。
SELECT
greatest(3, 9, 5) AS max_int, -- 9
least(3, 9, 5) AS min_int, -- 3
greatest('2025-01-01'::date, '2025-06-01'::date, '2025-03-01'::date) AS latest_date,
least(10.5, 2.75, 8.0) AS min_numeric; -- 2.75关键行为:任一参数为 NULL,结果通常为 NULL(NULL 传染)。这一点与很多人直觉中的「忽略 NULL」相反。
-- NULL 传染
SELECT
greatest(1, NULL, 3) AS with_null, -- NULL,不是 3
least(1, NULL, 3) AS least_null; -- NULL,不是 1
-- 需要忽略 NULL 时必须先用 COALESCE 兜底
SELECT greatest(coalesce(a, 0), coalesce(b, 0)) FROM t;在 PostgreSQL 中,GREATEST / LEAST 也支持字符串与日期,比较规则遵循该类型的排序规则。
-- 字符串比较(依赖 collation)
SELECT greatest('apple', 'banana', 'cherry') AS g; -- cherry
-- 日期比较:常用于取「最后期限」
SELECT greatest(due_date, now()::date) AS effective_due FROM tasks;三者的组合用法
这几个函数经常组合出现,用于复杂的数据清洗与计算。
-- 带宽限制在 [min_val, max_val] 区间内(clamp)
SELECT
sku,
least(greatest(stock, 0), 100) AS clamped_stock -- 先下限后上限
FROM products;
-- 安全除法 + 兜底默认值
SELECT
coalesce(round(amount / NULLIF(qty, 0), 2), 0) AS unit_price
FROM orders;
-- 空串、NULL 统一为默认值
SELECT coalesce(nullif(trim(name), ''), '未命名') AS clean_name FROM products;clamp 的写法顺序很重要:必须先 greatest 抬下限,再 least 压上限;否则先把大值压到上限后再抬下限,虽然本例结果相同,但在上下限可能反转时语义会出错。
与 CASE 的选择
| 场景 | 推荐写法 |
|---|---|
| 只在 NULL 与非 NULL 之间选择 | COALESCE |
| 判断业务条件(大小、区间、枚举) | CASE WHEN |
| 相等即视为无效(如 0、空串) | NULLIF |
| 多值取最大最小 | GREATEST / LEAST |
| 多值取最大最小但需忽略 NULL | GREATEST(coalesce(...), ...) |
COALESCE 与 CASE 性能基本一致,优化器会做等价改写;GREATEST / LEAST 在多参数时比手写嵌套 CASE 更简洁且更快。
NULLIF 的进阶:唯一约束中的「有效值唯一」
NULLIF 常配合唯一索引使用,实现「空值不参与唯一性检查」的语义。
-- 希望:非空值时唯一,空串与 NULL 视为「无值」不参与去重
CREATE TABLE users (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text
);
CREATE UNIQUE INDEX uq_users_email_valid
ON users (nullif(lower(trim(email)), ''));实战示例
步骤 1:建表并造数据
-- 统一的示例库
CREATE DATABASE demo;
DROP TABLE IF EXISTS campaigns;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
nickname text,
price numeric(10, 2),
min_price numeric(10, 2),
max_price numeric(10, 2),
stock integer
);
CREATE TABLE users (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username text,
nickname text,
phone text,
email text,
wechat text
);
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no text NOT NULL,
qty integer,
amount numeric(10, 2)
);
CREATE TABLE campaigns (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
clicks integer NOT NULL DEFAULT 0,
orders integer NOT NULL DEFAULT 0
);
INSERT INTO products (sku, name, nickname, price, min_price, max_price, stock) VALUES
('SKU-1001', '27 英寸显示器', '显示器', 1299.00, 999.00, 1599.00, 12),
('SKU-1002', '机械键盘', NULL, 399.00, 299.00, 499.00, -3),
('SKU-1003', '无线鼠标', '鼠标', 159.00, NULL, NULL, 150),
('SKU-1004', 'HDMI 线缆', NULL, 49.00, 39.00, 59.00, NULL);
INSERT INTO users (username, nickname, phone, email, wechat) VALUES
('zhangsan', '张三', '13800000001', 'zs@example.com', NULL),
('lisi', NULL, NULL, 'ls@example.com', 'lisi_wx'),
('wangwu', '小王', '13800000003', NULL, NULL),
(NULL, NULL, NULL, NULL, NULL);
INSERT INTO orders (order_no, qty, amount) VALUES
('SO-2001', 2, 2598.00),
('SO-2002', 0, 399.00),
('SO-2003', NULL, 159.00),
('SO-2004', 3, 477.00);
INSERT INTO campaigns (name, clicks, orders) VALUES
('春季促销', 1200, 36),
('新品首发', 0, 0),
('会员日', 800, 12);步骤 2:COALESCE 多级兜底
SELECT
id,
coalesce(nickname, username, '匿名用户') AS display_name,
coalesce(phone, email, wechat, '无联系方式') AS contact
FROM users
ORDER BY id; id | display_name | contact
----+--------------+----------------
1 | 张三 | 13800000001
2 | lisi | ls@example.com
3 | 小王 | 13800000003
4 | 匿名用户 | 无联系方式
(4 rows)-- 聚合兜底:无数据时返回 0
SELECT coalesce(sum(amount), 0) AS total_amount
FROM orders
WHERE qty > 100; -- 无匹配行 total_amount
--------------
0.00
(1 row)步骤 3:NULLIF 防除零
-- 单笔订单均价;qty 为 0 或 NULL 时返回 NULL 而非报错
SELECT
order_no,
qty,
amount,
round(amount / NULLIF(qty, 0), 2) AS unit_price
FROM orders
ORDER BY order_no; order_no | qty | amount | unit_price
----------+-----+---------+------------
SO-2001 | 2 | 2598.00 | 1299.00
SO-2002 | 0 | 399.00 |
SO-2003 | | 159.00 |
SO-2004 | 3 | 477.00 | 159.00
(4 rows)-- 再把 NULL 兜底成 0,用于报表展示
SELECT
order_no,
coalesce(round(amount / NULLIF(qty, 0), 2), 0) AS unit_price
FROM orders
ORDER BY order_no; order_no | unit_price
----------+------------
SO-2001 | 1299.00
SO-2002 | 0.00
SO-2003 | 0.00
SO-2004 | 159.00
(4 rows)-- 转化率:避免除零
SELECT
name,
clicks,
orders,
round(orders::numeric / NULLIF(clicks, 0), 4) AS conversion_rate
FROM campaigns
ORDER BY id; name | clicks | orders | conversion_rate
----------+--------+--------+-----------------
春季促销 | 1200 | 36 | 0.0300
新品首发 | 0 | 0 |
会员日 | 800 | 12 | 0.0150
(3 rows)步骤 4:GREATEST / LEAST 与 NULL 传染
-- 演示 NULL 传染
SELECT
sku,
min_price,
max_price,
greatest(min_price, max_price) AS raw_greatest,
least(min_price, max_price) AS raw_least
FROM products
ORDER BY sku; sku | min_price | max_price | raw_greatest | raw_least
----------+-----------+-----------+--------------+-----------
SKU-1001 | 999.00 | 1599.00 | 1599.00 | 999.00
SKU-1002 | 299.00 | 499.00 | 499.00 | 299.00
SKU-1003 | | | |
SKU-1004 | 39.00 | 59.00 | 59.00 | 39.00
(4 rows)-- 修正:先用 coalesce 兜底,避免 NULL 传染
SELECT
sku,
greatest(coalesce(min_price, 0), coalesce(max_price, 0)) AS greatest_safe,
least(
coalesce(min_price, 'Infinity'::numeric),
coalesce(max_price, 'Infinity'::numeric)
) AS least_safe
FROM products
ORDER BY sku; sku | greatest_safe | least_safe
----------+---------------+------------
SKU-1001 | 1599.00 | 999.00
SKU-1002 | 499.00 | 299.00
SKU-1003 | 0.00 | Infinity
SKU-1004 | 59.00 | 39.00
(4 rows)步骤 5:clamp 区间限制与空串归一
-- 把库存限制在 [0, 100] 区间
SELECT
sku,
stock,
least(greatest(coalesce(stock, 0), 0), 100) AS clamped
FROM products
ORDER BY sku; sku | stock | clamped
----------+-------+---------
SKU-1001 | 12 | 12
SKU-1002 | -3 | 0
SKU-1003 | 150 | 100
SKU-1004 | | 0
(4 rows)-- 空串与 NULL 统一为默认值
SELECT
sku,
coalesce(nullif(trim(nickname), ''), name) AS label
FROM products
ORDER BY sku; sku | label
----------+----------------
SKU-1001 | 显示器
SKU-1002 | 机械键盘
SKU-1003 | 鼠标
SKU-1004 | HDMI 线缆
(4 rows)注意事项
GREATEST/LEAST遇 NULL 返回 NULL,不会被忽略。 这是与「取最大最小」直觉最大的偏差,务必先用COALESCE兜底。
-- 错误预期:希望得到 3
SELECT greatest(1, NULL, 3); -- NULL
-- 正确:先兜底
SELECT greatest(coalesce(1, 0), coalesce(NULL, 0), coalesce(3, 0)); -- 3COALESCE不能替代业务条件判断。 它只判断是否为 NULL,判断范围或枚举仍需CASE。
-- 错误理解:以为会返回 0(但 0 不是 NULL,会原样返回)
SELECT coalesce(0, 100); -- 0NULLIF(a, b)只比较a与b,不处理多值。 要排除多个无效值需嵌套调用。
-- 排除 '' 与 'unknown'
SELECT nullif(nullif(status, ''), 'unknown') FROM t;- 除零错误只被
NULLIF屏蔽,不会被COALESCE屏蔽。coalesce(b, 0)后仍可能为 0。
-- 错误!b 为 0 时依然报错
SELECT a / coalesce(b, 0);
-- 正确
SELECT a / NULLIF(b, 0);- 函数参数类型必须能归一到同一类型。 混用文本与数字需要显式转换,否则报错。
SELECT coalesce(NULL, 1) || 'x'; -- 可以(隐式转 text)
SELECT greatest(1, '2'); -- 错误!类型不兼容
SELECT greatest(1, '2'::int); -- 正确字符串的
GREATEST/LEAST依赖排序规则(collation)。 不同collation下大小写与重音的处理不同,跨库迁移时结果可能变化。coalesce在WHERE中包裹索引列会让索引失效。
-- 不推荐:索引失效
WHERE coalesce(price, 0) > 100
-- 推荐:拆成等价条件
WHERE (price > 100) OR (price IS NULL AND 0 > 100)NULLIF判断基于等值比较,NULL 与任何值比较都不相等,因此nullif(NULL, 'x')返回 NULL(原样返回第一个参数)。
最佳实践
- 聚合结果一律用
COALESCE兜底为 0,避免前端拿到 NULL 需要额外判空。
SELECT coalesce(sum(amount), 0) AS total FROM orders;- 任何除法都用
NULLIF(分母, 0)包裹,这是防止线上division by zero报错的最低成本手段。
SELECT amount / NULLIF(qty, 0) AS unit_price FROM order_items;GREATEST/LEAST的每个参数都先用COALESCE兜底,把 NULL 传染风险消灭在函数调用内部。
SELECT greatest(coalesce(a, 0), coalesce(b, 0), coalesce(c, 0)) FROM t;- 用
NULLIF+ 唯一索引实现「有效值唯一」,让空串与 NULL 不参与去重。
CREATE UNIQUE INDEX uq_users_email ON users (nullif(lower(trim(email)), ''));- 多级兜底写成一个
COALESCE而不是嵌套CASE,可读性与性能都更好。
SELECT coalesce(nickname, username, email, '匿名') FROM users;clamp 时先抬下限再压上限:
least(greatest(v, lo), hi),顺序固定便于统一规范。把兜底默认值定义成常量或函数,避免在多个查询里硬编码字符串,便于统一修改。
CREATE OR REPLACE FUNCTION display_name(nick text, uname text)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
SELECT coalesce(nullif(trim(nick), ''), nullif(trim(uname), ''), '匿名用户');
$$;- 避免在
WHERE与JOIN条件中对索引列使用这些函数。若必需,考虑建立对应的表达式索引。
-- 若查询固定为 mode,建立表达式索引
CREATE INDEX idx_users_email_norm ON users (nullif(lower(trim(email)), ''));下一节