☰
SQL数据类型详解:从建表选型到索引失效的避坑指南
2026/10/11 19:29:01 网站建设 项目流程

简介:这份《SQL数据类型详解》PDF文档面向数据库初学者与SQL开发人员,系统梳理SQL Server中各类数据类型的定义、取值范围与适用场景,帮助读者在建模与建表时准确选型、避免存储与精度问题。资源共1个PDF文件,压缩包约71KB,内容以文字讲解为主,便于随时查阅与打印。文档将数据类型划分为二进制、字符、Unicode、日期时间、数字、货币及特殊类型等类别,逐一说明Binary与Varbinary的定长变长差异、Char与Varchar的8KB边界、Nchar与Nvarchar的Unicode双倍存储代价、Datetime与Smalldatetime的日期范围,以及Int、Smallint、Tinyint的字节占用与数值区间,并补充Decimal、Numeric、Float、Real、Money、Timestamp、Bit、Uniqueidentifier等类型的要点。目前已有614人学习,适合作为SQL入门与日常开发的速查参考。

1. SQL 数据类型详解:为什么你建的表总在半夜报长度溢出

线上告警在凌晨两点炸开,日志里一行Data too long for column 'phone'格外刺眼。翻建表语句,phone varchar(11),存手机号明明够用,可上游把带国际区号的号码塞了进来,13 位直接顶穿。这不是个例,SQL 数据类型选错,轻则隐式转换拖慢查询,重则截断数据、精度丢失、索引失效。所谓 SQL 数据类型详解,讲的不是把 INT、VARCHAR、DECIMAL 背一遍,而是搞清楚每种类型在存储、比较、运算、索引四个环节的真实行为,以及建表时怎么选、迁移时怎么转、出问题时怎么查。这篇面向的是天天写 DDL、跑慢 SQL、做数据迁移的开发和 DBA,从选型逻辑一路讲到踩坑排查,新手能照着建表,熟手能对着参数抠边界。

2. 建表前先想清楚:整数、字符串、时间到底怎么选

2.1 整数类型不是越大越好,宽度和范围是两回事

很多人建表习惯性写INT(11),以为 11 是能存 11 位数字,其实在 MySQL 里括号里的数字只是显示宽度,配合ZEROFILL才有意义,跟存储范围毫无关系。真正决定能存多大的是类型本身:TINYINT占 1 字节,范围 -128 到 127;SMALLINT2 字节;MEDIUMINT3 字节;INT4 字节;BIGINT8 字节。选型的核心就一句话:按业务上限留一倍余量,别用 BIGINT 存状态码,也别用 TINYINT 存用户 ID。

我一般这样定:状态、类型、性别这类枚举值用TINYINT;订单号、自增主键看增长速度,日增百万级用BIGINT稳妥;金额绝对不用浮点,后面单独讲。无符号UNSIGNED能把正数范围翻倍,但一旦业务出现负数(比如退款金额为负),改类型就是一次全表重建,所以除非确定永不为负,否则别轻易加。

-- 推荐:状态用 TINYINT,主键用 BIGINT UNSIGNED CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待付 1已付 2取消', amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_created (user_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 DDL 里,BIGINT UNSIGNED给主键留足增长空间,TINYINT存状态省空间,DECIMAL(12,2)保证金额精确到分且最大支持百亿级,utf8mb4是为了存 emoji 和生僻字。索引idx_user_created把用户和时间放一起,方便按用户查最近订单。参数上,DECIMAL(12,2)的 12 是总位数、2 是小数位,改小数位会触发精度重算,上线前定死。

2.2 字符串类型:CHAR、VARCHAR、TEXT 的边界在哪

CHAR定长,存不足会补空格,读取时又去掉,适合长度固定的编码,比如 MD5 值 32 位、国家代码 2 位。VARCHAR变长,只存实际长度加 1 到 2 字节长度前缀,是绝大多数场景的默认选择。TEXT系列用于大文本,但它不能有默认值、排序时可能走磁盘临时表、索引只能前缀索引。

一个高频翻车点:VARCHAR(255)不是万能。在 utf8mb4 下,255 个字符最多占 1020 字节,加上长度前缀,单列就接近 1KB。InnoDB 单行上限约 65535 字节(不含 TEXT/BLOB 的溢出页),如果一张表塞十几个VARCHAR(255),建表直接报Row size too large。我的经验是:能预估长度的就写死,比如手机号VARCHAR(20)、邮箱VARCHAR(100)、昵称VARCHAR(64);实在不确定再用VARCHAR(255),但别滥用。

