简介:这份SQL数据类型详解PDF面向数据库初学者与SQL Server开发人员,系统梳理了数据类型这一建表与查询中的基础核心知识,帮助读者在字段设计时准确选型、避免存储与精度问题。资源包共1个PDF文件,大小约71KB,内容按二进制、字符、Unicode、日期时间、数字、货币及特殊数据类型逐类展开,并延伸至用户自定义数据类型。文中对Binary与Varbinary的定长变长差异、Char与Varchar的8KB边界、Nchar与Nvarchar的存储翻倍特性、Datetime与Smalldatetime的日期范围、Int与Smallint与Tinyint的取值范围,以及Decimal、Float、Money、Timestamp、Bit、Uniqueidentifier等类型均给出具体说明与字节占用,还涉及Set DateFormat日期格式设置。目前已有614人学习,适合作为速查手册与建表参考。
1. 选错一个字段类型,上线后多花三天改表
上周帮一个朋友看他的订单系统,一张三百万行的表,金额字段当初随手写了float。结果对账时发现,同一批订单在报表里汇总出来的总额,跟财务系统差了七分钱。七分钱不多,但财务不认,业务方追着要解释,最后定位到就是浮点累加误差。改字段类型、写迁移脚本、停机窗口重跑,前后折腾了三天。这种事在 SQL 数据类型上太常见了——建表时觉得无所谓,跑起来才发现每个类型都有自己的脾气。
这份 SQL 数据类型详解资料,把 SQL Server 里常用的数据类型按二进制、字符、Unicode、日期时间、数字、货币、特殊类型分了七大类,还带上了用户自定义类型的创建和删除。它解决的不是"什么是 int"这种入门问题,而是帮你在建表和改表时,能对着存储字节数、取值范围、定长变长的差异,做出不后悔的选择。适合正在写建表语句的后端、要做数据迁移的 DBA,以及被varchar和nvarchar搞混过的所有人。
2. 二进制与字符类型:定长变长差在哪,怎么选不翻车
2.1 Binary、Varbinary、Image 的存储账
先看二进制这一组。Binary[(n)]是固定长度的二进制数据,n 从 1 到 8000,存储空间是 n + 4 个字节。注意这个 +4,它不是数据本身,是 SQL Server 用来记录长度的额外开销。Varbinary[(n)]是变长的,n 同样 1 到 8000,但存储空间是实际数据长度 + 4,不是 n + 4。这个区别很关键:如果你声明varbinary(8000)但实际只存 10 个字节,占用的就是 14 字节左右,而不是 8004 字节。
Image类型存的是位字符串,SQL Server 不解释它的内容,必须由应用程序自己解析。比如你把 BMP、GIF、JPEG 塞进去,取出来还得靠程序还原成图片。它的最大长度是 2^31-1,也就是 2GB。
那什么时候用 Binary 而不是 Varbinary?常见做法是:长度完全固定的用 Binary,比如存一个固定 16 字节的哈希值;长度会变的用 Varbinary,比如加密后的密文、序列化后的对象。我一般会优先选 Varbinary,因为定长类型在数据长度参差不齐时反而浪费空间。
-- 建一张存文件指纹的表,指纹固定 32 字节,用 binary CREATE TABLE FileFingerprint ( FileId INT IDENTITY(1,1) PRIMARY KEY, Fingerprint BINARY(32) NOT NULL, -- SHA-256 固定 32 字节 Thumbnail VARBINARY(MAX) NULL -- 缩略图长度不定,用 varbinary ); -- 插入时注意:binary 长度不足会补 0x00,不是报错 INSERT INTO FileFingerprint (Fingerprint, Thumbnail) VALUES (0x1A2B3C4D5E6F708192A3B4C5D6E7F801, 0xFFD8FFE0);这段代码里,BINARY(32)声明了固定 32 字节,如果插入的十六进制串不足 32 字节,SQL Server 会在右侧补0x00,不会报错。这一点很容易踩坑:你以为存进去的是 10 字节,取出来变成 32 字节,前面 10 个对,后面全是 0。VARBINARY(MAX)则按实际长度存,适合缩略图这种大小不一的场景。
提示:SQL Server 2005 之后
varbinary(max)已经能替代image,新项目不建议再用image,它在后续版本里属于被弃用的类型。
2.2 Char、Varchar、Text 的长度边界
字符类型这一组,Char是定长,Varchar是变长,Text用来存超过 8KB 的 ASCII 数据。资料里写得很清楚:Varchar长度不超过 8KB,Char最多 8KB,超过 8KB 的用Text。
这里有个细节值得展开。Char(n)不管你实际存几个字符,都占 n 个字节,不足的部分用空格补齐。Varchar(n)只占实际字符数加一点长度开销。所以像身份证号、统一社会信用代码这种长度固定的,用Char反而更合适,因为定长在索引和比较时效率更稳定;而姓名、地址这种长度不一的,必须用Varchar,否则空间浪费惊人。
Text类型现在也基本被varchar(max)取代了。varchar(max)能存到 2GB,而且支持大部分字符串函数,Text类型很多函数用不了,排序、比较也受限。
-- 对比 char 和 varchar 的实际占用 CREATE TABLE CharDemo ( FixedCode CHAR(18) NOT NULL, -- 统一社会信用代码,固定 18 位 Name VARCHAR(50) NOT NULL, -- 姓名,长度不定 Remark VARCHAR(MAX) NULL -- 长文本备注,替代 text ); -- 插入一条,FixedCode 不足 18 位会补空格 INSERT INTO CharDemo (FixedCode, Name, Remark) VALUES ('91310000MA1K35QX8H', '张三', '这是一段很长的备注...'); -- 查询时注意 char 的尾部空格:下面两个条件结果可能不同 SELECT * FROM CharDemo WHERE FixedCode = '91310000MA1K35QX8H'; SELECT * FROM CharDemo WHERE FixedCode = '91310000MA1K35QX8H '; -- 带空格CHAR(18)在比较时,SQL Server 默认会忽略尾部空格,所以上面两条查询通常返回相同结果。但如果你用LIKE或者把值拼进字符串,尾部空格就会显形。我一般会在应用层对Char字段做TRIM,避免这种玄学问题。
2.3 Unicode 类型:Nchar、Nvarchar、Ntext 的取舍
Unicode 这一组是中文项目里最容易出问题的。Nchar、Nvarchar、Ntext分别对应Char、Varchar、Text,但每个字符占 2 个字节,所以同样声明长度,Unicode 类型能存的字符数是非 Unicode 的一半。资料里说Nvarchar最多存 4000 个字符,Nchar也是 4000,Ntext可以超过 4000。
关键点在于:非 Unicode 类型依赖安装时选的字符集,如果字符集不支持中文,存进去就是乱码。Unicode 类型不依赖字符集,任何 Unicode 标准定义的字符都能存。代价是存储空间翻倍。
那到底怎么选?我的习惯是:只要字段可能存中文、日文、韩文或者 emoji,一律用Nvarchar。纯英文的编码、状态值可以用Varchar省空间。Ntext同样建议用Nvarchar(max)替代。
-- 中文场景建表:用 nvarchar,插入时字符串前加 N CREATE TABLE Customer ( CustomerId INT IDENTITY(1,1) PRIMARY KEY, CustomerName NVARCHAR(100) NOT NULL, -- 中文姓名 Address NVARCHAR(200) NULL, -- 中文地址 Bio NVARCHAR(MAX) NULL -- 长简介 ); -- 插入中文必须加 N 前缀,否则可能乱码 INSERT INTO Customer (CustomerName, Address, Bio) VALUES (N'李四', N'北京市朝阳区', N'这是一段中文简介'); -- 不加 N 前缀,在非 Unicode 字符集下会丢字符 INSERT INTO Customer (CustomerName, Address) VALUES ('王五', '上海市浦东新区'); -- 有风险N'李四'里的N表示这个字符串是 Unicode 常量。如果漏了N,SQL Server 会先按当前数据库的默认字符集解析,再转成NVARCHAR存储,中间可能丢字符。这个坑在跨库迁移时特别常见,血泪经验就是:只要目标列是NVARCHAR,插入值一律加N。
3. 日期、数字与货币类型:范围、精度和那个 +4 字节
3.1 Datetime 与 Smalldatetime 的范围差异
日期时间类型资料里列了两种:Datetime和Smalldatetime。Datetime存储范围从 1753 年 1 月 1 日到 9999 年 12 月 31 日,每个值 8 个字节,最小时间单位是 3.33 毫秒(资料里写"百分之三秒")。Smalldatetime范围从 1900 年 1 月 1 日到 2079 年 12 月 31 日,每个值 4 个字节,最小时间单位是分钟。
这个范围差异不是随便定的。Datetime的起点 1753 年跟英国采用格里高利历的时间有关,SQL Server 沿用了这个传统。Smalldatetime省了一半空间,但精度只到分钟,而且 2079 年之后就不能用了。
选型上,如果只是记录创建时间、更新时间,Smalldatetime够用且省空间;但如果要记录精确到毫秒的操作日志、金融交易时间,必须用Datetime。现在 SQL Server 还有Datetime2、Date、Time等更细的类型,但这份资料聚焦在经典类型上,先把这两个吃透。
日期格式可以通过SET DATEFORMAT调整,有效参数包括 MDY、DMY、YMD、YDM、MYD、DYM,默认是 MDY。这个设置影响的是字符串转日期时的解析顺序,比如'01/02/2024'在 MDY 下是 1 月 2 日,在 DMY 下是 2 月 1 日。
-- 设置日期格式为年月日 SET DATEFORMAT YMD; -- 建表:日志时间用 datetime,生日用 smalldatetime CREATE TABLE UserAction ( ActionId INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL, ActionTime DATETIME NOT NULL, -- 精确到毫秒 BirthDate SMALLDATETIME NULL -- 生日,分钟精度足够 ); -- 插入时用明确格式,避免受 DATEFORMAT 影响 INSERT INTO UserAction (UserId, ActionTime, BirthDate) VALUES (1001, '2024-06-15T10:30:00.000', '1990-01-01T00:00:00');'2024-06-15T10:30:00.000'这种 ISO 8601 格式不受DATEFORMAT影响,是最安全的写法。我一般会要求团队统一用这种格式,避免不同会话的DATEFORMAT设置导致解析结果不一致。
3.2 Int、Smallint、Tinyint 的取值范围
整数类型三个:Int、Smallint、Tinyint。资料里给了明确范围:Int从 -2,147,483,648 到 2,147,483,647,4 字节;Smallint从 -32,768 到 32,767,2 字节;Tinyint从 0 到 255,1 字节。
选型逻辑很直接:先估算字段可能的最大值,再选能覆盖的最小类型。比如年龄用Tinyint就够,0 到 255 覆盖所有人;端口号用Smallint,最大 65535 但Smallint上限 32767 不够,得用Int;订单号、用户 ID 这种可能上亿的,必须Int甚至Bigint。
这里有个容易忽略的点:Tinyint没有负数,如果你要存温度差、库存调整量这种可能为负的值,不能用Tinyint。
-- 按取值范围选类型 CREATE TABLE Product ( ProductId INT NOT NULL PRIMARY KEY, -- 可能上亿,用 int StockQty SMALLINT NOT NULL DEFAULT 0, -- 库存,几万以内,smallint AgeLimit TINYINT NULL, -- 年龄限制,0-255,tinyint Price DECIMAL(10,2) NOT NULL -- 金额,精确小数 ); -- 插入边界值测试 INSERT INTO Product (ProductId, StockQty, AgeLimit, Price) VALUES (2147483647, 32767, 255, 99999999.99); -- 下面这条会报错:smallint 溢出 -- INSERT INTO Product (ProductId, StockQty, AgeLimit, Price) -- VALUES (1, 32768, 10, 1.00);DECIMAL(10,2)表示总共 10 位数字,其中 2 位是小数,能存的最大值是 99,999,999.99。金额字段必须用DECIMAL或NUMERIC,不能用FLOAT或REAL,否则就会出现开头说的对账差几分钱的问题。
3.3 Decimal、Numeric、Float、Real 的精度陷阱
精确小数用Decimal和Numeric,这两个是同义词,存储空间根据精度和小数位数确定。近似小数用Float和Real,资料里特别提醒:三分之一记作 0.3333333,用近似类型存储后,检索出来的数据可能跟存进去的不完全一样。
这就是浮点数的本质:它用二进制表示十进制小数,很多十进制小数在二进制里是无限循环的,只能截断。所以FLOAT适合科学计算、统计场景,不适合金额、数量这种要求精确的字段。
Money和Smallmoney是货币专用类型,Money8 字节,Smallmoney4 字节。资料里给了范围:Money从 -922,337,203,685,477.5808 到 922,337,203,685,477.5807,最小单位千分之十。Smallmoney从 -214,748.3648 到 214,748.3647。
那金额到底用Decimal还是Money?我的经验是:Decimal更通用,精度自己定,跨数据库迁移也方便;Money是 SQL Server 特有的,虽然运算快一点,但精度固定到万分之四,而且有些客户端工具显示格式不统一。新项目我一般用Decimal(18,4)或Decimal(19,4)。
-- 金额字段对比 CREATE TABLE Payment ( PaymentId INT IDENTITY(1,1) PRIMARY KEY, AmountDec DECIMAL(18,4) NOT NULL, -- 推荐:精度可控 AmountMoney MONEY NULL, -- SQL Server 特有 TaxRate FLOAT NULL -- 税率,允许近似 ); -- 插入并观察精度 INSERT INTO Payment (AmountDec, AmountMoney, TaxRate) VALUES (12345.6789, 12345.6789, 0.3333333); -- 查询:float 可能显示 0.3333333 但实际存储有微小偏差 SELECT AmountDec, AmountMoney, TaxRate, AmountDec * 2 AS DecDouble, TaxRate * 3 AS FloatTriple FROM Payment;AmountDec * 2结果是精确的 24691.3578,而TaxRate * 3可能显示 0.9999999 而不是 1。这就是精确类型和近似类型的区别。做财务系统时,所有参与计算的金额字段都必须是Decimal,中间变量也要用Decimal,不能中途转成Float。
4. 特殊类型与用户自定义类型:Timestamp、Bit、GUID 和 sp_addtype
4.1 Timestamp、Bit、Uniqueidentifier 的适用场景
特殊类型资料里列了三种:Timestamp、Bit、Uniqueidentifier。
Timestamp不是日期时间,它表示 SQL Server 活动的先后顺序,以二进制格式表示,跟插入的日期时间没有关系。每次更新行,Timestamp值会自动变化。它常被用来做乐观锁:读取时拿到Timestamp值,更新时带上这个值作为条件,如果期间被别的事务改过,Timestamp变了,更新影响行数为 0,应用就知道发生了并发冲突。
Bit由 1 或 0 组成,表示真/假、ON/OFF。适合存布尔状态,比如"是否已删除""是否启用"。注意Bit类型在 SQL Server 里不能为 NULL 时默认是 0,但允许 NULL 时会有三种状态:0、1、NULL。
Uniqueidentifier是 16 字节的十六进制数字,表示全局唯一标识符 GUID。资料里说当表的记录行要求唯一时,GUID 非常有用,比如客户标识号列。GUID 的优点是可以在应用层生成,不依赖数据库自增,适合分布式系统;缺点是 16 字节比Int的 4 字节大很多,而且作为聚集索引时会导致页分裂和碎片。
-- 乐观锁 + 布尔状态 + GUID 主键 CREATE TABLE Document ( DocId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() PRIMARY KEY, Title NVARCHAR(200) NOT NULL, IsDeleted BIT NOT NULL DEFAULT 0, RowVersion TIMESTAMP NOT NULL, -- 乐观锁版本 CreatedAt DATETIME NOT NULL DEFAULT GETDATE() ); -- 插入 INSERT INTO Document (Title) VALUES (N'测试文档'); -- 读取时拿到 RowVersion SELECT DocId, Title, RowVersion FROM Document WHERE Title = N'测试文档'; -- 更新时带上 RowVersion 做乐观锁 UPDATE Document SET Title = N'更新后的标题' WHERE DocId = '...' AND RowVersion = 0x00000000000007D1; -- 如果影响行数为 0,说明期间被改过NEWID()生成随机 GUID,NEWSEQUENTIALID()生成有序 GUID,后者作为聚集索引时性能更好。如果非要用 GUID 做主键,我一般会加一个自增的Int列做聚集索引,GUID 列做唯一非聚集索引。
4.2 用 sp_addtype 创建用户自定义类型
用户自定义类型基于系统提供的数据类型。当几个表要存同一种数据,并且要保证这些列有相同的数据类型、长度和可空性时,可以定义一次,多处复用。资料里给了语法:
sp_addtype {type}, [,system_data_type][, 'null_type']type是自定义类型名,system_data_type是系统类型,null_type是空值处理方式,必须用单引号引起来,比如'NULL'、'NOT NULL'或'NONULL'。
资料里的例子是创建一个ssn类型,基于Varchar(11),不允许空:
USE cust; EXEC sp_addtype ssn, 'Varchar(11)', 'NOT NULL';再创建一个birthday类型,基于DateTime,允许空:
USE cust; EXEC sp_addtype birthday, datetime, 'NULL';还可以一次创建多个:
USE master; EXEC sp_addtype telephone, 'varchar(24)', 'NOT NULL'; EXEC sp_addtype fax, 'varchar(24)', 'NULL';删除自定义类型用sp_droptype:
USE master; EXEC sp_droptype 'ssn';注意资料里的提醒:当表中的列还在使用这个自定义类型,或者上面绑定了默认值或规则时,不能删除。必须先改掉所有引用它的列,或者先解绑默认值和规则。
-- 完整流程:创建、使用、删除自定义类型 USE cust; GO -- 1. 创建自定义类型 EXEC sp_addtype ssn, 'Varchar(11)', 'NOT NULL'; EXEC sp_addtype birthday, datetime, 'NULL'; GO -- 2. 在表中使用 CREATE TABLE Person ( PersonId INT IDENTITY(1,1) PRIMARY KEY, Ssn ssn NOT NULL, BirthDate birthday NULL ); GO -- 3. 插入数据 INSERT INTO Person (Ssn, BirthDate) VALUES ('12345678901', '1990-01-01'); GO -- 4. 删除自定义类型前,先确认没有列在用 -- 下面这条会报错,因为 Person.Ssn 还在用 ssn 类型 -- EXEC sp_droptype 'ssn'; -- 5. 先改列类型,再删除 ALTER TABLE Person ALTER COLUMN Ssn VARCHAR(11) NOT NULL; GO EXEC sp_droptype 'ssn'; GO自定义类型的好处是统一约束,比如所有存身份证的列都必须是Varchar(11)且非空,改的时候改一处就行。但缺点也很明显:很多 ORM 框架和客户端工具对自定义类型支持不好,迁移到别的数据库时也不兼容。我一般只在同一个库内、团队约定明确的情况下用,跨库项目会直接用系统类型加约束。
5. 类型选型验证与迁移排查:几个能救命的检查习惯
5.1 用系统视图反查字段类型和长度
建完表之后,怎么确认字段类型真的符合预期?我习惯用INFORMATION_SCHEMA.COLUMNS查一遍,重点看DATA_TYPE、CHARACTER_MAXIMUM_LENGTH、NUMERIC_PRECISION、NUMERIC_SCALE、IS_NULLABLE这几列。
-- 查指定表的字段类型详情 SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH AS CharMaxLen, NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Product' ORDER BY ORDINAL_POSITION;这个查询能帮你发现几个常见问题:VARCHAR长度是不是写成了MAX导致无法建索引;DECIMAL的精度和小数位是不是符合业务要求;NVARCHAR是不是漏了N前缀导致实际存的是非 Unicode。我一般会在建表脚本执行后跑一遍这个查询,跟设计文档对一遍。
5.2 类型不匹配导致的隐式转换排查
隐式转换是性能杀手。比如VARCHAR列跟NVARCHAR参数比较,SQL Server 会把VARCHAR列转成NVARCHAR,导致索引失效,全表扫描。排查方法是看执行计划里的CONVERT_IMPLICIT警告。
-- 假设 CustomerName 是 NVARCHAR,下面这条会触发隐式转换 SELECT * FROM Customer WHERE CustomerName = '张三'; -- 漏了 N -- 正确写法 SELECT * FROM Customer WHERE CustomerName = N'张三'; -- 查看执行计划中的隐式转换 SET SHOWPLAN_ALL ON; GO SELECT * FROM Customer WHERE CustomerName = '张三'; GO SET SHOWPLAN_ALL OFF; GO执行计划里如果出现CONVERT_IMPLICIT,就说明发生了隐式转换。解决办法是让参数类型跟列类型一致:NVARCHAR列用N'...',VARCHAR列用'...',INT列不要传字符串。
5.3 改字段类型的正确姿势
改字段类型不是ALTER COLUMN一句话就完事。如果表里已有数据,新类型范围比旧类型小,或者精度不够,就会报错。我一般按这个顺序走:
-- 1. 先查现有数据的最大长度和范围 SELECT MAX(LEN(Remark)) AS MaxLen, MAX(Amount) AS MaxAmount, MIN(Amount) AS MinAmount FROM Payment; -- 2. 确认新类型能覆盖 -- 如果 MaxLen 是 500,就不能改成 VARCHAR(100) -- 3. 加一个新列,把数据迁过去 ALTER TABLE Payment ADD AmountNew DECIMAL(19,4) NULL; GO UPDATE Payment SET AmountNew = CAST(Amount AS DECIMAL(19,4)); GO -- 4. 验证数据一致 SELECT COUNT(*) FROM Payment WHERE AmountNew <> Amount; GO -- 5. 删旧列,改新列名 ALTER TABLE Payment DROP COLUMN Amount; GO EXEC sp_rename 'Payment.AmountNew', 'Amount', 'COLUMN'; GO这个流程比直接ALTER COLUMN慢,但安全。直接改类型如果失败,可能锁表很久,甚至回滚不了。加新列、迁数据、验证、替换,每一步都可控,出问题也能回退。
注意:
sp_rename改列名后,依赖这个列的视图、存储过程、函数可能失效,需要重新编译或重建。改名前先用sys.sql_expression_dependencies查一下依赖关系。
5.4 迁移时字符集和排序规则的坑
跨库迁移时,源库和目标库的排序规则可能不同。VARCHAR列在排序规则不同的库之间迁移,可能出现乱码或比较结果不一致。NVARCHAR列不受排序规则影响字符存储,但比较时仍受排序规则影响。
-- 查数据库排序规则 SELECT DATABASEPROPERTYEX('YourDB', 'Collation') AS DBColllation; -- 查列级排序规则 SELECT COLUMN_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Customer' AND COLLATION_NAME IS NOT NULL;如果迁移后发现中文乱码,先查源列和目标列的排序规则。常见做法是迁移前统一排序规则,或者把VARCHAR列转成NVARCHAR再迁。我一般会在迁移脚本里加一步:把所有含中文的VARCHAR列先ALTER成NVARCHAR,迁完再按需改回。
5.5 一个验证类型选型的小脚本
最后分享一个我常用的检查脚本,建完表后跑一遍,把可疑字段列出来:
-- 检查可能选错类型的字段 SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE FROM INFORMATION_SCHEMA.COLUMNS WHERE -- 金额字段用了 float/real (COLUMN_NAME LIKE '%Amount%' OR COLUMN_NAME LIKE '%Price%' OR COLUMN_NAME LIKE '%Money%') AND DATA_TYPE IN ('float', 'real') -- 中文字段用了 varchar OR (COLUMN_NAME LIKE '%Name%' OR COLUMN_NAME LIKE '%Address%' OR COLUMN_NAME LIKE '%Title%') AND DATA_TYPE = 'varchar' -- 长度用了 max 但可能不需要 OR CHARACTER_MAXIMUM_LENGTH = -1 ORDER BY TABLE_NAME, COLUMN_NAME;这个脚本会列出金额用了浮点、中文用了非 Unicode、长度用了MAX的字段。每次建完表跑一遍,能提前发现大部分类型选型问题。从那以后我每次建表或改表,都强制走一遍这个检查,再也没出现过上线后改字段类型的事故。希望帮到你。
本文还有配套的精品资源,点击获取