
我用 CONCAT 拼员工姓名库里只漏填了一个姓氏整列结果全变成 NULL。这篇我用一条报表处理链把字符串、日期两类高频函数的坑点和标准写法一次讲清代码全部可直接复制运行。一、拼接一个完整姓名结果为什么全是 NULL报表要把员工的姓和名拼成一列展示我写了一条最朴素的拼接语句SELECT CONCAT(first_name, , last_name) AS full_name FROM employees;注意单引号里的那个空格它负责分隔姓和名去掉它「张」和「三」会紧贴成「张三」这是拼接时最容易漏掉的细节。跑完结果对不上第三个员工只是没填姓氏整列直接变成 NULLfirst_namelast_namefull_name张三张 三李四李 四王NULLNULL问题来了明明只有姓氏缺失为什么连名字「王」也一起消失了NULL 表示「未知值」不是空字符串。任何值与 NULL 运算结果都为 NULL这是 SQL 的三值逻辑Three-Valued Logic。CONCAT 逐个拼接参数一旦碰到 NULL整个结果就坍塌。带分隔符拼接函数 CONCAT_WSConcatenate With Separator正好解决这个问题第一个参数是分隔符它会自动跳过值为 NULL 的参数SELECT CONCAT_WS( , first_name, last_name) AS full_name FROM employees;----------- | full_name | ----------- | 张 三 | | 李 四 | | 王 | -----------第三个员工保留了 「王」整列不再出现 NULL。你也可以用 IFNULL 把每个可空字段单独包成空串但字段一多包起来比 CONCAT_WS 啰嗦得多。记住NULL 不认亲戚只要它在场CONCAT 的结果就是 NULL。二、姓名拼对了字段清洗为什么还在出错拼接只是第一步。真实报表里工号要补零、手机号要截取、字段首尾还带着空格。高频字符串函数我整理成一张清单你可以直接当速查表用函数作用易错点CONCAT_WS(sep, s1, …)带分隔符拼接自动跳过 NULLSUBSTRING(str, start, len)截取子串索引从 1 开始REPLACE(str, a, b)字符串替换替换所有匹配位置TRIM(str)去首尾空白只去首尾不去中间LPAD / RPAD(str, len, pad)左 / 右填充长度不足才补UPPER / LOWER(str)大小写转换常用于统一格式LENGTH / CHAR_LENGTH(str)求长度字节数 / 字符数我最容易记错的是 SUBSTRING 的起始位置。刚从 Java 切过来时顺手写成 0结果把第一个字符丢了SELECT SUBSTRING(ABC123, 1, 3); -- ABC SELECT SUBSTRING(ABC123, 0, 3); -- 空串SQL 位置从 1 开始SQL 字符串位置从 1 开始计数省略第三个参数时截取到字符串末尾起始位置也支持负数从末尾往前数。REPLACE 会把所有匹配位置都替换掉而不是只替换第一个只想改特定位置的字符得换其他思路。TRIM 还支持TRIM(x FROM str)的写法可以去掉首尾指定字符不限于空格。SELECT REPLACE(a-b-a-b, a, x); -- x-b-x-b全部替换 SELECT TRIM(- FROM --hello--); -- hello去掉首尾横线长度函数要分清口径LENGTH 返回字节数CHAR_LENGTH 返回字符数。中文是多字节字符两者能差好几倍统计「多少个字」时用错结果直接翻倍。工号补零是填充函数最典型的场景把不足 5 位的工号统一补齐UPDATE emp SET workno LPAD(workno, 5, 0); -- 1 号员工1 变为 00001记住SQL 的字符串从 1 开始数索引错一位整列数据都错。三、文本干净了日期格式化为什么对不上字段清洗完成报表还要求把2024-04-06显示成「2024 年 04 月 06 日」。日期格式化函数 DATE_FORMAT 接收一个日期和一个格式串按格式符输出文本SELECT DATE_FORMAT(2024-04-06 22:23:01, %Y年%m月%d日); -- 2024年04月06日格式符Format Specifier以百分号%开头大小写是两套完全不同的语义。我把容易混淆的几对整理如下格式符含义格式符含义%Y四位年份 2024%y两位年份 24%m月份数字 04%M月份名 April%d日期数字 06%D带后缀 6th%H24 小时制 22%h12 小时制 10%i分钟 23%s秒 01需要紧凑时间戳时把年月日时分秒连起来格式串写%Y%m%d%H%i%s一条函数输出20240406222301常用于生成编号前缀或落表分区。取当前时间也要看粒度NOW () 返回日期加时间CURDATE () 只返回日期。做日报统计用 CURDATE () 就够带上时分秒反而会干扰匹配。记住格式符大小写不是风格问题% m 是月份% M 是月份名。四、反过来想字符串怎么变回日期去比较外部导入的数据往往是06/04/2024这样的文本没法直接和日期字段比较。字符串转日期函数 STR_TO_DATE 是 DATE_FORMAT 的逆操作按格式串把文本解析成日期SELECT STR_TO_DATE(06/04/2024, %m/%d/%Y); -- 2024-04-06它有一个隐蔽的静默失败Silent Failure文本和格式串对不上时返回 NULL 并产生警告而不是直接报错。我在一次数据导入中就被它悄悄塞了一整列空值。拿到文本后的处理分支可以这样判断外部文本 │ ▼ STR_TO_DATE 按格式解析 │ ├─ 解析成功 ──► 合法日期 ──► DATE_ADD / DATEDIFF 运算 │ └─ 解析失败 ──► NULL 警告不报错──► 先查空值拿到合法日期后常用的日期运算有三个函数作用注意DATE_ADD(d, INTERVAL n unit)日期加unit 取 DAY / MONTH / YEARDATE_SUB(d, INTERVAL n unit)日期减同上DATEDIFF(d1, d2)相差天数只算日期忽略时间SELECT DATE_ADD(2025-12-17, INTERVAL 7 DAY); -- 2025-12-24 SELECT DATEDIFF(2025-12-17, 2025-12-01); -- 16加减的单位 INTERVAL 不止 DAY按月、按年写成 MONTH、YEAR 即可算到期日、续费日都用它。传入带时分秒的 DATETIME 时DATEDIFF 只比较日期部分要精确到小时、分钟改用 TIMESTAMPDIFF。按年月分组统计时提取函数最顺手。YEAR ()、MONTH ()、DAY () 是 EXTRACT 的简写EXTRACT 能提取的部分更全SELECT YEAR(2019-07-02); -- 2019 SELECT EXTRACT(YEAR_MONTH FROM 2019-07-02 01:02:03); -- 201907记住STR_TO_DATE 解析失败只给 NULL 不报错导入后必须先查空值。五、串起来一条 SQL 生成标准订单号单个函数都不复杂真实业务里它们是组合出现的。订单号要求「前缀 当天日期 5 位流水号」一条 SQL 就能拼出SELECT CONCAT(ORD-, DATE_FORMAT(NOW(), %Y%m%d), -, LPAD(order_id, 5, 0)) AS order_no FROM orders;-------------------- | order_no | -------------------- | ORD-20261001-00007 | --------------------再比如把用户注册信息拼成一句话SELECT CONCAT(username, 注册于 , DATE_FORMAT(created_at, %Y年%m月%d日), 距今 , DATEDIFF(NOW(), created_at), 天) AS info FROM users;这一条链路里拼接、格式化、填充、日期差值全部串了起来。你只要替换前缀、格式串和流水号位数就能改成自己业务的编号规则。六、总结整条链路收束成三句话拼接遇 NULL 用 CONCAT_WS字符串位置从 1 开始数日期展示用 DATE_FORMAT文本转日期用 STR_TO_DATE运算用 DATE_ADD / DATEDIFF函数按功能分组记忆比逐个背诵效率更高术语速查表术语说明三值逻辑Three-Valued Logic谓词结果为真、假、未知三种NULL 参与比较结果为未知NULL未知值区别于空字符串和 0格式符Format SpecifierDATE_FORMAT 中以%开头的占位符号静默失败Silent Failure不抛错误以 NULL 或警告体现的失败方式DATETIME同时包含日期和时间的类型参考链接MySQL 8.0 字符串函数与运算符https://dev.mysql.com/doc/refman/8.0/en/string-functions.htmlMySQL 8.0 日期和时间函数https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.htmlMySQL 8.0 Working with NULL Valueshttps://dev.mysql.com/doc/refman/8.0/en/working-with-null.html