ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL Server数据类型全解析:选型原则、性能影响与常见陷阱

SQL Server数据类型全解析:选型原则、性能影响与常见陷阱 SQL Server的数据类型几乎每个接触过数据库的人都绕不开但真正把它彻底吃透的人并不多。许多看过的项目里性能问题、数据错乱、存储空间暴涨追根溯源都是建表时类型选错了。这篇东西就把 SQL Server 的所有内置数据类型从头到尾整理一遍包括它们之间的区别、各自适合的场景、容易踩的坑以及选型时的一些实战判断。不管你是刚入门的开发还是已经带过项目的老手都可以当一份速查手册来用。我不打算简单罗列官方文档里的定义和范围那太没意思了。我会结合实际开发中遇到的典型问题来讲比如为什么用 float 存金额会算错为什么明明有 varchar 还要用 nvarchar为什么 datetime2 比 datetime 更值得推荐。把这些问题搞清楚数据类型这关才算真正过了。1. 为什么数据类型这么重要先把底层逻辑理清楚1.1 数据类型决定存储和计算行为数据库本质上就是一张张表格每列的数据类型决定了这一列能存什么、占多大空间、怎么比较、怎么排序、怎么计算。你可以把 SQL Server 想象成一座精密的仓库数据类型就是货架规格有的货架只能放某种尺寸的盒子有的货架能放任意大小的包裹有的货架因为设计特殊放东西不仅占位置还额外占用过道空间。存储行为上的差异非常明显。比如int固定占 4 字节而varchar(100)是按实际字符长度动态占用空间最多不超过 100 个字符在 SQL Server 中 varchar 是按字节存储但 varchar 类型本身存储的实际是字符具体字节数还跟排序规则和字符编码有关。计算行为上的差异更微妙整数之间做除法会截断小数decimal运算则根据精度和标度保留小数位float则会引入二进制浮点的精度误差。这些差异直接影响业务结果尤其是涉及金额、数量、比例计算的场景。1.2 数据类型选错的典型后果选错类型的后果往往不是立刻暴露的而是以一种非常难受的方式出现。最常见的一个金额字段用float存。前期数据量小看不出问题等累计到几十万条时一条订单金额是 0.1另一个是 0.2加起来却是 0.30000000000000004。客户看到的对账单上写着一长串小数领导和业务部门同时炸锅。这不是偶然是二进制浮点数表示十进制的天然缺陷改一下字段类型就能解决。第二个常见问题字符串排序规则冲突。一个数据库里既有char字段又是中文排序规则做多表关联时两张表的排序规则不一致直接报错“无法解决排序规则冲突”。这个问题在新老系统迁移、跨库查询时特别多本质上就是设计表时没有统一类型配套规则。第三个问题日期精度不够。datetime的精度是 3.33 毫秒而且只能表示 1753 年 1 月 1 日以后的日期。有些业务系统需要记录毫秒级精确时间或者处理更早的历史数据就必须改用datetime2。很多人接手老系统时看到一整排datetime规则已经定死改起来要动一堆存储过程异常痛苦。所以建表时多花几分钟想清楚每个字段的类型后面就能节省几周的返工时间。下面逐类过一遍。2. 数值类型全解析从bit到decimal一张表说清楚2.1 整数类型怎么选tinyint/smallint/int/bigintSQL Server 提供了四种整数类型它们之间的差别主要是取值范围和存储字节数。类型存储大小取值范围典型用途tinyint1 字节0 到 255状态码、开关标识、年龄smallint2 字节-32768 到 32767人数、配置项序号int4 字节-2147483648 到 2147483647最常见的主键、外键、常规计数bigint8 字节-9223372036854775808 到 9223372036854775807雪花ID、大表自增主键、时间戳数值选择原则很直观够用就好但尽量留一点余量。状态码用tinyint完全没问题因为业务状态就那几个。订单表的主键用int通常就够了但如果你预估单表数据量会超过 21 亿行那就得直接上bigint。别觉得 21 亿很远日志表、流水表、物联网数据表很容易达到。需要留意的是程序语言和数据库类型不是一一对应的。比如 C# 的byte对应tinyintshort对应smallintint对应intlong对应bigint。如果类型不对齐EF Core 或者 ADO.NET 在映射时会出现异常或转换开销这也是为什么好多团队坚持用int撑起所有整数列省的出幺蛾子。2.2 精确数值decimal与money的坑decimal(p,s)在 SQL Server 里也可以写成numeric(p,s)是真正的精确十进制类型。p是精度表示总共能存多少位数字s是标度表示小数点后保留几位。比如decimal(10,2)最多表示 10 位有效数字其中小数点后 2 位所以整数部分最多 8 位范围是 -99999999.99 到 99999999.99。计算存储大小时有个经验公式存储字节数大约是(p s) / 2 1左右但具体值可以查官方文档。实际开发中不需要算那么细只需要明白p越大越占空间。decimal特别适合金额、税率、折扣等需要精确计算的字段。我在订单系统里几乎全部用decimal(18,2)18 位有效数字对绝大多数企业级应用来说绰绰有余2 位小数对应“分”。如果你要处理加密货币那种 8 位小数可以设成decimal(28,8)。说完 decimal就必须提money和smallmoney。money占 8 字节范围约 -922337203685477.5808 到 922337203685477.5807精度是小数点后 4 位。它确实是个数值类型但做除法或跨币种换算时会出现四舍五入的诡异行为而且money被微软标记为“传统类型”并不推荐在新系统中使用。更关键的是ORM 对money的映射有时会跟decimal不一致很容易在报表里看到奇怪的精度尾巴。我的建议是新项目一律用decimal老项目如果遇到money就别轻易动它但新代码不要再写money了。2.3 浮点类型float/real慎用的经典原因real占 4 字节约等于 C# 的floatfloat在 SQL Server 中默认占 8 字节约等于 C# 的double。它们适合科学计算、物理模拟、统计指标这类对精度要求不苛刻、但对取值范围和计算性能有要求的场景。但千万注意浮点类型不能用于需要精确表示的金额、数量、比率。背后的原因很简单计算机用二进制表示小数时很多十进制小数是无限循环的。比如 0.1 在二进制里是一个无限循环小数存储时只能截断一部分所以 0.1 0.2 不等于 0.3。这就是很多财务系统被坑的原因。如果必须用浮点数做范围比较一定要加一个容差比如ABS(a - b) 0.0001否则查询结果会让你怀疑人生。另外对float列做索引、分组、排序也会因为精度问题导致意外的重复和缺失。能避开就避开。3. 字符串类型varchar与nvarchar的世纪难题3.1 定长、变长与maxchar/varchar/text这三种字符类型的使用场景、存储方式和性能表现差异很大。char(n)是定长字符串无论你存储多少字符它都会占满 n 个字符的空间具体字节数还要乘以编码字节数。它的优势是存取速度快适合长度基本固定的数据比如 MD5、身份证号如果只考虑数字和X、国家代码。问题是如果存的数据长度波动很大比如char(100)存一个“OK”后面 98 个字符全是空格不仅浪费存储还会在查询时产生意外的空格比较问题。varchar(n)是变长字符串只存实际需要的内容加少量额外开销。varchar(50)存“OK”就只占几个字节远小于定长的 100 字节。它适合绝大多数文本字段比如用户名、邮箱、订单号。注意varchar(n)的n表示最多能存多少字符不是字节数。如果是英文字母和数字一个字符占 1 字节如果是中文在varchar里一个汉字可能占 2 字节取决于排序规则的代码页比如简体中文 GBK 编码下是 2 字节。因此varchar(20)不一定能存 20 个汉字这点新人特别容易踩坑。text是一个老古董用于存储长文本但现在已经被废弃了。微软官方明确建议用varchar(max)替代。varchar(max)最多可以存 2GB 的字符数据适合文章正文、描述、JSON 等。但要提醒一点varchar(max)的大字段会直接影响查询性能不要把大文本放在频繁查询的表里否则会把整个行的数据页撑大增加 IO 开销。更合理的做法是单独建一张“内容表”把大字段独立出去。3.2 Unicode与N前缀nvarchar和nchar存在的意义nvarchar(n)和nchar(n)对应的是 Unicode 字符集用N前缀标识。它们能存储所有语言的字符包括中文、日文、韩文、阿拉伯文、emoji 等无论什么排序规则nvarchar下的每个字符都按 Unicode 编码存储。SQL Server 2019 及以后版本引入了 UTF-8 支持的排序规则varchar也能存中文了但为了稳妥我仍然推荐存字符串默认用nvarchar。存储上的代价是nvarchar每个字符通常占 2 字节UTF-16空间占用比非 Unicode 的varchar更大。但现代服务器存储和内存都不贵换来的是绝对稳定的字符兼容性避免乱码问题。尤其是做国际化系统、用户生成内容、跨平台数据交换用nvarchar省心得多。一个经典错误是很多人省事建表时所有字符串都用nvarchar(max)。这种做法虽然避免了长度不够的问题但会让索引变得非常笨重。SQL Server 的索引键最大支持 900 字节普通非聚集索引nvarchar(max)根本没法直接建索引你必须用nvarchar(450)以下的长度或者在列上计算哈希列。所以不要无脑用 max按实际业务最长值再加一点余量去定长度是最稳妥的。3.3 排序规则Collation对字符串存储和比较的影响排序规则决定了字符串的比较规则、排序顺序、大小写敏感度和重音敏感度。比如Chinese_PRC_CI_AS表示中文简体、不区分大小写、区分重音。排序规则还会影响字符串存储编码同一个varchar列在不同排序规则下存储同一个汉字占用的字节可能不同。最常见的问题是跨库关联时排序规则冲突。比如一个库的排序规则是SQL_Latin1_General_CP1_CI_AS另一个是Chinese_PRC_CI_AS两个表的varchar列做JOINSQL Server 直接报错。解决办法是在查询里用COLLATE DATABASE_DEFAULT统一规则但这对性能有影响。更推荐的做法是建库时统一规划排序规则新库直接用Chinese_PRC_CI_AS或Latin1_General_CI_AS尽量避免混合环境。另外如果你需要精确匹配用户密码哈希或敏感编码值要特别注意大小写敏感排序规则。默认的_CI_AS是不区分大小写的如果业务里需要区分大小写比如优惠码要么用_CS_AS排序规则要么在比较时加COLLATE Latin1_General_CS_AS。这也是一个容易忽视的小坑。4. 日期时间类型别再全部用datetime了4.1 七种日期时间类型的参数对比SQL Server 里日期时间相关的内置类型一共有七种很多新人只知道datetime但实际上它们之间的差异非常关键。类型存储大小精度/刻度范围说明date3 字节1 天0001-01-01 到 9999-12-31只有日期没时间time3 到 5 字节100 纳秒00:00:00.0000000 到 23:59:59.9999999只有时间没日期smalldatetime4 字节1 分钟1900-01-01 到 2079-06-06精度太低适合粗粒度datetime8 字节3.33 毫秒1753-01-01 到 9999-12-31老项目里最常见的类型datetime26 到 8 字节100 纳秒0001-01-01 到 9999-12-31微软推荐的现代类型datetimeoffset8 到 10 字节100 纳秒0001-01-01 到 9999-12-31带时区偏移量适合分布式系统timestamp/rowversion8 字节数据库自动生成不表示时间在 5.2 节单独讲smalldatetime在旧系统中常见但它的精度只能到分钟存2024-01-01 12:34:56会被四舍五入成12:35:00做考勤、订单时间计算时会出错。新项目完全没必要用它。4.2 datetime2为什么是更好的默认选择datetime2比datetime的优势有三个精度更高、范围更广、存储空间有时更小。datetime2(3)存储毫秒级数据只需要 6 字节而datetime是 8 字节datetime2(7)精度到 100 纳秒存储 8 字节和datetime一样大但精度高了无数倍。在日常开发中我基本上把datetime2(3)作为默认选择既能满足毫秒级精度又能节省存储空间。如果业务场景不需要毫秒datetime2(0)也可以它甚至能直接表示0001-01-01有些历史系统需要记录公元前日期比如考古、天文数据时datetime是无能为力的只能用datetime2。不过要注意ORM 对datetime2的支持略有差异。EF Core 中默认会把DateTime映射为datetime2这没问题。但如果你的老项目里已经有大量的datetime存储过程参数改成datetime2后可能因为参数类型不匹配导致隐式转换执行计划就不走索引了。所以老库不要盲目全局替换datetime新库默认datetime2是最合理的策略。4.3 时区问题与datetimeoffset全球化业务一定会遇到时区问题。如果只存datetime2那你存储的是本地时间还是 UTC 时间靠程序约定靠注释靠 DBA 经验但这都不够可靠。datetimeoffset类型自带时区偏移量可以同时存储“本地时间 与 UTC 的偏移”。比如东京是2024-06-01 10:00:00 09:00伦敦是2024-06-01 02:00:00 01:00它们表示的是同一个瞬间但存储值不同。如果你要在多个时区的数据库之间同步数据或者用户遍布全球datetimeoffset是最不容易产生歧义的选择。SQL Server 还提供了SWITCHOFFSET、TODATETIMEOFFSET等内置函数可以很方便地把时间在不同时区之间换算。但也要注意很多老版本 ORM 对datetimeoffset的支持不完善比如一些版本的 EF Core 会把DateTimeOffset映射成datetime2并丢失偏移量。这就要看你使用的框架版本。总的来说如果系统只在单一地区运行前后端都明确使用 UTC 传输那么datetime2配合严格的 UTC 约定也够用一旦涉及跨时区协作datetimeoffset才更稳。5. 二进制、GUID与特殊类型容易被忽略但关键时刻能救命5.1 binary/varbinary/image字节流存储场景binary(n)和varbinary(n)用于存储字节数组适合存加密后的数据、哈希值、文件碎片、序列化对象等。binary是定长varbinary是变长varbinary(max)对应.NET的byte[]也用于替代废弃的image类型。实际项目中我经常看到有人把图片、PDF 文件直接塞进varbinary(max)字段。从技术上完全可行性能也不算差但真要存大量大文件时我建议把文件放到对象存储或者独立文件表数据库里只存文件路径或哈希值。为什么因为varbinary(max)会把大对象塞进 SQL Server 的数据文件备份恢复、扩容、迁移时都是一个巨大的压力而且不方便 CDN 加速。对于小文件比如几百 KB 的头像存库内反而更方便事务一致性好各有利弊。另外存密码哈希时很多人用varchar存十六进制字符串比如0x5F4DCC3B5AA765D61D8327DEB882CF99这种字符串会占用双倍空间。更规范的方式是用binary(16)存 MD5 结果、binary(32)存 SHA-256 结果这样占用更小、比较更快。不过使用二进制哈希列时一定要在应用层处理大小写因为二进制比较是区分大小写和原始字节的。5.2 rowversion与uniqueidentifier并发控制与分布式主键rowversion旧称timestamp是一个自动生成的二进制值每次对行做更新时SQL Server 会自动把它改成一个新的值。它不表示日期时间只是单调递增的版本号。它的主要用途是乐观并发控制读取一行记录rowversion更新时在WHERE条件里带上这个值如果别人已经更新过rowversion变了你的更新条件不成立这样就能避免丢失更新。我之前在多个团队里推过这个做法用rowversion做并发控制比“用更新时间戳判断”可靠得多因为它完全由数据库保证唯一性和变化性不受应用层时钟和精度影响。不过要注意如果一个表有多个用户同时频繁更新rowversion会在每个事务里自动变化所以在批量更新场景下可能造成预期的行不匹配。uniqueidentifier就是 GUID16 字节几乎可以保证全球唯一。它常用于分布式系统、多库合并场景中的主键因为不同机器生成的 GUID 在主键合并时不会冲突。缺点是随机性导致索引碎片严重插入性能不如自增bigint。如果要用 GUID 做主键最好配上NEWSEQUENTIALID()生成顺序 GUID减少页拆分。另外GUID 作为主键时占用的存储空间更大外键也会跟着变大对大型系统来说是一个需要权衡的问题。分库分表时有时也用bigint 号段方案不一定非要 GUID。5.3 XML与sql_variant半结构化数据的两种思路xml类型可以直接存储 XML 文档或片段并支持 XQuery 查询、修改、索引。它适合存储配置文件、报表模板、消息协议这类本身是 XML 格式的数据。SQL Server 里还可以对xml列建立主 XML 索引和辅助 XML 索引让针对 XML 内部节点的查询变快。但我觉得如果目标只是存一段 XML没有在数据库里面查询节点内容的需求那直接用nvarchar(max)存原始文本就够了做 XML 解析放到应用层数据库的压力会小很多反之如果你要频繁根据 XML 内部元素做筛选就要认真建 XML 索引否则全表扫描会把数据库拖垮。sql_variant是一种可以存储多种数据类型值的特殊类型。同一列里第一行是整数、第二行是字符串、第三行是小数都可以。它适合那种结构不确定、无法用普通列建模的场景比如通用配置表、扩展属性表。但它不能作为主键或外键不支持全文索引也不能参与ORDER BY直接比较要先转换。实际开发里大多数场景可以用json、nvarchar(max)或者多个可空列来替代sql_variant属于“万不得已才用”的类型。5.4 空间数据、hierarchyid与CLR类型只在特定领域用SQL Server 提供了geometry和geography两种空间数据类型。geometry处理平面坐标系geography处理椭球体地球坐标。它们支持空间索引、距离计算、交集判断等操作常用于地图、GIS、物流路径规划。如果项目跟地理位置相关用空间索引去算“附近的人”会比自己在应用层用经纬度公式算快很多。hierarchyid用于表示树形结构比如组织架构、分类树。它把每个节点编码成一条路径字符串能直接通过GetAncestor、GetDescendant等方法操作。比传统的parent_id递归表要简洁但复杂的树操作有时候还是递归查询更直观看团队熟悉度。还有 CLR 自定义类型可以基于 .NET 开发自定义数据类型但微软对这类支持已经慢慢降低热度除非有极强的定制需求否则不建议新项目尝试。这些特殊类型属于“用对场景是利器用错场景是灾难”普通业务系统里并不常见。6. 实战选型原则与常见问题排查6.1 一劳永逸的数据类型选型清单基于我日常建表的经验整理了下面一套默认规则可以帮你快速决策整数主键用bigint还是int看数据量预期普通系统int够用但如果你不敢打包票直接用bigint前期多花 4 字节后期省一次大迁移。布尔标识用bit。不要用char(1)存Y/N更不要用int存 0/1。金额、单价、折扣用decimal(18,2)或decimal(28,8)根据币种和精度要求调整。百分比、比例可以用decimal保留 4 位小数足够比如0.1234。一般字符串用户可输入内容用nvarchar程序生成的有限字符集如手机号、订单号用varchar也许省空间但为兼容中文和特殊符号新项目还是建议nvarchar。日期时间默认datetime2(3)全球分布系统考虑datetimeoffset。大文本/长 JSON用nvarchar(max)但注意独立表存储或分表。文件内容尽量不存数据库一定要存用varbinary(max)。并发控制版本号用rowversion别手动生成。这套规则不一定适合所有场景但可以让 90% 的建表需求稳稳妥妥地落地。6.2 隐式转换为什么会杀性能类型不匹配时SQL Server 会在后台自动做隐式转换。隐式转换本身不可怕可怕的是它出现在查询条件列上导致索引失效。举个例子表里OrderTime是datetime2但存储过程参数是varchar查询写WHERE OrderTime param。如果字符串转换成datetime2参数转换没问题但如果优化器选择先转换列值那么这一列上的索引就无法使用了。如果表很大一次查询就是全表扫描。另一个典型varchar列和nvarchar参数比较。因为varchar隐式转换成nvarchar的优先级更高所以实际上列值会逐行转换成nvarchar再比较索引照样失效。所以一定要保持应用传入参数的类型与列类型一致。我在排查慢查询时第一步就是看执行计划里有没有 CONVERT_IMPLICIT 警告十次有八次能在这里发现问题。6.3 常见报错与解决从转换失败到排序规则冲突数据类型相关的报错五花八门这里列几个出现频率最高的“将 varchar 转换为数据类型 int 时失败。”这种通常是把数字字符串列和数字列做了比较或者程序传入的参数是字符串但列是 int。解决方法是审查查询条件和传入参数类型不要在 SQL 里写WHERE VARCHAR_COL 123而是让应用传字符串或者反过来把列转成数字。但后者通常意味着索引失效。“String or binary data would be truncated.”这个报错是因为插入的数据超过了字段长度限制。老版本 SQL Server 会直接报错2016 以后引入了截断警告。解决办法是检查字段长度与实际数据。我通常建议建表时给长度留 20% 余量比如用户名最长 30 个字符不要咔咔定varchar(20)直接给nvarchar(50)。“Cannot resolve collation conflict.”多表关联时排序规则不一致。解决办法是给比较或连接列统一COLLATE DATABASE_DEFAULT或直接改表字段排序规则。最彻底的做法是建库时统一排序规则。“Arithmetic overflow error converting numeric to data type numeric.”这个是因为数字超出decimal定义的精度和标度。比如decimal(5,2)最多存 999.99你非要存 1000。反过来如果你的应用层计算结果是12.345而字段是decimal(18,2)SQL Server 在隐式转换时会四舍五入到 12.35而不是报错这也会造成数据偏差。“The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.”日期字符串格式不对或者数据库的日期语言设置跟传入格式不匹配。比如SET LANGUAGE English之后01/02/2024表示 1 月 2 日还是 2 月 1 日取决于排序规则和语言设置。稳妥做法是应用层统一使用 ISO 8601 格式2024-01-02T10:00:00或者参数化查询。还有一类和登录密码策略相关的报错比如“密码已过期无法登录”其实就是 SQL Server 登录名的密码策略到期了这类不是数据类型问题但经常被误以为是数据库类型兼容问题。如果是自己本地测试库可以用ALTER LOGIN ... CHECK_POLICY OFF临时解除限制但在生产环境一定要遵循公司的密码安全策略别乱来。7. 写在最后我的一点经验数据类型看着是建表时几秒钟的选择但它决定了一个系统未来几年的稳定性和性能。我在实际项目中见过因为一个nvarchar(max)用错导致索引失效、慢查询堆满监控面板的也见过一个decimal精度配错导致财务报表小数点对不上的。这些问题说大不大但排查起来非常花时间。个人建议每个团队最好沉淀一份自己的“建表规范”把常见的字段类型路由表定下来。比如主键统一bigint、金额统一decimal(18,4)、时间统一datetime2(3)、用户输入统一nvarchar(n)。新人照着写老手检查起来也轻松。最后分享一个小技巧如果你不确定某个类型的具体表现直接在 SQL Server Management Studio 里用SELECT CAST(... AS 类型)或CONVERT(类型, 值)快速验证。比如SELECT CAST(10.0 / 3 AS DECIMAL(18,2))看看结果是不是你预期的 3.33。多做这类小实验比死记硬背文档有用得多。
返回列表