先说个频率高得离谱的场景:一段数据对接脚本跑得好好的,某天突然报“将 varchar 值转换成 int 列时发生语法错误”,或者报表上的日期全变成了“May 1 2024 12:00AM”这种谁看谁懵的样子,改来改去最后发现就是 CONVERT 转换函数里少写了个参数、多写了个长度。这东西在 SQL 脚本里出现的次数仅次于 SELECT,但真正把它吃透的人不多。大部分人写它的时候脑子里只有两个动作:类型不匹配了,外面套一层;日期要输出成特定样子,后面加个数字。至于那个第三个参数到底有多少种取值、小数转整型是截断还是进位、套完函数之后索引还在不在,基本靠试。
这篇就把 CONVERT 转换函数从头到尾捋一遍。从三个参数的骨架讲起,把日期样式表和数值精度的坑逐个摊开,再聊隐式转换怎么悄悄把索引废掉,最后给一份我自己攒下来的报错速查表。适合刚接触 SQL、天天跟数据导入导出打交道的人,也适合写了几年脚本但一直靠复制粘贴过日子的老手——大概率你能在里面找到一两个自己一直在犯的小毛病。
1. CONVERT 到底在解决什么问题
1.1 三个参数构成的转换骨架
CONVERT 的完整写法是CONVERT(目标数据类型[(长度)], 表达式, [style])。三个位置,前两个必填,第三个可选。看起来简单,但它和大多数函数不一样的地方在于:括号里的“目标数据类型”不是一个值,而是一个类型声明,这在所有编程语言里都属于比较特殊的存在。你写CONVERT(int, '123'),这里的 int 不是变量也不是常量,而是告诉数据库“请把后面那个东西按 int 的规则重新解释一遍”。
第一个参数里还能带长度,比如varchar(10)、nvarchar(50)、decimal(18,2)。这个长度不是装饰品,它直接决定了转换结果会不会被砍掉一截。很多人图省事写成CONVERT(varchar, GETDATE(), 120),能跑,结果也对,因为不指定长度时 varchar 默认是 30,而yyyy-mm-dd hh:mi:ss正好 19 个字符,塞得下。但你要是拿它转一个 50 个字符的备注字段,出来就只剩 30 个字符,而且不报错,就这么静悄悄地少了一半内容。这种 bug 最难查,因为脚本不报错,数据看着也像那么回事。
第二个参数是被转换的表达式,可以是列、常量、变量,也可以是另一个函数的返回值。这里有个执行顺序的问题值得说清楚:数据库会先把第二个参数算出来,再按第一个参数的类型去做转换。所以CONVERT(int, '12' + '3')是先把两个字符串拼成 '123',再转成 123,结果是 123 而不是 15。想先做数值加法,得写成CONVERT(int, '12') + CONVERT(int, '3')。这个细节在拼接动态 SQL 或者处理外部数据时特别容易翻车。
第三个参数 style 是 CONVERT 区别于 CAST 的核心。它只在特定类型组合下生效,主要是“日期时间转字符串”和“数值转字符串”这两类。其他组合下你写了也是白写,数据库不会报错,但也不会理你。这个“静默忽略”的特性挺坑人的,后面会专门讲。
1.2 和 CAST 的取舍:什么时候非 CONVERT 不可
既然有 CAST,为什么还要用 CONVERT?答案就一个字:样式。CAST 的写法是CAST(表达式 AS 目标类型),语法干净,符合标准,几乎所有关系型数据库都支持。但它没有第三个参数,日期转字符串时只能用数据库的默认格式。
默认格式长什么样?在常见的 SQL Server 环境里,CAST(GETDATE() AS varchar(30))出来是May 1 2024 12:00AM这种。月份是英文缩写,日期和月份之间还有两个空格(因为日期是两位数时占两位,一位数时补空格),上午下午用 AM/PM 表示。这种格式拿去给前端展示,产品经理能追着你问三天。更麻烦的是它受语言设置影响,换个语言环境,月份缩写就变成别的语言,接口直接解析不了。
所以我的习惯很明确:只要涉及日期时间转字符串,一律用 CONVERT 并显式指定 style;纯类型转换、不涉及格式化的场景,用 CAST,因为语义更清楚,代码也短。这个分工用了几年,基本没出过问题。
还有一类场景必须用 CONVERT:把字符串解析成日期。虽然 CAST 也能做,CAST('2024-05-01' AS date)完全合法,但当你需要明确告诉数据库“这个字符串是 日/月/年 顺序”时,只有 CONVERT 的 style 能给这个承诺。比如CONVERT(date, '01/05/2024', 103)明确按 dd/mm/yyyy 解析,得到 5 月 1 日;不写 style 的话,同一个字符串在 mdy 环境下会被解析成 1 月 5 日。这种歧义在跨系统对接时是重灾区,后面会展开。
顺带提一句:SQL Server 2012 之后还有个 FORMAT 函数,能写出FORMAT(GETDATE(), 'yyyy-MM-dd')这种非常直观的格式串,看着比 style 数字舒服一百倍。但它的实现方式是调用 .NET 的格式化引擎,逐行调用,几十万行的结果集上性能差距能到几十倍。我实测过一个百万行的导出,用 CONVERT 大概几秒,换成 FORMAT 要跑一分多钟。所以除非是单行配置表或者报表标题这种不在乎性能的地方,我基本不用 FORMAT。
2. 参数逐个抠:九成的坑都藏在这里
2.1 目标类型和长度:别让默认值替你决定
前面提过不写长度时 varchar 默认 30,这个默认值在日期上刚好够用,所以很多人一直没意识到它的存在,直到某天处理长文本才发现被截断。我把几个常见类型的默认行为整理一下:
varchar/nvarchar/char/nchar不写长度时默认 30decimal/numeric不写精度时默认 (18,0),也就是没有小数位datetime2不写精度时默认 7 位小数秒varbinary不写长度时默认 30
第二条是重灾区。CONVERT(decimal, 12.34)会得到 12,不是 12.34。因为默认精度是 18 位、小数位 0 位,转换时直接四舍五入到整数。更隐蔽的是,这个结果再参与后续计算,误差会被一路放大。我见过一个算费率的脚本,中间步骤用了CONVERT(decimal, ...)没写小数位,最后对账差了几万块,查了两天才定位到这一行。
所以我的建议很直接:除了日期转 varchar 这种长度确定、闭着眼睛都知道够用的场景,其他一律把长度和精度写全。decimal(18,4)、varchar(200)、nvarchar(max),多敲几个字符,能省掉后面几小时的排查。
还有一个细节:往char(n)转的时候,如果源字符串比 n 短,右边会补空格;往varchar(n)转则不会补。这两个类型在比较时行为还不一样——char比较时会忽略尾部空格,varchar不会。CONVERT(char(5), 'ab')得到'ab '(三个空格),拿去和'ab'比较是相等的;但如果转成varchar(5)得到'ab',比较也相等。真正的差异出现在你把它存进char(5)列再取出来,长度永远是 5。这个坑在做固定宽度报文对接时会遇到,报文要求定长字段,用 char 没问题,但用 char 做等值查询时要注意尾部空格的影响,我个人的做法是查询前统一RTRIM。
2.2 style 参数:日期格式化的总开关
style 是 CONVERT 最有价值的部分,也是最容易记混的部分。它是一组整数,分成两个家族:0 到 14 是不带世纪的两位年份,100 到 114 是带世纪的四位年份。两个家族的数字后半段一一对应,比如 1 和 101 都是 mm/dd/yyyy,区别只在于年份写 24 还是 2024。
常用样式我整理成一张表,照着抄就行:
| style 值 | 输出格式 | 示例(2024 年 5 月 1 日下午 3 点 20 分 45 秒) |
|---|---|---|
| 0 / 100 | mon dd yyyy hh:miAM | May 1 2024 3:20PM |
| 1 / 101 | mm/dd/yyyy | 05/01/2024 |
| 2 / 102 | yy.mm.dd | 24.05.01 |
| 3 / 103 | dd/mm/yyyy | 01/05/2024 |
| 4 / 104 | dd.mm.yy | 01.05.24 |
| 5 / 105 | dd-mm-yy | 01-05-24 |
| 110 | mm-dd-yyyy | 05-01-2024 |
| 111 | yyyy/mm/dd | 2024/05/01 |
| 112 | yyyymmdd | 20240501 |
| 120 | yyyy-mm-dd hh:mi:ss | 2024-05-01 15:20:45 |
| 121 | yyyy-mm-dd hh:mi:ss.mmm | 2024-05-01 15:20:45.000 |
| 126 | ISO8601 | 2024-05-01T15:20:45.000 |
| 23 | yyyy-mm-dd | 2024-05-01 |
| 108 | hh:mi:ss | 15:20:45 |
| 113 | dd mon yyyy hh:mi:ss:mmm | 01 May 2024 15:20:45:000 |
这里面我最常用的是 120、112、23 和 126。120 适合给人看,年月日时分秒齐全,排序也天然正确;112 适合拼进文件名或者做分区键,纯数字没分隔符;23 只取日期部分,做日报统计最合适;126 带 T 分隔符,是标准的 ISO8601,对接外部接口时用这个最稳妥,解析方基本都认。
需要提醒的是 121 和 126 末尾的毫秒。datetime类型的精度是 3.33 毫秒,所以你写.000、.003、.007这种值;换成datetime2(7)才能精确到 100 纳秒。如果对接方要求精确毫秒,源列又是datetime,转换出来的毫秒数会有点“跳”,别以为是转换函数的问题,是源类型本身的精度限制。这个我在做日志时间戳对齐时被坑过一次,两边差了 3 毫秒,查了半天以为是时区问题。
还有一个必须记住的点:不带世纪的样式(0-14)在做年份解析时有歧义。CONVERT(datetime, '01/02/24', 1)里那个 24 到底是 1924、2024 还是 2024?数据库的规则是 49 及以下算 2000 年代,50 及以上算 1900 年代。所以 24 是 2024,50 是 1950。这个规则很反直觉,我在做历史档案导入时踩过一次,1949 年之前的记录全被解析成了 2049 年。现在的做法是:只要源数据可能跨世纪,一律用带世纪的 100+ 样式,不给自己留隐患。
2.3 数字转字符:style 在数值上的另一种含义
日期之外,style 在数值转字符时也有作用,但适用范围窄得多。它主要对float、real、money、smallmoney这几类有效,规则是这样的:
| style 值 | 效果 | 输入 1234.5678 的输出 |
|---|---|---|
| 0 | 保留两位小数 | 1234.57 |
| 1 | 三位加逗号,保留两位小数 | 1,234.57 |
| 2 | 保留四位小数 | 1234.5678 |
| 3 | 去掉多余的零,保留有效小数 | 1234.5678 |
这个功能在生成财务报表或者导出 CSV 时挺好用,尤其是 style 1,自带千分位分隔符,省得在应用层再处理一遍。但要注意两点:
第一,它只对上面列的这几种类型有效。你把一个decimal(18,4)转成 varchar 再写 style 1,数据库会安静地忽略这个参数,输出还是原样。这不是 bug,是设计如此,但足够让人抓狂。我当初就是拿 decimal 试的,怎么试都不生效,翻了文档才明白适用范围。
第二,money类型本身只有四位小数精度,超过的部分会被舍入。用它做金额计算是够的,但如果你的业务涉及更多位小数的汇率换算,还是老老实实用decimal(18,6),转字符串时手动ROUND一下再拼千分位,虽然麻烦点,但结果可控。
至于整数转字符串,style 完全不起作用,CONVERT(varchar(10), 12345, 1)还是12345。想要千分位,得先转成 money 或者用其他办法。这个弯路我走过,希望你别再走。
3. 真刀真枪:几类典型场景的写法
3.1 日期与字符串来回倒腾
日常写得最多的就是这两件事:把日期格式化成字符串输出,把字符串解析成日期入库。
格式化的标准写法是这样:
SELECT CONVERT(varchar(19), CreateTime, 120) AS FullTime, CONVERT(varchar(10), CreateTime, 23) AS DateOnly, CONVERT(varchar(8), CreateTime, 112) AS DateCompact FROM Orders WHERE CreateTime >= '2024-05-01';注意每一处我都写了显式长度:19 对应yyyy-mm-dd hh:mi:ss,10 对应yyyy-mm-dd,8 对应yyyymmdd。长度刚好卡住,不多不少。写长了不会错,但写短了一定截断,而且不报错。
反过来,字符串解析成日期:
SELECT CONVERT(date, '2024-05-01', 23) AS A, CONVERT(date, '01/05/2024', 103) AS B, CONVERT(datetime, '2024-05-01 15:20:45', 120) AS C;这里有个关键认知:不带分隔符的标准格式,数据库其实能自动识别。'20240501'和'2024-05-01'这两种写法,不写 style 也能正确解析,因为它们是 ISO 格式,没有歧义。真正需要 style 的是那些“月日在前后”说不清的格式,比如01/05/2024。这种情况下,写不写 style 决定了命运——写 103 就是 5 月 1 日,写 101 就是 1 月 5 日,什么都不写在 mdy 环境下默认按 101 走。
我做跨系统对接有个固定动作:收到任何日期字符串,先问清楚格式,然后显式指定对应的 style。哪怕对方说“就是标准格式”,我也要确认这个“标准”是yyyy-mm-dd还是mm/dd/yyyy。这两个都叫标准,但差了一个月。
还有一个高频需求:只比较日期的某一天。正确姿势是把当天的起止时间算出来,用范围查询:
-- 推荐:能用索引 WHERE CreateTime >= '2024-05-01' AND CreateTime < '2024-05-02' -- 不推荐:索引失效 WHERE CONVERT(varchar(10), CreateTime, 23) = '2024-05-01'第二种写法结果是对的,但性能是灾难级的。原因在第四节细讲。
3.2 金额与精度:小数位、四舍五入与溢出
数值转换里最容易出问题的是小数位和溢出。
先看小数位。CONVERT(int, 12.5)的结果是多少?很多人以为是 12(截断),实际上是 13。SQL Server 的规则是四舍五入且“远离零”,也就是说CONVERT(int, -12.5)得到 -13,不是 -12。这个行为和 C 语言的强制类型转换、Java 的(int)转换都不一样,后者是直接砍掉小数部分。从别的语言转过来的人,第一反应往往是截断,然后对账差一分钱。
要截断的话,得用FLOOR或者ROUND(值, 0, 1)(第三个参数传非零值表示截断)。这个组合我记了很多年,因为每次都要想一下才能写对。
再看溢出。CONVERT(tinyint, 300)直接报错“算术溢出”,因为 tinyint 的范围是 0 到 255。这种情况不会给你一个“聪明”的结果,就是硬邦邦的报错。所以在做批量导入时,如果源数据的值域不完全可控,最好先用TRY_CONVERT(2012 及以后版本可用)探一下:
SELECT RawValue, TRY_CONVERT(tinyint, RawValue) AS SafeValue FROM ImportStage;TRY_CONVERT的语法和 CONVERT 完全一样,区别是转换失败时返回 NULL 而不是抛异常。这个函数在做数据清洗时太好用了,一行就能把脏数据挑出来,不会让整批任务挂掉。代价是你要额外处理 NULL,但对导入场景来说,NULL 恰恰是最好的“标记可疑行”的方式。
金额字段我一般这么写:
SELECT CONVERT(decimal(18,2), Amount) AS AmountRounded, CONVERT(decimal(18,4), Rate) AS RateRaw FROM Billing;decimal(18,2)意味着总长度 18 位、小数 2 位,也就是整数部分最多 16 位,大约能表示到千万亿级别。对绝大多数业务够用。如果你的业务涉及超大金额或者高精度汇率,把小数位加到 4 或 6,整数位相应减少,别指望默认值救你。
3.3 外部数据导入:脏数据的兜底转换
导入是最考验转换功底的地方。外部 Excel、CSV、接口返回的 JSON,字段全是字符串,还夹着一堆空格、全角符号、各种日期写法。我的标准流程是这样的:
第一步,全部字段先按nvarchar(max)进临时表,不做任何转换。这一步的目的是保真,原样存下来,方便出问题后回溯。
第二步,用TRY_CONVERT做试探性转换,把成功的行和失败的行分开:
-- 先标记转换结果 UPDATE Stage SET ConvertedDate = TRY_CONVERT(date, LTRIM(RTRIM(DateText)), 120), ConvertedAmount = TRY_CONVERT(decimal(18,2), REPLACE(REPLACE(AmountText,' ',''),',','')) WHERE Status = 'Pending'; -- 再挑出失败的 SELECT * FROM Stage WHERE ConvertedDate IS NULL AND DateText IS NOT NULL;这段里有几个细节值得说。LTRIM(RTRIM(...))是为了去掉首尾空格,导入数据里这种情况太常见了,肉眼看不出来,转换就失败。REPLACE去掉逗号是因为很多系统导出的金额带千分位,12,345.67这种直接转 decimal 会报错。注意要先去掉逗号再去空格,顺序反了遇到12, 345这种(逗号后带空格)就处理不干净。
第三步,人工核对失败行,把规律摸出来,回头改清洗逻辑。这一步别省,我见过太多人一看到失败率低就直接ISNULL(转换结果, 0)糊过去,结果报表上的金额全变成了 0,月底对账才发现。
还有一点:如果源数据的日期格式不统一,比如有的行是2024-05-01,有的是05/01/2024,有的是2024年5月1日,那就不能靠单一的 style 解决。这时候得分情况处理,先按最规范的格式试,失败的再按第二种格式试,逐层兜底。代码会啰嗦一点,但比统一报错强:
SELECT COALESCE( TRY_CONVERT(date, DateText, 120), TRY_CONVERT(date, DateText, 101), TRY_CONVERT(date, REPLACE(REPLACE(DateText,'年','-'),'月','-'), 120) ) AS ParsedDate FROM Stage;COALESCE按顺序试,第一个非 NULL 的结果胜出。这种写法在 import 场景里非常实用,缺点是执行计划会略复杂,但导入任务不在乎这点开销。
4. 隐式转换:写错一行,索引白建
4.1 隐式转换是怎么发生的
这个话题比函数本身更重要,因为它不报错、不影响结果正确性,只影响速度,属于那种“看不见的损耗”。
规则是这样的:当两个不同数据类型的值做比较或者运算时,数据库不会报错,而是按一套优先级规则,把低优先级的那一方转换成高优先级的那一方。常见的优先级顺序里,数值类型高于字符类型,int高于varchar,datetime高于varchar。
举个具体的:
-- OrderNo 是 varchar(20),上面有索引 SELECT * FROM Orders WHERE OrderNo = 1001;OrderNo是字符型,1001是数值型。按规则,字符型优先级低,所以数据库会把OrderNo这一列逐行转成 int 再比较。注意是“逐行”,因为每一行的值都可能不同,无法预先算好。这就意味着索引里存的字符值没法直接拿来比对,只能全表扫一遍。一个几百万行的表,原本毫秒级的查询变成几十秒。
反过来写就没有这个问题:
SELECT * FROM Orders WHERE OrderNo = '1001';这时候两边都是字符型,类型一致,索引可以正常走。这个例子里的区别就是一对单引号,但性能天差地别。我见过的最惨案例是一个核心查询因为少了这对引号,从 200 毫秒变成 18 秒,压测直接挂掉,查了一整天才发现是这个问题。
日期字段同理:
-- CreateTime 是 datetime,有索引 SELECT * FROM Orders WHERE CreateTime = '2024-05-01'; -- 常量被转成 datetime,走索引 SELECT * FROM Orders WHERE CONVERT(varchar(10), CreateTime, 23) = '2024-05-01'; -- 列被包了函数,不走索引第二行的本质不是隐式转换,而是列上套了函数。索引里存的是原始的 datetime 值,你把它转成字符串再比,数据库没法用索引定位,只能逐行算。这两种失效方式结果一样:查询变慢。
4.2 把条件写成“可搜索”的形式
判断标准很简单:等号或比较符号的左边只放列名,不放任何函数和运算。只要违反了,索引基本就用不上(除了少数函数索引的场景,普通索引无能为力)。
按照这个标准,把常见的错误写法改成正确写法:
| 需求 | 走索引的写法 | 索引失效的写法 |
|---|---|---|
| 查某一天 | CreateTime >= '2024-05-01' AND CreateTime < '2024-05-02' | CONVERT(varchar(10), CreateTime, 23) = '2024-05-01' |
| 按月统计 | CreateTime >= '2024-05-01' AND CreateTime < '2024-06-01' | CONVERT(varchar(7), CreateTime, 120) = '2024-05' |
| 字符列比数值 | OrderNo = '1001' | OrderNo = 1001 |
| 数值列比字符 | Id = 1001 | Id = CONVERT(varchar(20), 1001)(这种属于过度设计) |
第三行和第四行看着对称,其实不一样。数值列和字符常量比较时,常量被转成数值,列本身没动,索引照走。所以WHERE Id = '1001'是安全的,不用刻意去改。真正危险的是字符列和数值常量比较,这个方向必须避免。
我在项目里养成了一个习惯:凡是字符类型的列,比较时统统加引号,哪怕值是纯数字。看起来有点强迫症,但确实能避免一堆隐式转换。代码审查的时候,看到字符列旁边有裸数字,直接标红。
还有一种情况容易被忽略:JOIN的连接条件。如果两张表的关联字段类型不一致,一张是varchar,一张是int,那么连接时同样会发生隐式转换,执行计划可能从哈希连接退化成嵌套循环,性能断崖式下跌。这种问题在数据仓库里特别常见,因为上游系统的 ID 类型五花八门。解决办法只有一个:建表时统一类型,实在统一不了,就在子查询里显式CONVERT一次再关联,虽然多一步,但至少是可控的。
5. 常见问题速查与排查思路
5.1 高频报错对照表
CONVERT 相关的报错其实就那么几种,认出关键词就能快速定位。我整理了一张速查表:
| 报错信息关键词 | 常见原因 | 处理办法 |
|---|---|---|
| 将 varchar 转换成 int 时失败 | 字符串里有非数字字符,或含空格、逗号、单位符号 | 先TRY_CONVERT找出脏行,再REPLACE清洗 |
| 算术溢出错误 | 目标类型范围不够,如 tinyint 存 300、int 存 30 亿 | 换成更大的类型,或先判断值域 |
| 从字符串转换日期/时间时失败 | 格式不匹配 style,或存在非法日期如 2 月 30 日 | 核对源格式与 style 是否对应,用TRY_CONVERT定位 |
| 转换时字符串被截断 | 目标长度不够,如varchar(10)转 20 个字符 | 加长目标长度,或改用varchar(max) |
| 将 datetime 转成 int 时失败 | 类型完全不兼容 | 先转字符串再转数值,两步走 |
| 参数数据类型 ntext 对于 convert 函数的参数 1 无效 | 用了已废弃的ntext/text/image | 改用nvarchar(max)/varchar(max) |
最后一条现在少见了,但维护老系统时还是会遇到。text和ntext是早期版本的类型,2005 之后就建议淘汰,2025 年前后已经在部分版本里彻底移除。碰到这种表,正确做法是先把列类型改成nvarchar(max),再谈转换。
关于截断,有个细节值得单独说:CONVERT在SELECT里截断是静默的,不报错;但如果是INSERT到目标表,而目标列长度不够,就会报“字符串或二进制数据将被截断”。同一个截断问题,在不同环节表现不一样。所以排查时先看清楚报错发生在哪一步,别被表象带偏。
5.2 我踩过的几个坑与实操心得
说几个印象深的。
第一个坑:varchar 默认长度 30 的静默截断。早年做一个日志导出,脚本里写的是CONVERT(varchar, Message, 120),Message 是个长文本字段。导出的文件看着没问题,抽查几行也正常,上线跑了三个月,业务方反馈“日志内容不全,后半段没了”。回头一查,全被砍到 30 个字符。从那以后,我写CONVERT到字符串类型时,长度一定显式写,哪怕是varchar(10)这种明摆着够用的场景。
第二个坑:两位年份的世纪判定。前面提过,49 是分界线。这个规则不难记,难的是“你以为数据里不会出现两位年份”。我做历史数据迁移时,源表里混着一批 1998 年的单据,格式是不带世纪的dd-mm-yy,用 style 5 解析,结果 98 变成了 1998(因为 98 大于 49),但 05 变成了 2005。同一批数据里跨了两个世纪,而且逻辑上说得通,肉眼根本看不出来。后来全部改成纯手工判断:先用带世纪的样式解析,解析失败再走另一条路。这件事给我的教训是,日期解析永远不要依赖默认规则。
第三个坑:CONVERT 在 WHERE 里把索引搞没了。这个前面讲过了,但我想补充一个排查方法。当你不确定某个查询有没有走索引时,去看执行计划里的“扫描”还是“查找”。如果是“聚集索引扫描”或者“索引扫描”,而你的条件是等值匹配,那八成是被函数或者隐式转换挡住了。这时候把条件里的函数挪到等号右边(对常量做转换),计划立马变“查找”。
第四个心得:转换逻辑尽量往前放。意思是,能在数据入库时就转好的类型,不要留到查询时再转。查询时转,意味着每次执行都要算一遍,而且会破坏索引。入库时转,只算一次,后续所有查询都受益。这条原则听起来简单,但在实际项目里,很多人为了“保持原始数据”而把什么字段都存成字符串,最后在报表层做大量的CONVERT,性能一塌糊涂。我的做法是:原始数据存一份到归档表,业务表按正确的类型存,两不耽误。
第五个心得:写转换脚本先小批量跑一遍。不管逻辑多简单,先在几百行数据上跑,看看失败率、看看转换后的样子对不对,再用全量跑。我见过太多次“直接上百万行然后报错回滚”的场面,虽然是事务安全的,但时间成本白白浪费。
最后分享一个平时用得最多的小工具:定位脏数据的三行 SQL。
SELECT TOP 100 * FROM SourceTable WHERE TRY_CONVERT(decimal(18,2), AmountText) IS NULL AND AmountText IS NOT NULL;这三行的意思是:原值不是空,但转换失败了。跑一下就能看到前 100 条问题数据,基本上一眼就能看出规律——是带了货币符号,还是有全角数字,还是压根就是文字备注填错了格子。比一行行翻原始数据快太多。我现在的习惯是,任何导入脚本上生产之前,先把这三行贴进去跑一遍,心里有底了再动手。