-- 反例:一行塞太多大 VARCHAR,容易触发行大小限制 CREATE TABLE bad_example ( a VARCHAR(255), b VARCHAR(255), c VARCHAR(255), d VARCHAR(255), e VARCHAR(255), f VARCHAR(255), g VARCHAR(255), h VARCHAR(255), i VARCHAR(255) ); -- 报错:Row size too large. Change some columns to TEXT or BLOB.

遇到这个报错,解决办法不是无脑改 TEXT,而是先问:这些字段真的都要 255 吗?把能收窄的收窄,把确实超长的改成TEXT并接受它不能建普通索引的限制。TEXT要索引就建前缀,比如KEY idx_title (title(20)),但前缀索引的选择性会下降,查询时可能回表变多。

2.3 时间类型:DATETIME 和 TIMESTAMP 的时区陷阱

DATETIME存的是字面值,范围 1000 到 9999 年,占 8 字节(MySQL 5.6 后可指定精度);TIMESTAMP存的是 UTC 时间戳,范围只到 2038 年,占 4 字节,写入读取会随时区转换。选哪个取决于你要不要时区感知。跨时区业务用TIMESTAMP省心,但 2038 问题必须提前规划;单时区业务用DATETIME更直观,也不受时区配置影响。

-- 跨时区场景:TIMESTAMP 自动按会话时区转换 CREATE TABLE events ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, event_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ); SET time_zone = '+00:00'; INSERT INTO events (event_time) VALUES ('2024-01-01 12:00:00'); SET time_zone = '+08:00'; SELECT event_time FROM events; -- 显示 2024-01-01 20:00:00

上面演示了TIMESTAMP的时区转换行为:同一份数据,换个会话时区读出来就不一样。如果你要的是「不管在哪读都是同一个墙上时间」,那就用DATETIME。另外DATETIME和TIMESTAMP都支持小数秒,DATETIME(3)存毫秒,但精度越高占字节越多,日志类表用秒级就够,别为了好看全上毫秒。

3. 精度、转换与索引:那些让慢 SQL 现原形的类型细节

3.1 金额为什么必须用 DECIMAL 而不是 FLOAT

浮点数用二进制表示小数,很多十进制小数根本存不下精确值。FLOAT和DOUBLE做金额运算,累加几次就会出现0.1 + 0.2 = 0.30000000000000004这种结果,对账时差一分钱能查一整天。DECIMAL是定点数,按十进制存储,DECIMAL(12,2)就是精确到分。

-- 对比:FLOAT 累加出现误差,DECIMAL 精确 CREATE TABLE money_test ( f FLOAT, d DECIMAL(12,2) ); INSERT INTO money_test VALUES (0.1, 0.10), (0.2, 0.20); SELECT SUM(f), SUM(d) FROM money_test; -- SUM(f) = 0.30000001192092896 -- SUM(d) = 0.30

参数上,DECIMAL(M,D)的 M 是总位数、D 是小数位,M 最大 65,D 最大 30。金额一般DECIMAL(12,2)够用,涉及汇率用DECIMAL(16,6)。注意DECIMAL运算比整数慢,但金额场景这点开销换来的正确性完全值得。如果历史表已经用了FLOAT,迁移时用ALTER TABLE ... MODIFY col DECIMAL(12,2),但要先SELECT检查有没有已经失真的值,别把错误数据一起搬过去。

3.2 隐式类型转换:索引失效的隐形杀手

这是慢 SQL 优化里最容易被忽略的一条。当查询条件里的类型和列类型不一致,数据库会做隐式转换,而转换方向决定了索引还能不能用。规则是:字符串列和数字比较,字符串列会被转成数字,索引失效;数字列和字符串比较,字符串被转成数字,索引仍可用。

-- phone 是 VARCHAR,下面这条会让索引失效 SELECT * FROM users WHERE phone = 13800138000; -- 正确写法:加引号,保持类型一致 SELECT * FROM users WHERE phone = '13800138000';

第一条语句里,phone列被逐行转成数字再比较,idx_phone用不上,全表扫描。第二条保持字符串比较,索引正常走。用EXPLAIN一看便知:type从ALL变成ref,key从NULL变成idx_phone。养成习惯,字符串列的条件永远带引号,日期列用'2024-01-01'而不是数字。

3.3 类型转换函数与迁移时的安全改法

显式转换用CAST或CONVERT,跨数据库写法略有差异。MySQL 里CAST(x AS SIGNED)转整数,CAST(x AS CHAR)转字符串,CONVERT(x, DATETIME)转时间。SQL Server 用CONVERT(VARCHAR, x)或TRY_CAST。迁移或清洗数据时,转换失败会直接报错或返回 NULL,所以要先探测脏数据。

