ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SQL数据补零实战:数值、日期与报表的完整解决方案

2026/9/13 12:53:18 拓冰建站 浏览量
SQL数据补零实战:数值、日期与报表的完整解决方案 做数据库开发这些年“SQL实现数据补零”这种需求几乎在每个项目里都会遇上订单号要显示成定长的“01002345”月份要输出成“2024-01-05”而不是“2024-1-5”每月统计报表里没有销售的月份还得硬补出一个零。听着都简单真去写的时候坑不少——有人用字符串拼接拼出半个位数的编号有人把数字转字符串后排序排得乱七八糟还有人为了补零把查询慢了好几倍。这篇文章就把我在SQL里做数据补零的完整方法整理出来覆盖数值左补零、右补零、日期格式化补零、缺失统计补零、空值转零五个大类给出MySQL、SQL Server、Oracle、PostgreSQL主流数据库的直接可用写法并解释每一步为什么这么写。适合正在写报表SQL、做数据清洗或者刚入门数据库开发的读者当工具手册翻也没问题。1. 数据补零到底在解决什么问题1.1 三个最常见的补零场景业务系统里的补零我拆成三个最典型的场景。第一个是编码补位。订单号、会员编号、合同号、批次号业务上通常要给一个固定长度。比如电商订单号要求8位那“123”就要显示成“00100123”。为什么要这么做一方面是打印出来的单据、条码需要对齐另一方面是文件交互接口对字段长度有硬性要求比如银行对账文件里固定12位不足必须补零。第二个是日期时间格式化。数据库里的日期类型本身是二进制存储显示时默认可能有“2024-1-5 8:3:5”这种形态小时甚至不带前导零。业务报表、日志文件、表名后缀都希望统一的“2024-01-05 08:03:05”。这不仅仅是好看很多解析程序按固定格式读月份日期不补零直接解析失败。第三个是统计报表占位。按月统计销售额2月份没有订单直接GROUP BY之后2月这一行会消失。但老板看报表的时候一定期望全年12行都在只是2月金额为0。这种“数据不存在的月份/日期也要出现在结果里”也是补零的常见形态而且是最容易被忽视的一种。这三个场景对应了三种完全不同的实现手法字符串函数、日期格式化、维度表关联。后面会一一展开。1.2 补零的本质让“字符串排序”等于“数值排序”其实把“补零”这件事往深了想它背后有一个很底层的逻辑。数据库里的数字1、2、10如果直接按字符串排序结果是“1、10、2”这个顺序对很多业务人员来说是反直觉的。但如果你把它们先补成同样长度的“0001、0002、0010”再按字符串排序结果就和数值排序完全一致了。补零的本质是把一个“语义上是数字、但展示上要求是文本”的字段变成定长文本让它在传输、排序、比对时行为稳定。这就是为什么补零不是随便加几个0就行而是必须形成“定长”的结果。长度只要不一致字符串排序的秩序就会崩掉。理解这一点后面看LPAD的截断规则、ORDER BY的坑就都能想通了。1.3 动手之前先问自己三个问题见到补零需求我建议先别急着敲SQL先把自己问清楚补几位业务上对长度有没有明确要求。条码位数、接口字段规格、系统编号规则这些通常会直接给出答案。补左边还是右边编号类几乎都是左补零金额小数类通常是右补零或者按精度格式化。方向错了数据就完全变了。是查询时格式化显示还是写入时把定长值落库前者对表结构没要求适合报表展示后者要求字段本身定义成CHAR/VARCHAR定长适合频繁使用定长值的核心业务。这三个问题决定了你是用LPAD、DATE_FORMAT还是要在建表时就把字段定义成CHAR(8)。我见过太多在查询里反复格式化结果被慢查询逼得回头改造表结构的项目提前想清楚能省很多事。2. 数值与字符串补零LPAD、RPAD与FORMAT的取舍2.1 左补零首选LPAD但要小心截断LPAD是SQL里最常见的左补零函数MySQL、Oracle、PostgreSQL都有基本语法是LPAD(原字符串, 目标长度, 补位字符)。直接把字符串的左侧填上指定字符填到目标长度为止。来一个具体例子。建一张订单临时表CREATE TABLE tmp_order ( id INT PRIMARY KEY ); INSERT INTO tmp_order VALUES (1), (12), (123), (12345), (12345678);把id补成6位SELECT id, LPAD(id, 6, 0) AS padded_id FROM tmp_order;结果如下idpadded_id1000001120000121230001231234501234512345678123456注意最后一行原id是8位目标长度只有6位LPAD不会报错而是直接从右侧截断只保留最左边的6个字符变成“123456”。数据在看结果的那一刻已经悄悄丢了两位。出现这种情况的根源是补零时没有先确认原字段的最大长度。我给的建议是补零前先看一遍业务侧的最大值比如执行SELECT MAX(LENGTH(id))确认最长的编号是多少然后在此基础上预留几位余量免得以后编号位数涨上去报表数据莫名被切短。Oracle的LPAD行为类似参数类型上要求第一个参数是字符串如果字段是数字最好显式转一下LPAD(CAST(id AS VARCHAR(20)), 6, 0)避免数据库隐式转换带来的不确定行为。2.2 右补零RPAD报文定长和金额场景更常见右补零用得比左补零少但真实业务里也有典型场景是接口报文的定长处理。比如物流面单的某个扩展字段要求固定10字节内容是“ABC”右补零后就是“ABC0000000”。语法上就是把LPAD换成RPADSELECT RPAD(ABC, 6, 0); -- MySQL / Oracle / PostgreSQL: ABC000RPAD同样有截断问题原字符串长于目标长度时保留左侧目标长度个字符右侧被截掉。所以报文字段如果内容超长补零函数不会提示只会安静地切数据。另一个容易混淆的场景是金额小数。商品价格12.1要显示成12.10这个方法不是RPAD字符串来解决的小数会由 FORMAT 或者 CAST AS DECIMAL(10,2) 处理。如果非要用字符串补零会得到12.100这种错误结果因为它是把文本末位补0而不是按数值精度补位。这里的正确姿势是用格式化函数具体见2.4。2.3 没有LPAD的数据库SQL Server和SQLite的平替方案SQL Server 和 SQLite 没有 LPAD这是很多从MySQL切换过来的人第一个不适应的地方。切换方案是有的。SQL Server在2012版本以后可以用FORMAT函数直接套用格式字符串非常方便SELECT FORMAT(123, D6); -- 结果: 000123如果还在用SQL Server 2008R2这种老版本没有FORMAT那就只能拼字符串SELECT RIGHT(REPLICATE(0, 6) CAST(id AS VARCHAR(20)), 6);REPLICATE(0, 6)先生成6个0拼接id后再用RIGHT取右边6位效果和LPAD一样。这里有个隐藏前提id本身不能超过6位否则RIGHT取右6位会把前几位丢掉。所以使用这种写法前同样要确认最大长度。SQLite的写法更像拼接艺术SELECT SUBSTR(000000 || id, -6); -- 结果是000123把6个0拼在前面再用SUBSTR取倒数6位。另一种是SQLite的PRINTF函数很顺手SELECT PRINTF(%.6d, 123); -- 结果: 000123各数据库补零函数对照整理成一张表数据库左补零写法说明MySQLLPAD(12, 6, 0)原生支持Oracle / PostgreSQLLPAD(12, 6, 0)Oracle记得先转字符串SQL Server 2012FORMAT(12, D6)简单但数据量大慎用SQL Server 2008R2RIGHT(REPLICATE(0,6) 12, 6)兼容老版本SQLiteSUBSTR(0000002.4 负数和小数补零别硬套字符串函数负数补零是个容易翻车的点。LPAD(-12, 6, 0)的结果在很多实现里会得到“000-12”这种把负号和数字混在一起、位数也对不上的结果。如果确实需要给负数做定长展示建议先取绝对值补零再拼接负号例如SQL Server里可以写SELECT CASE WHEN val 0 THEN - RIGHT(REPLICATE(0, 5) CAST(ABS(val) AS VARCHAR(20)), 5) ELSE RIGHT(REPLICATE(0, 5) CAST(val AS VARCHAR(20)), 5) END;不过大多数真实场景里编号类字段不会出现负数这种写法更多是用来处理温度、库存差异这类可正可负的数值字段。小数补零则应该直接交给数值格式化MySQL里FORMAT(12.1, 2)返回“12.10”注意MySQL的FORMAT会带千分位逗号不需要逗号时可以用CAST或配合REPLACEOracle用TO_CHAR(12.1, FM9990.00)SQL Server用FORMAT(12.1, 0.00)或CAST(12.1 AS DECIMAL(10,2))。核心逻辑是让数据库按数值精度补位不要自己拼字符串。3. 日期时间补零格式化函数一劳永逸3.1 日期格式化一张表看穿主流数据库日期时间补零的核心是用格式化函数而不是手工拼字符串。主流数据库的映射关系需求MySQLSQL ServerOracle / PostgreSQLSQLite格式化日期时间DATE_FORMAT(d, %Y-%m-%d %H:%i:%s)FORMAT(d, yyyy-MM-dd HH:mm:ss)TO_CHAR(d, YYYY-MM-DD HH24:MI:SS)strftime(%Y-%m-%d %H:%M:%S, d)仅日期DATE_FORMAT(d, %Y-%m-%d)CONVERT(VARCHAR(10), d, 120)TO_CHAR(d, YYYY-MM-DD)strftime(%Y-%m-%d, d)年月字符串DATE_FORMAT(d, %Y%m)FORMAT(d, yyyyMM)TO_CHAR(d, YYYYMM)strftime(%Y%m, d)把2024年1月5日早上8点3分5秒统一成“2024-01-05 08:03:05”MySQL写作DATE_FORMAT(create_time, %Y-%m-%d %H:%i:%s)。这里我见过太多人栽在格式符上分钟是%i不是%m%m是月份。%Y是大写四位年份%y是小写两位年份写错一个符号结果就完全不对。SQL Server的FORMAT函数格式串大小写敏感yyyy是年MM是月dd是日HH是24小时制hh是12小时制mm是分钟ss是秒。写成大写MM和小写mm含义完全不同。Oracle的TO_CHAR里分钟是MI月份是MM这个和MySQL的%i/%m又不一样跨数据库写代码时最容易记混。3.2 别用CONCAT手工拼日期除非你真的清楚规则我看到过不少开发者在报表SQL里这么写CONCAT(YEAR(d), -, MONTH(d), -, DAY(d))结果就是2024年1月5日变成“2024-1-5”月份日期全不补零。要修复的话得写成CONCAT(YEAR(d), -, LPAD(MONTH(d), 2, 0), -, LPAD(DAY(d), 2, 0))这段能跑但远不如DATE_FORMAT(d, %Y-%m-%d)一行简洁。手工写法的真正问题不在长度而在于每次都要维护一组拼接逻辑日期格式在SQL里出现七八次改起来就痛苦了。如果必须做手工拼接比如某些老库不支持DATE_FORMAT我建议把“取位数、补零”的逻辑写成一个SQL函数或者公共表达式例如把“月份补零”抽象成LPAD(MONTH(d), 2, 0)然后各处复用而不是每次重写一遍。3.3 实战案例账期后缀和日志文件名日期补零最实用的一个场景是生成账期、分区目录名这类定长字符串。比如要在报表SQL里生成“202401”这种账期MySQL可以写SELECT DATE_FORMAT(sale_date, %Y%m) AS period FROM sales_record;SQL Server这么写SELECT FORMAT(sale_date, yyyyMM) AS period FROM sales_record;这种定长的“YYYYMM”字符串有个好处排序和比较的时候字典序就等于时间顺序。“202402”一定排在“202410”前面不需要再转成数字或者日期。这就是补零带来的秩序。日志按小时分片的场景也类似小时必须补成两位否则文件名会变成“log-20240105-8.txt”和“log-20240105-10.txt”这种顺序混乱的情况。统一的补零格式能保证按字典序排列就是按时间排列。4. 统计报表补零把不存在的月份“画”出来4.1 经典问题直接GROUP BY没有数据的月份直接消失这是报表SQL里最常见的“隐形补零”需求。先建一张销售记录表CREATE TABLE sales_record ( id INT PRIMARY KEY, sale_date DATE, amount DECIMAL(10, 2) ); INSERT INTO sales_record VALUES (1, 2024-01-15, 600.00), (2, 2024-01-28, 300.00), (3, 2024-03-02, 1000.00), (4, 2024-03-20, 500.00);按月汇总SELECT DATE_FORMAT(sale_date, %Y-%m) AS month, SUM(amount) AS total_amount FROM sales_record WHERE sale_date BETWEEN 2024-01-01 AND 2024-12-31 GROUP BY DATE_FORMAT(sale_date, %Y-%m) ORDER BY month;结果只有两行monthtotal_amount2024-01900.002024-031500.002月、4月到12月一整个区块都没了。老板拿到这张表肯定要问2月份是没数据还是报表漏了只有把2月补成total_amount0报表才完整。GROUP BY的底层逻辑决定了它只能从“已经存在的数据”里分组数据源里没有2月的行结果里自然不会有2月。所以缺失月份的补零本质上是在查询前构造一张“完整的月份表”。4.2 递归CTE生成月份序列再用LEFT JOIN兜底MySQL 8.0及以上支持WITH RECURSIVE生成连续月份WITH RECURSIVE month_seq AS ( SELECT 2024-01-01 AS month_start UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM month_seq WHERE month_start 2024-12-01 ) SELECT m.month_start, COALESCE(SUM(s.amount), 0) AS total_amount FROM month_seq m LEFT JOIN sales_record s ON DATE_FORMAT(s.sale_date, %Y-%m) DATE_FORMAT(m.month_start, %Y-%m) GROUP BY m.month_start ORDER BY m.month_start;这段SQL的逻辑分三层第一递归CTE month_seq从2024-01-01开始每次加一个月直到2024-12-01正好生成12行。递归一定要有终止条件也就是WHERE month_start 2024-12-01否则SQL会无限循环下去。第二用month_seq作为LEFT JOIN的左表右表是销售明细。这个方向很关键维度表必须在左边聚合表在右边这样维度表的每一行都会保留下来。第三COALESCE(SUM(s.amount), 0)是补零的收尾动作。因为LEFT JOIN后2月没有匹配记录SUM(s.amount)的值是NULL而不是0需要把它转成0。结果就是全年12行每个月都有数据2月显示0。这一步才算真正完成了“数据补零”。这里我多说一句性能细节上面这个写法用DATE_FORMAT做关联条件数据量小没问题数据量大时每行都要格式化索引也帮不上忙。更稳妥的关联方式是范围匹配LEFT JOIN sales_record s ON s.sale_date m.month_start AND s.sale_date DATE_ADD(m.month_start, INTERVAL 1 MONTH)范围匹配可以走sale_date上的索引报表数据量大时差距很明显。4.3 没有递归CTE的数据库怎么生成月份SQL Server的递归CTE不需要RECURSIVE关键字直接写成WITH month_seq AS ( SELECT DATEFROMPARTS(2024, 1, 1) AS month_start UNION ALL SELECT DATEADD(MONTH, 1, month_start) FROM month_seq WHERE month_start DATEFROMPARTS(2024, 12, 1) ) SELECT m.month_start, ISNULL(SUM(s.amount), 0) AS total_amount FROM month_seq m LEFT JOIN sales_record s ON YEAR(s.sale_date) YEAR(m.month_start) AND MONTH(s.sale_date) MONTH(m.month_start) GROUP BY m.month_start ORDER BY m.month_start;PostgreSQL有一种更简洁的玩法直接用generate_series生成时间序列WITH month_seq AS ( SELECT generate_series(2024-01-01::date, 2024-12-01::date, interval 1 month) AS month_start ) SELECT m.month_start, COALESCE(SUM(s.amount), 0) AS total_amount FROM month_seq m LEFT JOIN sales_record s ON date_trunc(month, s.sale_date) date_trunc(month, m.month_start) GROUP BY m.month_start ORDER BY m.month_start;Oracle没有RECURSIVE但可以用CONNECT BYSELECT ADD_MONTHS(DATE 2024-01-01, LEVEL - 1) AS month_start FROM dual CONNECT BY LEVEL 12;思路完全一致先生成完整的月份名单再和业务数据关联。4.4 多维度补零门店乘以月份的笛卡尔积只看“月份”一个维度还不够实际报表经常要“每个门店、每个月”都有一条记录。这时候单靠一个月份CTE不够需要先做门店表和月份序列的笛卡尔积再LEFT JOIN销售数据。WITH RECURSIVE month_seq AS ( SELECT 2024-01-01 AS month_start UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM month_seq WHERE month_start 2024-12-01 ) SELECT st.store_name, DATE_FORMAT(m.month_start, %Y-%m) AS month, COALESCE(SUM(s.amount), 0) AS total_amount FROM store st CROSS JOIN month_seq m LEFT JOIN sales_record s ON s.store_id st.id AND s.sale_date m.month_start AND s.sale_date DATE_ADD(m.month_start, INTERVAL 1 MONTH) GROUP BY st.store_name, m.month_start ORDER BY st.store_name, m.month_start;CROSS JOIN把门店和月份组合成了一张完整维度表哪家店哪个月没销售都会以0出现。这里的GROUP BY只能按维度表字段不能按sales_record的store_id或月份来分组否则NULL行会被过滤掉或者归错组。这个细节我调试时栽过跟头特此强调。4.5 窗口函数和补零配合移动累计、同比环比的正确姿势窗口函数和补零确实是绝配。最常见的场景是累计金额。如果直接用缺失2月的原始聚合结果算SUM() OVER(ORDER BY month)得到的是1月900、3月24002月完全没露面。报表看板上累计曲线在2月那里直接断掉视觉上就像2月被吞了。正确的做法是先补零得到连续月份之后再在补零结果上套窗口函数WITH month_data AS ( SELECT m.month_start, COALESCE(SUM(s.amount), 0) AS total_amount FROM month_seq m LEFT JOIN sales_record s ON s.sale_date m.month_start AND s.sale_date DATE_ADD(m.month_start, INTERVAL 1 MONTH) GROUP BY m.month_start ) SELECT DATE_FORMAT(month_start, %Y-%m) AS month, total_amount, SUM(total_amount) OVER (ORDER BY month_start) AS cumulative_amount FROM month_data ORDER BY month_start;同比环比的计算也是同样逻辑先把缺失周期补零再用LAG或者LEAD取上一期数值。所有依赖“相邻行”的分析前提都是行本身不能缺。这个顺序一定不能反先补零再开窗不然结果全是错的。5. 空值转零COALESCE、IFNULL与CASE WHEN的选用5.1 NULL不是零但报表里经常要“当零看”“空值转零”严格来说和补零不是同一个动作但凡是做数据补零几乎都逃不掉NULL处理。LEFT JOIN之后右表没有匹配行关联字段就是NULLSUM聚合一个空集合结果也是NULL不是0。我用一句话概括两者的区别NULL表示“没有值”0是一个实实在在的值。报表里“没有销售”和“销售了0元”语义都不完全相同但落到数字上老板要看到0不能看到一个空格或者NULL。所以补零的常规操作是两步走先用维度表把缺失行查出来再用COALESCE把NULL金额变成0。缺一不可。5.2 各数据库空值转零函数标准写法与方言写法COALESCE是SQL标准里最通用、最推荐的空值转换函数多个数据库通用SELECT COALESCE(amount, 0) AS amount FROM sales_record;COALESCE支持多个参数COALESCE(field1, field2, 0)从前往后找第一个非NULL值。这个特性在多层兜底时很有用。各数据库还有自己的方言函数数据库空值转零函数说明MySQLIFNULL(expr, 0) 或 COALESCE(expr, 0)IFNULL只支持两个参数OracleNVL(expr, 0) 或 COALESCE(expr, 0)NVL只支持两个参数SQL ServerISNULL(expr, 0) 或 COALESCE(expr, 0)ISNULL只支持两个参数PostgreSQLCOALESCE(expr, 0)没有独立方言函数CASE WHEN也可以实现CASE WHEN amount IS NULL THEN 0 ELSE amount END写法啰嗦但语义最清晰适合在逻辑分支多的时候用。大多数情况下我直接用COALESCE因为它兼容性最好、可读性也强。5.3 聚合函数对NULL的隐藏行为容易一脚踩偏COUNT、AVG、SUM这三个聚合函数对NULL的处理规则不同做数据清洗时特别容易踩坑。COUNT()统计的是行数而COUNT(amount)统计的是amount不为NULL的行数。如果一张表有100行其中20行amount为NULLCOUNT()是100COUNT(amount)是80。想在报表里显示“有金额记录的行数”用COUNT(amount)想显示“参与统计的总行数”用COUNT(*)。AVG(amount)计算平均值时自动忽略NULL行。假如5行数据里3行是1002行是NULLAVG(amount)返回100而不是60。如果业务上“没有金额”要按0参与平均必须先转零再求平均AVG(COALESCE(amount, 0))结果才是60。SUM(amount)如果作用在全是NULL的集合上返回NULL不是0。所以前面统计报表里COALESCE(SUM(amount), 0)这层包裹是不能省的。6. 常见问题与实战排查6.1 补零后排序反而乱了字符串排序和数值排序的区别补零本身是为了排序更稳定但前提是长度必须统一。假如订单号字段里既有“0010”这种补好零的又有“100”这种没补零的混合在一起按字符串排序“0010”会排在“100”前面因为你实际上在比字符而不是比数值。解决办法有两个要么把所有编号都统一补到同一长度要么在做排序的时候把字段转回数值再排MySQL写法SELECT id, LPAD(id, 8, 0) AS order_no FROM tmp_order ORDER BY CAST(id AS UNSIGNED);SQL Server用CAST(id AS INT)Oracle用TO_NUMBER(id)PostgreSQL用id::int。定长补零是治本转数值排序是治标生产环境里两个方法都值得掌握。6.2 大表上做补零格式化当心索引失效对已建索引的字段做函数运算会让查询计划放弃索引。比如WHERE LPAD(id, 6, 0) 001001这种写法id列虽然建了索引但LPAD之后索引就没用了全表扫描来得顺理成章。我自己在几百万行的流水表上踩过响应时间从毫秒级直接飙到秒级。这一类问题的根源在于补零本质上是“展示层的格式化”不太适合当查询条件。如果业务确实需要频繁按定长编号查询最稳妥的方案是在写入的时候就把定长编号落到表里字段定义CHAR(8)查询时直接等值匹配。我印象很深刻的案例是订单表高频查询字段干脆用CHAR定长序列生成时就在应用层补好零SQL里再也不需要LPAD。6.3 数据导入时前导零被吞根源往往在建模Excel或CSV导入的时候编号“00123”很容易被识别成数值123前导零静默丢失。导进数据库后如果目标字段还是INT类型那这个信息就彻底回不来了。对策分两层第一层导入前在Excel里把编码列设成文本或者CSV导出时给字段加前导引号防止自动转数值。第二层建模时就别把编号设计成数字类型。订单号、会员号、批次号这类字段如果不是用来做算术运算一律用VARCHAR或CHAR。很多看似是“数字”的标识符本质上是“由数字组成的编号”它们需要的不是INT而是定长字符串——这在建模源头就把补零问题解决了。6.4 补零场景速查表最后把整个文章涉及的写法浓缩成一张速查表方便实际工作里照着查场景MySQLSQL ServerOracle / PostgreSQLSQLite数值左补零LPAD(id, 6, 0)FORMAT(id, D6) 或 RIGHT(REPLICATE(0,6)CAST(id AS VARCHAR),6)LPAD(CAST(id AS VARCHAR), 6, 0)SUBSTR(000000数值右补零RPAD(id, 6, 0)LEFT(CAST(id AS VARCHAR)REPLICATE(0,6),6)RPAD(CAST(id AS VARCHAR), 6, 0)SUBSTR(id日期格式化DATE_FORMAT(d, %Y-%m-%d)FORMAT(d, yyyy-MM-dd) / CONVERTTO_CHAR(d, YYYY-MM-DD)strftime(%Y-%m-%d, d)缺失月份补零递归CTE LEFT JOIN递归CTE LEFT JOINCONNECT BY / 递归CTE LEFT JOIN递归CTE LEFT JOIN空值转零COALESCE(x, 0)ISNULL(x, 0)NVL(x, 0) / COALESCE(x, 0)COALESCE(x, 0)收藏这张表大多数补零需求基本一查就能写出来。最后说一个我自己的习惯补零的代码我从来不会埋在报表SQL的各个角落里而是要么在最初建模时就把编码字段定义成定长CHAR要么在最后展示层统一格式化。原因很简单——补零本来就是一个“展示约定”不是业务规则把它放在离数据源头越近的地方后面维护的人就越省事。你如果也遇到过那种“订单号看着是定长的、放进Excel就变成123”的情况大概就能理解我为什么这么说了。