开篇先聊个我实际接过的需求:业务方让我把一张用户表里的积分字段转成“等级”,比如积分大于等于10000显示“钻石会员”,大于等于5000显示“黄金会员”,其他显示“普通用户”。当时表里几百万行数据,我第一反应就是写UPDATE,把每个等级用单独的UPDATE跑一遍。同事看了一眼就说:你直接CASE WHEN一把梭不完了吗?我问CASE WHEN能在一个UPDATE里同时判断多个条件吗?他没有正面回答,只是让我去查PostgreSQL的CASE WHEN语法。
这就是PostgreSQL里日常到不能再日常的CASE WHEN语句。很多人知道它能在SELECT里做条件判断,但用的深度往往止步于“及格线”——能跑,但绕远路、踩坑、写出来的SQL没法维护。这篇文章我把CASE WHEN从语法、场景、踩坑到性能优化一次说透,都是我在生产环境里实打实用过的方案,适合正在学PostgreSQL的入门者,也适合写了几年SQL但没细抠过细节的开发者。
1. 先把CASE WHEN到底是个什么东西说清楚
1.1 为什么大家都绕不开这一句
CASE WHEN本质上是SQL语言里的条件表达式,它允许你在一条SQL语句内部实现“如果怎么样,就返回什么;否则就返回另一个什么”的编程逻辑。通俗点说,它弥补了SQL在“程序化判断”上的不足,让你不用把所有数据捞到应用层再用Java、Python、Go写if-else,直接在数据库里就把数据加工好了。
同样一份查询,如果你不走CASE WHEN,通常只能拆成多条SQL分别跑,然后在内存里拼装。或者是用JOIN一张“等级映射表”来做区间匹配。但很多场景下等级区间是硬编码的业务规则,没必要单独建一张表,用CASE WHEN就是最轻量、最直白的选择。
我在生产环境里总结下来,CASE WHEN最少有四个核心用途:第一,SELECT查询时做字段级的逻辑转换;第二,UPDATE时做有条件的更新;第三,配合聚合函数SUM、COUNT做“条件计数”和“按条件求和”;第四,在ORDER BY、WHERE、HAVING等子句里做复杂逻辑处理。很多开发者只用了第一种,后面几种用得少,这恰恰是CASE WHEN拉开SQL水平差距的地方。
1.2 两种语法形态,别搞混了
PostgreSQL支持两种CASE WHEN写法,看起来很像,但语义有差别。第一种叫“简单表达式”,格式是:
sql复制CASE column_name
WHEN value1 THEN result1
WHEN value2 THEN result2
ELSE default_result
END
这种写法把某个字段放在CASE后面,WHEN后面直接跟值,适合做“等值判断”。例如判断订单状态码是1、2还是3,分别映射成“待支付”“已支付”“已取消”,用简单表达式最顺手。
第二种叫“搜索表达式”,格式是:
sql复制CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
ELSE default_result
END
注意区别:搜索表达式在CASE后面不跟字段,WHEN后面跟的是一个完整的布尔条件表达式。这种写法更灵活,凡是可以写成布尔表达式的地方都能用,包括大于、小于、区间、模糊匹配、子查询等。开篇说的积分转等级,就是典型的搜索表达式场景。
两种写法不是二选一的关系。我个人的习惯是:等值判断用简单表达式,多条件区间判断用搜索表达式。这样做主要是为了让SQL在视觉上更清晰——别人一眼就能看出你这句CASE是做等值映射还是做区间判断。你非要反过来写也能跑,但维护性会差一些,尤其在代码Review的时候,同事看你用搜索表达式写了100个WHEN,没有一个等值判断,多少会觉得你绕。
另外两种写法之间还有一层容易忽略的关系:简单表达式其实可以改写成搜索表达式。比如CASE col WHEN 1 THEN 'A' END就等价于CASE WHEN col = 1 THEN 'A' END。所以在PostgreSQL内部处理逻辑上,两者最终是归一化的,但作为写代码的人,你还是要根据场景选合适的形态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写CASE WHEN最容易踩的坑
2.1 NULL不会等于NULL
这是我在面试别人时最爱问的一个点,也是实际开发中出现频率最高的CASE WHEN误用场景。很多人写判断时会下意识地用CASE WHEN col = NULL THEN '空' ELSE '非空' END,然后发现结果永远走ELSE分支,根本走不到THEN。原因在于SQL里NULL不等于NULL,NULL = NULL的结果是NULL,而NULL在布尔判断里会被当作FALSE,于是CASE就落到ELSE上了。
正确写法是WHEN col IS NULL THEN '空'。这个坑说大不大,说小不小,但一旦出现,往往是整段逻辑静默出错——SQL不报错,只是结果不对。我在一次数据校验任务中排查了两个小时,最后发现是一位同事把IS NULL写成了= NULL,导致所有空值都被分类到“非空”里。
顺带提一个更隐蔽的变种:有人喜欢用CASE WHEN NULLIF(col, '') IS NULL THEN '空' ELSE '非空' END,这也是可行的,但逻辑上多绕了一层。更推荐的做法是直接CASE WHEN col IS NULL OR col = '' THEN '空' ELSE '非空' END,把NULL和空字符串两种“空”显式区分开来。
2.2 各种数据类型的强制统一
CASE WHEN的所有THEN分支返回值的类型必须一致,或者至少能被PostgreSQL隐式转换。比如你写了CASE WHEN flag = 1 THEN '是' ELSE 0 END,PostgreSQL会报错或者把0隐式转成'0',具体行为取决于上下文。我在生产库里见过因为类型不一致导致查询计划器无法走索引的案例——字段是VARCHAR类型,但CASE里某个分支返回了数字,结果索引失效,全表扫描。
所以建议写CASE WHEN时,所有THEN分支都保持同样的数据类型。如果业务上确实需要返回不同精度的数值,最稳的做法是全部显式转成统一类型,比如都用CAST(... AS NUMERIC)。数值型还要注意整数除法的问题,1/2在PostgreSQL里返回0而不是0.5,如果想返回0.5,你得写成1::numeric/2。这一点在CASE里做计算比例时非常常见,是我踩过最多次的坑。
2.3 ELSE不写会怎样
有些同学写CASE WHEN时图省事,只写了几个WHEN分支,不写ELSE。这种做法能跑,但是当所有WHEN条件都不满足时,CASE表达式的返回值是NULL。很多刚接触的人会以为“都不满足就返回空字符串”,其实不然——PostgreSQL默认返回NULL。
这个行为有两种影响。第一,如果下游程序把NULL当成普通值处理,可能因为NPE或者空指针崩溃;第二,如果这个CASE的结果要插入到NOT NULL约束的字段里,会被直接拒绝。所以我的习惯是,除非你明确要做到“匹配不到就返回NULL”,否则一律写ELSE。哪怕ELSE就是ELSE '未知',也比留空更安全。
另一个细节是ELSE的位置。PostgreSQL要求在语法上ELSE必须写在所有WHEN分支的后面,THEN后面不能直接跟END,也不能把ELSE写到END后面。有些从Oracle转过来的同事会把NO_DATA_FOUND之类的处理逻辑跟在ELSE里,但那样写的是PL/SQL,不是标准PostgreSQL。
3. 从简单查询到复杂业务:六种高频场景实操
3.1 查询结果自动打标
这是最基础也最常见的用法。比如在一张订单表orders里,有一个状态字段status,类型为小整数,0表示待支付,1表示已支付,2表示已发货,3表示已完成。查询的时候不能直接把数字丢给前端,前端拿数字去翻译又会多一次联调,于是直接在SQL里映射成可读文本:
sql复制SELECT
order_id,
amount,
CASE status
WHEN 0 THEN '待支付'
WHEN 1 THEN '已支付'
WHEN 2 THEN '已发货'
WHEN 3 THEN '已完成'
ELSE '未知状态'
END AS status_text
FROM orders;
这种写法下,应用层拿到status_text直接展示即可。要提醒一句:如果状态枚举值会持续增加,建议把状态字典做成一张维表,然后用JOIN去关联,而不是无限堆CASE。虽然CASE WHEN写起来快,但每次加状态都要改SQL、发版,远不如改数据库维表来得灵活。
3.2 UPDATE时做条件更新
假设你要把一批用户的等级重新计算,原来可能要先SELECT出来,在应用层判断等级,再一条条UPDATE。用CASE WHEN可以直接在一个UPDATE里完成这件事:
sql复制UPDATE users
SET level = CASE
WHEN points >= 10000 THEN '钻石会员'
WHEN points >= 5000 THEN '黄金会员'
WHEN points >= 1000 THEN '白银会员'
ELSE '普通用户'
END;
注意这里会更新全表所有行。如果只想更新一部分行,一定要在UPDATE后面加WHERE条件。否则CASE虽然能把不需要更新的行也“算”一遍,虽然结果可能不变,但实际上会触发每一行的写操作,产生大量的WAL日志,性能损耗很大。更好的写法是:
sql复制UPDATE users
SET level = CASE
WHEN points >= 10000 THEN '钻石会员'
WHEN points >= 5000 THEN '黄金会员'
WHEN points >= 1000 THEN '白银会员'
ELSE '普通用户'
END
WHERE points <> CASE
WHEN points >= 10000 THEN 10000 -- 这是一个占位,实际应该用原level比较
END;
不过这个写法有点绕。更推荐的优化方式是先查出需要更新的主键范围,再UPDATE:
sql复制UPDATE users
SET level = CASE
WHEN points >= 10000 THEN '钻石会员'
WHEN points >= 5000 THEN '黄金会员'
WHEN points >= 1000 THEN '白银会员'
ELSE '普通用户'
END
WHERE id IN (
SELECT id FROM users
WHERE level IS DISTINCT FROM CASE
WHEN points >= 10000 THEN '钻石会员'
WHEN points >= 5000 THEN '黄金会员'
WHEN points >= 1000 THEN '白银会员'
ELSE '普通用户'
END
);
这样虽然多了一层子查询,但能大大减少无谓的写操作。生产环境里,尤其是有大量历史数据的表,这个优化收益非常明显。我在一个百万级用户表上实测过,从全表更新7秒多降到只更新实际变化行不到1秒。
3.3 配合聚合函数做分组统计
这是一个非常经典、也非常好用的场景。有时候你不需要知道每一行的明细,只要按照某些条件统计数量和金额,用CASE WHEN配合SUM和COUNT就能一次性完成。
比如统计每个订单来源的转化效果:表里有一个渠道字段channel和是否支付字段is_paid(0/1),现在想按渠道统计订单总数、支付订单数、支付总金额,一个SQL搞定:
sql复制SELECT
channel,
COUNT(*) AS total_cnt,
SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) AS paid_cnt,
SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) AS paid_amount
FROM orders
GROUP BY channel;
这里SUM(CASE WHEN ... THEN 1 ELSE 0 END)其实是CountIf的PostgreSQL实现方式。PostgreSQL没有内置CountIf函数,所以用CASE WHEN配合SUM是最常用的替代方案。逻辑也很简单:符合条件的行返回1,否则返回0,SUM之后就是符合条件的行数。
如果想要计算支付率,可以写成:
sql复制SELECT
channel,
COUNT(*) AS total_cnt,
SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) AS paid_cnt,
ROUND(
100.0 * SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) / COUNT(*),
2
) AS paid_rate
FROM orders
GROUP BY channel;
这里有个细节:一定要用100.0乘以总额,而不是100。如果写100 * ...,PostgreSQL会把整条除法表达式推断为整数除法,导致支付率全部变成0或1。这一点我踩过不止一次,建议各位在写比率计算时,优先用100.0或者显式::numeric转一下类型。
3.4 ORDER BY里做自定义排序
默认的ORDER BY只能按字母、数字大小排序,但业务上经常出现需要按照“给定顺序”排序的场景。比如一个页面要展示订单状态,要求顺序是“待支付、已支付、已完成、已取消、售后中”,不是按状态码大小排。你可以用FIELD函数,但PostgreSQL没有这个函数,标准做法是ORDER BY + CASE:
sql复制SELECT
order_id,
status
FROM orders
ORDER BY
CASE status
WHEN '待支付' THEN 1
WHEN '已支付' THEN 2
WHEN '已完成' THEN 3
WHEN '已取消' THEN 4
WHEN '售后中' THEN 5
ELSE 99
END;
这种方法尤其在报表排序、BI看板、门户首页等场景中很实用。当然,如果这个自定义排序规则要被多处SQL复用,建议把排序权重做成一张维表,避免每个SQL里都粘一大段CASE。
3.5 在WHERE里用CASE做复杂过滤
WHERE子句里也能用CASE,但性能上要谨慎。有些需求是“如果用户传入了状态筛选,就按状态过滤;如果没有传入,就不过滤”。这种动态查询条件,很多人会用ORM拼SQL,或者在应用层拼AND条件。其实用CASE也能做,比如:
sql复制SELECT *
FROM orders
WHERE
CASE
WHEN :input_status = '' THEN TRUE
WHEN status = :input_status THEN TRUE
ELSE FALSE
END;
这个写法本身是对的,但性能上很容易让优化器放弃索引。原因是CASE表达式内的判断对优化器来说不够透明,计划器没办法确定它能否改写为简单的status = $1。更推荐的方式是用( :input_status = '' OR status = :input_status )这种写法,PostgreSQL会把它优化得更充分。
所以在WHERE里使用CASE时,我的原则是能用OR/AND组合实现的就不写CASE。只有在OR条件导致索引失效、需要强制统一逻辑时才考虑CASE。实际生产里,OR改写为UNION ALL往往比CASE更好。
3.6 与NULLIF、COALESCE配合
CASE WHEN经常和其他条件函数配合使用,尤其是NULLIF和COALESCE。NULLIF(a, b)表示如果a = b则返回NULL,否则返回a;COALESCE(a, b)表示如果a为空则返回b,否则返回a。两者结合可以替代很多CASE WHEN的写法。
比如要防止除数为0,传统写法是:
sql复制SELECT CASE WHEN total = 0 THEN 0 ELSE paid / total END
用NULLIF配合COALESCE会更紧凑:
sql复制SELECT COALESCE(paid / NULLIF(total, 0), 0)
这条语句的逻辑是:当total为0时,NULLIF把0转成NULL,整个除法变成NULL,再被COALESCE替换成0。效果和CASE WHEN完全一致,但代码更短。不过要注意,如果paid本身也可能是NULL,那么结果可能和CASE WHEN的预期不同——CASE WHEN里你可以单独控制paid为NULL时的返回值,而简化写法没法精细控制。所以我的建议是:逻辑简单时用NULLIF+COALESCE简化;逻辑复杂、分支多时还是老实写CASE WHEN,可读性优先。
4. 常见问题与排查技巧实录
4.1 问题排查速查表
我整理了一个常见的CASE WHEN问题速查表,都是我实际排过的:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| CASE结果总是NULL | 没有写ELSE | 在末尾补ELSE分支 |
col = NULL判断不生效 |
SQL里NULL不能使用等号 | 改成col IS NULL |
| 查询结果精度不对 | 整数除法截断 | 用100.0 *或::numeric |
| THEN分支类型不一致 | PostgreSQL隐式转换导致意外行为 | 统一所有THEN返回类型 |
| UPDATE更新了所有行 | 没有加WHERE,或WHERE条件没排除不变行 | 在WHERE里加上只更新需要变动的条件 |
| WHERE里用CASE导致索引失效 | 优化器无法将CASE改写为简单比较 | 改用OR/AND组合或UNION ALL |
| CASE里嵌套太深,可读性差 | 分支过多,逻辑复杂 | 拆成多个CASE,或使用子查询/CTE |
| 结果集不符合预期但没报错 | 条件之间有重叠,前面的WHEN把后面的覆盖了 | 检查WHEN顺序,按优先级排列 |
| 字符串比较忽略大小写导致判断不准 | PostgreSQL默认区分大小写 | 用WHERE col ILIKE或LOWER(col)统一小写 |
| 多行CASE的缩进混乱,肉眼难以检查 | 没有统一格式 | 对齐THEN、ELSE缩进 |
4.2 与其他数据库的兼容性差别
不少团队是从Oracle、MySQL迁移到PostgreSQL的,CASE WHEN相关语法总体兼容性不错,但有几个差异点值得注意。
Oracle的写法是CASE WHEN ... THEN ... ELSE ... END,也支持DECODE函数。从Oracle迁到PostgreSQL时,要把所有DECODE改写成CASE WHEN。相比Oracle,PostgreSQL对NULL的处理更严格、类型检查也更细。比如Oracle里空字符串会被当作NULL,而PostgreSQL里空字符串和NULL是两回事,这一点在CASE WHEN分支判断时特别容易出错——如果你在Oracle里写WHEN col = '',迁到PostgreSQL后可能匹配不到任何数据,因为PostgreSQL的col = ''不等价于col IS NULL。
MySQL的IF函数在PostgreSQL里没有直接对应,标准做法也是改成CASE WHEN。不过MySQL的CASE WHEN在排序规则、字符集上的行为和PostgreSQL有差异,尤其是中文环境下的比较,要注意排序规则是否一致。
SQL Server的IIF函数同理也需要改写。让我印象深刻的一次是,从SQL Server迁移一张报表SQL,里面用了8个IIF嵌套,我全部改写成CASE WHEN后,不仅逻辑清晰了,查询速度还快了一些——因为PostgreSQL优化器对CASE WHEN的识别能力比IIF嵌套更友好。
4.3 一段让我印象深刻的排查经历
有一次线上报表数据一直不对,现象是某个月的数据比其他月份少了一大截。查了半天发现是CASE WHEN的WHEN顺序问题。当时同事写的逻辑是:
sql复制CASE
WHEN order_time >= '2023-01-01' THEN '2023年'
WHEN order_time >= '2022-01-01' THEN '2022年'
WHEN order_time >= '2021-01-01' THEN '2021年'
END
猛一看没问题,逐条评估每个条件,数据会被分到正确的年份。但问题出在他把条件写反了:先判断较大的年份范围,然后判断较小的年份范围。这种情况在数值型区间里可能不明显,因为比较是顺序执行的,但是当时他把条件写成WHEN order_time < '2022-01-01'在前面,恰好又被2021年的数据遮挡了,于是2021年的数据全部被归进“2022年之前”的分支里。
这个案例给我的教训是:写CASE WHEN的分支条件时,一定要在草稿纸上把边界值列出来,逐个比对,避免条件重叠或者顺序颠倒。尤其是时间区间、金额区间这种连续型数据,边界值最容易出错。
5. 从能用到好用:写出可维护的CASE WHEN
5.1 用CTE或子查询把CASE逻辑抽出来
当一条SQL里出现多个CASE WHEN,而且这些CASE之间还有依赖关系时,直接堆在SELECT里会非常难看。更推荐的做法是先用子查询或CTE把中间结果计算好,再在外层引用。比如先计算每个用户的标签,然后根据标签统计:
sql复制WITH user_tagged AS (
SELECT
user_id,
CASE
WHEN points >= 10000 THEN '钻石'
WHEN points >= 5000 THEN '黄金'
WHEN points >= 1000 THEN '白银'
ELSE '普通'
END AS user_tag
FROM users
)
SELECT
user_tag,
COUNT(*) AS cnt
FROM user_tagged
GROUP BY user_tag;
这样好处很明显:逻辑分层,阅读SQL的时候先看内层看懂了怎么打标签,再看外层看懂了怎么统计。如果内层逻辑变了,只需要改一处。实际项目里,这种“一层CTE负责打标,一层CTE负责汇总”的模式几乎可以覆盖80%的报表需求。
5.2 在生成列里固化CASE逻辑
PostgreSQL支持生成列(Generated Column),它允许你在建表时用一个表达式自动算出某个字段的值。如果某个CASE WHEN逻辑被好几个查询反复使用,可以直接建成生成列。
sql复制CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
points INTEGER NOT NULL DEFAULT 0,
level TEXT GENERATED ALWAYS AS (
CASE
WHEN points >= 10000 THEN '钻石会员'
WHEN points >= 5000 THEN '黄金会员'
WHEN points >= 1000 THEN '白银会员'
ELSE '普通用户'
END
) STORED
);
这样以后查询直接查level字段就行,不需要每次写CASE WHEN。而且生成列会自动更新,插入或修改points时会同步计算出level。这种方案的优点是逻辑集中在表结构里,所有查询统一复用,不会出现不同SQL里CASE WHEN条件不一致的问题。缺点是无法在生成列上直接建索引(PostgreSQL目前不支持索引生成列),所以如果要用level做过滤条件且数据量大,还是要权衡。
5.3 函数封装CASE逻辑
如果CASE WHEN的业务逻辑特别复杂,比如涉及多个字段的综合判断,建议封装成一个PostgreSQL函数。这样好处是:逻辑只写一遍,调用时语义清晰,测试时可以单独对函数做单元测试。例如:
sql复制CREATE OR REPLACE FUNCTION order_level(paid_amount NUMERIC, is_vip BOOLEAN)
RETURNS TEXT
LANGUAGE SQL
IMMUTABLE
AS $$
SELECT CASE
WHEN is_vip THEN 'VIP订单'
WHEN paid_amount >= 1000 THEN '大额订单'
WHEN paid_amount >= 100 THEN '普通订单'
ELSE '小额订单'
END;
$$;
注意IMMUTABLE这个标记,它告诉PostgreSQL优化器:相同的输入一定产生相同的输出。加了IMMUTABLE之后,函数可以被用在索引表达式中,性能更好。如果你的函数逻辑只依赖入参、不查表,一定要记得加这个标记。我见过很多人写函数从不标注IMMUTABLE,结果白白损失了优化机会。
5.4 动态SQL里的CASE生成技巧
最后聊一个进阶用法:当你在存储过程或函数中拼接动态SQL时,可能要根据外部条件动态生成CASE WHEN的WHEN分支。比如做一个通用的业绩分成计算模板,不同计费模式有不同的区间规则。这种情况下可以先获取配置,然后拼接出CASE WHEN。
sql复制DO $$
DECLARE
rule_sql TEXT;
dynamic_case TEXT;
BEGIN
SELECT string_agg(
format('WHEN amount >= %s THEN %s', min_amount, rate),
' '
ORDER BY min_amount DESC
)
INTO dynamic_case
FROM commission_rule
WHERE rule_type = 'standard';
rule_sql := format(
'SELECT id, CASE WHEN amount < 0 THEN 0 %s ELSE 0 END AS commission FROM orders',
dynamic_case
);
EXECUTE rule_sql;
END $$;
这种写法在计费系统、佣金计算里特别好用。但一定要防止SQL注入——要用format或者参数化方式拼SQL,不要直接拼接外部输入。另外,动态SQL执行前要确保至少有一条WHEN分支,否则生成的CASE只有ELSE,逻辑可能不对。
6. 写在最后的实战建议
我在实际项目中大量使用CASE WHEN之后,最深的体会是:它虽然简单,但要写出高质量、可维护、性能有保障的CASE WHEN,仍然需要积累。这里分享几个我个人的判断原则。
第一个原则是:能用简单表达式解决的尽量不写搜索表达式。等值映射场景下,简单表达式的可读性远高于一堆col = value的搜索表达式。第二个原则是:分支超过五个就警惕。CASE WHEN一旦分支太多,代码就会变得臃肿难读。此时优先考虑用维表JOIN,或者把逻辑抽取成函数。第三个原则是:在写每个分支前先问一句“如果前面的分支没匹配上,应该返回什么?”把ELSE想清楚再动笔。
最后再分享一个我在调优时反复使用的小技巧:把CASE WHEN直接放在INDEX表达式里。如果你的查询条件中经常用到一个由CASE WHEN转换出来的值,你可以为这个表达式建索引:
sql复制CREATE INDEX idx_order_status_text ON orders (
(CASE status WHEN 0 THEN '待支付' WHEN 1 THEN '已支付' ELSE '未知' END)
);
这个索引建立后,查询时直接写和索引相同的CASE表达式,PostgreSQL就能走索引扫描。我在一个订单表上实验过,加了这种索引后,按状态文本过滤的查询从全表扫描的600ms降到索引扫描的50ms左右。虽然这种索引不是常规方案,但遇到固定逻辑、高频过滤的查询时,它确实是一条巧路。
CASE WHEN的玩法远不止这篇文章写的这些,比如配合窗口函数做分组内条件计数、配合数组做多值匹配、在触发器里做数据校验等等。核心思路都是相通的:把“条件判断”这个编程动作以数据库原生的方式表达出来。只要你把基础语法吃透、边界条件想清楚、不同场景的经验攒够,这个语句会成为你日常SQL工具箱里最顺手的一件兵器。