-- 迁移前先找出无法转成整数的脏数据 SELECT id, raw_value FROM staging WHERE raw_value NOT REGEXP '^-?[0-9]+$'; -- 确认干净后再改列类型 ALTER TABLE staging MODIFY raw_value BIGINT;

第一步用正则筛出非数字行,避免ALTER时因为个别脏值失败回滚。第二步改类型,大表上ALTER会锁表或重建,生产环境建议用pt-online-schema-change或gh-ost这类工具在线改。参数上,MODIFY只改类型不改名,CHANGE可以顺便改名,别混用。改完记得重新ANALYZE TABLE更新统计信息,否则执行计划可能还是旧的。

4. 避坑与排查:数据类型相关的 5 个高频翻车现场

4.1 现象:插入报 Data too long,原因:长度按字符算但没算字节

现象是插入中文或 emoji 时报Data too long for column,明明字符数没超。原因是VARCHAR(n)的 n 是字符数,但存储按字节算,utf8mb4 下一个 emoji 占 4 字节,VARCHAR(10)存 3 个 emoji 就可能顶到上限附近。解决:建表统一utf8mb4,长度按最坏情况估算,昵称这类给到VARCHAR(64),别卡着VARCHAR(10)省那点空间。

4.2 现象:金额对不上,原因:用了 FLOAT/DOUBLE 存钱

现象是对账差几分钱,或者SUM结果带一长串小数。原因就是浮点精度。解决:金额列一律DECIMAL,应用层也用BigDecimal或对应的高精度类型,别用double接。已经上线的表,先备份再改类型,改完跑一遍对账脚本验证。

4.3 现象:查询突然变慢,原因:隐式转换让索引失效

现象是某条查询昨天还快,今天全表扫描。原因往往是条件类型和列类型不匹配,或者参数由字符串变成了数字。解决:用EXPLAIN看type和key,确认索引是否命中;检查应用层传参类型,字符串列别传数字。这条在慢 SQL 优化里排查优先级很高,因为改动成本极低。

4.4 现象:TIMESTAMP 列读出时间不对,原因:时区配置不一致

现象是同一行数据,不同客户端读出来差 8 小时。原因是TIMESTAMP会按会话时区转换,而各连接时区设置不同。解决:统一用DATETIME存业务时间,或者全局固定time_zone并在应用层统一处理时区。用TIMESTAMP就要接受它的转换语义,别指望它当字面值用。

4.5 现象:改列类型后应用报错,原因:驱动映射和精度变化

现象是ALTER把INT改成BIGINT或把VARCHAR改TEXT后,应用反序列化失败。原因是 ORM 或驱动对类型的映射变了,比如TEXT在某些框架里映射成流而不是字符串。解决:改类型前评估应用层映射,改完在预发环境跑一遍核心链路,别直接上生产。

5. 进阶技巧:用 information_schema 给全库数据类型做一次体检

数据类型选错往往不是一次性的,而是历史遗留慢慢积累。与其等告警,不如定期给全库做一次体检。核心思路是查information_schema.COLUMNS,把可疑类型揪出来:金额列用了FLOAT/DOUBLE、状态列用了VARCHAR、时间列用了TIMESTAMP且临近 2038、字符串列长度过大等。

-- 找出所有用浮点存金额嫌疑的列(列名含 amount/price/money) SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE DATA_TYPE IN ('float','double') AND (COLUMN_NAME LIKE '%amount%' OR COLUMN_NAME LIKE '%price%' OR COLUMN_NAME LIKE '%money%'); -- 找出长度超过 200 的 VARCHAR,评估是否该收窄或改 TEXT SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS WHERE DATA_TYPE = 'varchar' AND CHARACTER_MAXIMUM_LENGTH > 200 ORDER BY CHARACTER_MAXIMUM_LENGTH DESC;

第一条查浮点金额列,命中就列入改造清单,按第 3 章的DECIMAL方案迁移。第二条查超长VARCHAR,结合业务判断是真需要还是当初偷懒写大了。体检脚本可以做成定时任务,每周跑一次,输出到报表。参数上,information_schema在 MySQL 8.0 之后性能好了很多,但大实例上仍建议加TABLE_SCHEMA限定,别全库扫。

再补一个验证方法:改完类型后,用CHECKSUM TABLE或对比改前改后的行数和抽样值,确认数据没被截断或失真。我自己的习惯是,任何ALTER之前先CREATE TABLE ... AS SELECT备份一份,改完抽样比对,确认无误再删备份。数据类型这事,后悔药就是备份,别嫌麻烦。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询