1. 函数本质:COALESCE到底在做什么
COALESCE在MySQL里的官方定义是:返回参数列表中第一个非NULL的值。语法极其简单,就是COALESCE(value1, value2, ..., valueN)。但越简单的东西,越容易被人低估。我见过不少开发者在SQL里写了一大堆IFNULL嵌套,或者用CASE WHEN写好几层判断,最后绕了一大圈,其实COALESCE一行就能解决。
1.1 从定义到执行机制
先把定义拆开看。COALESCE接收任意数量的参数,从左到右依次判断,遇到第一个不是NULL的,直接返回。如果所有参数都是NULL,返回NULL。
SELECT COALESCE(NULL, NULL, 'apple', 'banana'); -- 返回 'apple' SELECT COALESCE(NULL, NULL, NULL); -- 返回 NULL这个执行逻辑有个很容易被忽略的特性:它不一定会把所以参数都求值一遍。MySQL内部对COALESCE做了短路处理——从左往右扫,一旦遇到非NULL就直接返回,后面的参数就不再计算。这在处理复杂表达式的时候非常有用。举个例子:
SELECT COALESCE(expensive_function_1(), expensive_function_2(), 'default');如果expensive_function_1()已经返回了非NULL结果,expensive_function_2()压根不会被执行。这种特性在处理一些开销较大的计算或者可能报错的子查询时,能帮你省下不少性能开销。
参数个数方面,MySQL官方文档的说明是至少两个参数,但实际上你传一个参数也可以,只不过一个参数的情况下COALESCE就退化成单纯的NULL判断了,没什么意义。实际开发中建议不要传太多参数,超过四五个参数的时候,代码的可读性就会明显下降,后续维护起来也费劲。
1.2 MySQL版本兼容性与标准SQL身份
COALESCE是SQL标准里的函数,不是MySQL的方言功能。这意味着你写的SQL如果用了COALESCE,迁移到PostgreSQL、Oracle、SQL Server这些数据库的时候完全不需要改。这一点在做数据层抽象的时候非常重要。
MySQL从很早的版本就开始支持COALESCE,5.x系列就已经很完整了,8.0当然没有任何问题。所以你在任何现代MySQL环境里都能放心使用,不需要担心版本兼容问题。
相比之下,MySQL自己的IFNULL函数就是纯方言了,出了MySQL的门就不好使。有些团队做数据库选型的时候会刻意规避方言特性,COALESCE就成了NULL处理的首选。这也是为什么COALESCE出现在各种面试题、代码评审、数据库设计规范里的频率远高于IFNULL。
1.3 COALESCE与IFNULL的等价关系
经常有人问,COALESCE和IFNULL到底有什么区别?两者在只传两个参数的情况下,行为模式是等价的:
SELECT COALESCE(column_a, 'default_value'); SELECT IFNULL(column_a, 'default_value'); -- 两者返回结果一致但差异也很明显:
- IFNULL只能接收两个参数,COALESCE可以接收多个
- COALESCE是标准SQL,IFNULL是MySQL私有扩展
- COALESCE的参数类型规则更严格,返回值类型的选择逻辑略有不同
从代码的可扩展性角度考虑,如果你写两个参数的时候用IFNULL,后面业务需求变更需要再加一个兜底值,就得把IFNULL整个改成COALESCE。与其这样来回折腾,不如一开始就统一用COALESCE。我在实际开发中就因为这个改过不少SQL,深刻的教训。
2. 高频业务场景:从默认值到多字段兜底查询
COALESCE在真实业务里的应用范围比很多人想象的要广得多。它不只是简单处理一下NULL,还能解决很多看起来和NULL八竿子打不着的需求。下面这几个场景是我在实际项目里用得最多的情况,每个都附上了可复用的SQL片段。
2.1 默认值填充:用户昵称与展示字段的经典组合
最常见的使用场景就是查用户信息的时候,某个字段可能为NULL,业务上需要显示一个默认值。比如用户没有填写昵称时,界面显示一个系统生成的默认昵称:
SELECT user_id, COALESCE(nickname, CONCAT('用户', phone)) AS display_name, COALESCE(avatar_url, '/static/default_avatar.png') AS avatar FROM user_account WHERE status = 1;这里有个点值得注意:COALESCE的第二个参数可以是一个表达式,不只是静态值。用CONCAT('用户', phone)这种方式拼出来的默认昵称,比单纯写死一个“游客”要自然得多,用户体验也好一些。
2.2 多字段依次兜底:联系方式查询的连招用法
COALESCE最出彩的场景是它一次能传多个参数,做多字段级联兜底。比如一个订单表里记录了用户的电话、邮箱、微信三个联系方式,现在需要导出一份联系清单,要求如果电话为空就取邮箱,邮箱为空就取微信:
SELECT order_id, COALESCE(phone, email, wechat, '无联系方式') AS contact FROM orders WHERE created_at >= '2024-01-01';这种写法不用嵌套IFNULL,比如IFNULL(phone, IFNULL(email, IFNULL(wechat, '无联系方式')))——那玩意儿写出来之后自己都不想看第二遍。COALESCE用多个参数直接排开,逻辑一目了然,后面再加字段也只要在参数列表里往后追加就行。
类似的例子还有收货地址的省市区拼接,有些地址表结构是省市区县分开存的,但有的数据不完整,乡/镇级别的字段可能为空。展示的时候想要一个尽量完整的地址,就可以这样写:
SELECT CONCAT_WS(' ', COALESCE(province, ''), COALESCE(city, ''), COALESCE(district, ''), COALESCE(detail, '') ) AS full_address FROM shipping_address WHERE user_id = 10086;CONCAT_WS配合COALESCE可以让拼接结果里不出现NULL字样,算是一组非常实用的组合拳。
2.3 空字符串与NULL的双重归一化
MySQL里有个挺烦人的问题:有些历史数据,或者从Excel导入的数据,明明没有值,但存的不是NULL而是空字符串''。COALESCE判断的是NULL,空字符串对它来说就是一个合法的非NULL值,直接返回空字符串。处理这种场景的时候需要加一点判断:
SELECT COALESCE(NULLIF(TRIM(mobile), ''), '未填写') AS mobile_display FROM member_profile;这里用到了另一个函数NULLIF——如果TRIM(mobile)的结果等于空字符串,就返回NULL,然后COALESCE再接住这个NULL返回默认值。NULLIF加COALESCE的组合能同时处理NULL、空字符串、还有全是空格的情况。可以说这是我处理Excel导入数据时最常用的一招。
2.4 聚合计算中的NULL防范:SUM/AVG/MAX的隐形雷区
聚合函数和NULL的关系很多人都没彻底搞明白。比如AVG函数会自动忽略NULL,但如果分区里所有行的某一列都是NULL,AVG的结果就是NULL。再比如你往SUM的结果上再乘一个系数,NULL会让整个表达式的计算结果变成NULL,最后前端界面就会显示一个空。
SELECT dept_id, COALESCE(SUM(salary), 0) AS total_salary, COALESCE(AVG(score), 0) AS avg_score, COALESCE(MAX(login_time), '1970-01-01') AS last_login FROM employee GROUP BY dept_id;加一层COALESCE包在外面,就能保证没有数据的分组也能正常返回0,而不是返回NULL,避免上层应用报空指针异常。类似的情况在存储过程里也很常见,写过程序的人应该都懂这种返回空值的痛。
3. 同族函数横向对比:COALESCE、IFNULL、NULLIF与CASE WHEN
这几个处理NULL的函数放在一起对比能发现很多有意思的细节。选型的时候不是随便挑一个就行,每个函数的设计目标和适用场景其实差异挺大的。
3.1 函数维度对比表
| 函数名 | 参数个数 | 作用逻辑 | 标准SQL | 典型场景 |
|---|---|---|---|---|
| COALESCE | 2~N个 | 返回第一个非NULL值 | 是 | 多字段兜底、默认值填充 |
| IFNULL | 2个 | 和COALESCE两参数行为一致 | 否 | MySQL单字段默认值 |
| NULLIF | 2个 | 两值相等返回NULL,否则返回第一个值 | 是 | 空字符串转NULL、防除零 |
| CASE WHEN | 灵活 | 更复杂的条件分支 | 是 | 多条件判断且不便用函数的场景 |
从表达力的角度看,CASE WHEN其实能实现上面所有函数的逻辑。比如:
SELECT CASE WHEN phone IS NOT NULL THEN phone WHEN email IS NOT NULL THEN email ELSE '无联系方式' END AS contact FROM orders;这段逻辑和两参数的COALESCE完全等价,但写起来冗长很多。可是CASE WHEN也有它的独到优势——每个分支可以写更复杂的条件判断,比如字段值在某个范围内,这是COALESCE做不了的。
3.2 选型的判断依据
我的建议如下:
- 只处理单字段的NULL,且明确不打算迁移数据库 → IFNULL也行,但团队规范有要求的话以规范为准
- 多字段依次兜底 → COALESCE是首选
- 需要把空字符串当NULL处理 → NULLIF先处理,再套COALESCE
- 分支逻辑复杂,比如要根据某个字段的不同取值范围返回不同值 → 用CASE WHEN,不要强行用COALESCE嵌套
纯从代码风格统一性的角度考虑,我个人更建议全项目统一使用COALESCE。理由很简单,一种写法走天下,维护成本最低。我在代码评审里见过太多同事在同一个文件里混用IFNULL和COALESCE,逻辑倒是没错,但看起来总觉得别扭,风格不够统一。
3.3 一个完整的业务需求三种写法对比
假设有个需求:查询产品列表,产品标签为空时显示“暂无标签”,价格为空时显示0,且上下架状态以枚举值判断:
用COALESCE实现的写法:
SELECT product_id, product_name, COALESCE(tag, '暂无标签') AS tag, COALESCE(price, 0) AS price, status FROM product;用CASE WHEN实现的写法:
SELECT product_id, product_name, CASE WHEN tag IS NULL THEN '暂无标签' ELSE tag END AS tag, CASE WHEN price IS NULL THEN 0 ELSE price END AS price, status FROM product;用IFNULL实现的写法(假设只在MySQL上运行):
SELECT product_id, product_name, IFNULL(tag, '暂无标签') AS tag, IFNULL(price, 0) AS price, status FROM product;三种写法的可读性差距还是很明显的。这种简单的“NULL时给默认值”需求,COALESCE在简洁性上几乎没有对手。
4. 实战踩坑:类型优先级、索引失效与嵌套陷阱
COALESCE用了这么多年,在项目实战中也踩过不少坑。有些问题在本地测试环境根本发现不了,上了生产环境数据量一大就原形毕露。下面这几个坑是我认为最常见的,写出来给后来人提个醒。
4.1 参数类型不一致时的隐式转换规则
COALESCE在参数类型不一致的时候,MySQL会做隐式类型转换。具体规则是:优先转换成参数列表中出现次数最多的类型;如果次数一样,优先转换成第一个参数的类型。这个规则理解起来挺绕,但实际后果是会导致返回的字段类型和你预期的不一样。
举个例子:
SELECT COALESCE(price, '免费') FROM product;如果price字段是DECIMAL类型的,而'免费'是字符串,MySQL会把整体结果转成字符串类型。也就是说,即使price有值,返回给你的也不是数字,而是带小数的字符串。如果你在Java里用BigDecimal去接这个值,可能会收到一个字符串,隐式转换的时候就会出问题。
关键是数值类型和字符串类型混在一起的时候,MySQL的隐式转换策略并不总是一致的。所以我建议:COALESCE的所有参数尽量保持同一类型。如果必须要混合使用,提前用CAST或者CONVERT把类型统一:
SELECT COALESCE(CAST(price AS CHAR), '免费') AS price_display FROM product;这样处理以后,返回值在MySQL内部就是明确的字符串类型,上层应用解析起来不会出岔子。
再举一个更隐蔽的例子:COALESCE和日期时间类型混用的时候,很多人喜欢写COALESCE(date_col, '2024-01-01')。在MySQL 8.0里,如果date_col是DATE类型,'2024-01-01'会被自动转成日期,没毛病。但如果date_col是DATETIME类型,而默认值是'2024-01-01 00:00:00',掉个秒级时间单位倒是还行,就怕你随手写个'2024-01-01'上去,MySQL可能不会报错,但结果是2024-01-01 00:00:00,时间部分被自动补齐了。这种细节在写测试断言的时候很容易被忽略,到时候就真的只能一脸懵。
4.2 COALESCE与索引:什么时候会导致索引失效
这是一个非常重要的性能问题。很多人没意识到,对字段套了一层COALESCE之后,MySQL优化器可能就无法使用该字段上的索引了。
比如你现在有一条SQL:
SELECT * FROM orders WHERE COALESCE(order_status, 0) = 1;如果order_status字段上建了索引,这样的查询条件会导致索引失效,变成全表扫描。原因是:对字段做了函数运算之后,字段的原始值被修改过了,MySQL只能遍历所有行,计算出COALESCE的结果再和1比较。
对应的正确写法应该是:
SELECT * FROM orders WHERE order_status = 1 OR (order_status IS NULL AND 0 = 1); -- 或者更简单的 SELECT * FROM orders WHERE order_status = 1;这里要分情况讨论。如果你的业务逻辑本来就允许order_status为NULL,并且NULL时等价于某个默认状态,你最好不要在查询条件里用COALESCE去修饰已经被索引的字段,而是应该用OR把NULL的情况显式列出来。或者干脆在应用层处理NULL,让SQL保持简单。
还有一个套路是:在表达式上建一个生成列(Generated Column),然后给这个生成列建索引。MySQL 8.0完全支持这个功能,但属于进阶操作,不是每个表都值得这样做。常规的查询场景里,只要记住“WHERE条件里别给索引字段穿衣服”这个原则就够了。
4.3 过度嵌套引发的可读性灾难
COALESCE本身已经很简洁了,但有些人喜欢看网上老掉牙的教程,学了一堆嵌套写法,结果把代码写成这样:
SELECT COALESCE( NULLIF(TRIM(a.remark), ''), CONCAT('默认-', COALESCE(b.code, 'N/A')), '最终兜底' ) AS final_remark FROM table_a a LEFT JOIN table_b b ON a.b_id = b.id;这段SQL功能上是对的,但视觉上已经很难一眼看穿了。这种嵌套超过三层的表达式,建议直接拆成CASE WHEN,逻辑反而更清楚:
SELECT CASE WHEN TRIM(a.remark) IS NOT NULL AND TRIM(a.remark) != '' THEN TRIM(a.remark) WHEN b.code IS NOT NULL THEN CONCAT('默认-', b.code) ELSE '最终兜底' END AS final_remark FROM table_a a LEFT JOIN table_b b ON a.b_id = b.id;很多人追求用COALESCE解决一切,但其实SQL可读性比行数少更重要。代码是写给人看的,顺便给机器跑。我自己写SQL的原则是:COALESCE嵌套层数最多两层,再深就拆成CASE WHEN。这条规矩帮我在后期维护的时候省了可多事。
4.4 LEFT JOIN以后被COALESCE掩盖的数据问题
还有一个比较隐蔽的问题,LEFT JOIN之后驱动表和被驱动表的字段有可能都为NULL。很多人用COALESCE处理的时候,只兜住了被驱动表的字段,忽略了驱动表字段本身的数据异常。
举个例子:
SELECT o.order_id, COALESCE(u.nickname, '匿名用户') AS buyer_name FROM orders o LEFT JOIN users u ON o.buyer_id = u.id;这里u.nickname为NULL有两种可能:一是LEFT JOIN没匹配上,二是匹配上了但users表里这一行的nickname本身就是NULL。这两种情况的业务意义完全不一样。前者说明买家账号可能被删了,后者说明买家没填昵称。你用COALESCE一刀切,数据层面的脏数据问题就被隐藏起来了。
更稳妥的做法是透视出到底是哪种情况:
SELECT o.order_id, CASE WHEN u.id IS NULL THEN '买家账号不存在' WHEN u.nickname IS NULL THEN '匿名用户' ELSE u.nickname END AS buyer_name FROM orders o LEFT JOIN users u ON o.buyer_id = u.id;判断u.id是否为NULL才是确认JOIN是否匹配上的关键,因为主键通常不会为NULL。这种写法在数据分析和报表场景里特别重要,能帮你提前发现数据质量问题。
5. 在复杂SQL和存储过程中的高级用法
COALESCE不仅可以写简单的查询,在一些复杂的SQL场景里,它也是不可或缺的工具。这里分享几个不容易想到,但实际处理起来非常丝滑的用法。
5.1 行转列场景中的缺省填充
数据仓库和报表里经常要做行转列,MySQL里一般用SUM加CASE WHEN来实现。转列之后,原本没有数据的分组会变成NULL。比如统计每个月每个渠道的销售额,某些渠道某个月没有订单,对应的列就是NULL。报表系统直接展示NULL,前端图表就会断掉或者显示空白,很难看。
SELECT channel_id, COALESCE(SUM(CASE WHEN MONTH(order_date) = 1 THEN amount END), 0) AS jan_amount, COALESCE(SUM(CASE WHEN MONTH(order_date) = 2 THEN amount END), 0) AS feb_amount, COALESCE(SUM(CASE WHEN MONTH(order_date) = 3 THEN amount END), 0) AS mar_amount FROM orders WHERE YEAR(order_date) = 2024 GROUP BY channel_id;这里每个CASE WHEN表达式都只满足条件时返回amount,不满足时返回NULL。SUM函数忽略NULL,但前面提到的:如果所有行都是NULL,SUM的结果就是NULL。外面包一层COALESCE把NULL补成0,报表展示就正常了。
5.2 存储过程中的动态条件拼接
COALESCE在存储过程中也有很妙的应用。比如写一个通用的查询存储过程,某个参数可能传NULL表示不过滤条件。用COALESCE可以直接形成“若参数为NULL则忽略该条件”的效果:
CREATE PROCEDURE search_orders( IN p_status INT, IN p_channel VARCHAR(20) ) BEGIN SELECT order_id, order_status, channel, amount FROM orders WHERE order_status = COALESCE(p_status, order_status) AND channel = COALESCE(p_channel, channel) AND created_at >= '2024-01-01'; END这里的技巧是:当p_status传NULL时,COALESCE(p_status, order_status)返回的就是order_status本身,order_status = order_status恒成立,等于没有这个过滤条件。
需要注意的是,这种方式在字段本身允许NULL时有点问题。比如channel列里本身有NULL值,然后你传p_channel = NULL,此时条件变成channel = COALESCE(NULL, channel),如果channel是NULL,那么NULL = NULL的结果也是NULL而不是TRUE,该行就会被过滤掉。所以这种方式比较适合NOT NULL约束的字段,否则建议用动态SQL拼条件,或者用(p_channel IS NULL OR channel = p_channel)这种写法:
WHERE (p_status IS NULL OR order_status = p_status) AND (p_channel IS NULL OR channel = p_channel)这也是很多人踩过坑的地方。COALESCE简化了写法,但并不能完全替代显式的条件判断,适用场景要分清楚。
5.3 视图创建时的防御式设计
在创建视图的时候,提前用COALESCE处理NULL,可以让下游的报表、接口、数据导出的逻辑变得简单很多。比如建设了一个订单明细视图:
CREATE OR REPLACE VIEW v_order_detail AS SELECT o.order_id, o.order_no, COALESCE(u.nickname, '匿名') AS buyer_nickname, COALESCE(u.mobile, '') AS buyer_mobile, COALESCE(o.coupon_amount, 0) AS coupon_amount, COALESCE(o.freight_amount, 0) AS freight_amount, COALESCE(o.pay_amount, 0) AS pay_amount FROM orders o LEFT JOIN users u ON o.buyer_id = u.id;这样设计的好处是:业务方查这个视图的时候,不需要再关心NULL的处理逻辑,直接拿来用就行。NULL处理被统一收敛到视图层,代码里就不会到处散落COALESCE,逻辑也能集中管控。这也符合分层设计的思路。
5.4 存储过程中的UPDATE字段合并
处理动态更新的时候,COALESCE也能帮上忙。比如你在写一个Update操作,需要保留旧值,只在传入新值不为NULL时才更新字段:
UPDATE user_profile SET nickname = COALESCE(p_new_nickname, nickname), signature = COALESCE(p_new_signature, signature), avatar = COALESCE(p_new_avatar, avatar) WHERE user_id = p_user_id;这个用法非常实用,尤其是在做“局部更新”的接口时。应用程序只需要传入需要更新的字段,传NULL的字段保持不变。一条SQL就完成了以前需要先查询再拼接SQL的复杂操作。
不过还是要再提一下那个老生常谈的问题:如果业务上需要“把某字段显式置为NULL”这个操作,就不适合用这种方式。因为传入的NULL会被COALESCE拦截掉,你不会有机会把字段真正改成NULL。这类需求只能用动态SQL。
6. 写SQL时的经验总结与习惯建议
最后分享一些个人在实际项目中养成的习惯和踩过坑之后总结的经验,希望对读到这篇文章的朋友有帮助。
6.1 统一团队规范:把COALESCE定为默认的NULL处理函数
我在上家公司做技术负责人时,明确把“默认使用COALESCE处理NULL”写进了团队开发规范。原因如下:
- COALESCE是标准SQL,将来数据库迁移友好
- 多参数特性天然支持多级兜底,避免IFNULL嵌套
- 团队统一一种写法,代码审查的效率更高
有人会担心COALESCE的可读性,其实只要约定好“不嵌套超过两层,复杂逻辑用CASE WHEN”,可读性问题就不会出现。
6.2 与ORM框架配合时的注意事项
如果你日常开发用的是MyBatis-Plus、Hibernate这类ORM框架,COALESCE同样有很好的支持。MyBatis的XML里可以直接写原生的MySQL函数:
<select id="selectUserWithDefault" resultType="map"> SELECT user_id, COALESCE(nickname, '匿名用户') AS nickname FROM user WHERE user_id = #{userId} </select>Hibernate的JPQL里也可以用COALESCE关键字,Hibernate会自动把它翻译成底层数据库对应的SQL。
需要注意的是,ORM框架分页插件(比如PageHelper)在执行COALESCE查询时,自动生成的count语句可能会把COALESCE表达式也包进去,个别版本会出现SQL语法错误。遇到这种情况,建议手写count语句,或者升级分页插件版本。
6.3 在写Insert语句时也可以用COALESCE防脏数据
向表里插入数据时,也可以在VALUES里使用COALESCE做字段级别的默认值校验。比如一个表有sort字段,要求从1开始,新插入时如果没有传值则自动取当前最大sort加1:
INSERT INTO menu ( menu_name, parent_id, sort ) VALUES ( '用户管理', 0, COALESCE((SELECT MAX(sort) + 1 FROM menu), 1) );这种写法虽然和NULL处理关系不大,但定位逻辑上同样是“有值取原值,无值取兜底”,理念上是相通的。在实际项目中,这种需求非常常见,优先级的字段基本都要这样处理。
6.4 什么时候不要用COALESCE
这个函数也不是万能的,有几类场景我建议谨慎或者不要使用:
- 需要区分“没有关联数据”和“关联数据本身为NULL”的时候,用CASE WHEN判断主键是否为NULL
- 字段需要做精确的类型处理时,比如需要保留DECIMAL的类型精度,就不要让字符串和数值混在一起
- WHERE条件里对索引字段做函数运算时,尽量避免
- 需要显式把字段置为NULL的UPDATE,不要用COALESCE做动态更新
- 参数列表超过四五个时,考虑拆写成CASE WHEN,保证可读性
6.5 一个完整的最佳实践示例
最后放一个综合运用COALESCE的完整案例,把一个相对复杂的查询通过合理使用COALESCE控制得清晰且高效:
SELECT o.order_id, o.order_no, u.nickname AS buyer_name, COALESCE(addr.province, '') AS province, COALESCE(addr.city, '') AS city, -- 订单金额:券后金额为空则取原价,原价为空则取商品金额合计 COALESCE(o.coupon_amount, o.original_amount, o.goods_amount, 0) AS final_amount, -- 支付状态说明 CASE WHEN o.pay_time IS NULL AND o.order_status = 0 THEN '待支付' WHEN o.pay_time IS NULL AND o.order_status != 0 THEN '未支付但锁定' ELSE '已支付' END AS pay_status_desc, -- 商品名称聚合展示 (SELECT GROUP_CONCAT(COALESCE(g.goods_name, '已下架商品') SEPARATOR '、') FROM order_item oi LEFT JOIN goods g ON oi.goods_id = g.id WHERE oi.order_id = o.order_id) AS goods_summary FROM orders o LEFT JOIN users u ON o.buyer_id = u.id LEFT JOIN address addr ON o.address_id = addr.id WHERE o.created_at >= '2024-06-01' AND o.channel = COALESCE(p_channel_param, o.channel)这段SQL里包含了我上面提到的很多关键点:多字段兜底、CASE WHEN处理复杂分支、LEFT JOIN后的NULL区分、GROUP_CONCAT里嵌套COALESCE防止掷错误文案。把这些技巧组合在一起,就是日常开发中很典型的复杂查询模板了。
COALESCE不是那种很少人知道的高级黑科技,而是每个写SQL的人都该熟练掌握的必备工具。但“知道”和“用得好”中间隔着的,正是对它的底层逻辑、类型规则、边界条件的理解。希望这篇内容能帮你把这两个字吃透。