简介:这是一份面向数据库初学者与SQL开发人员的SQL数据类型系统梳理资料,聚焦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. SQL 数据类型详解:为什么你写的索引没生效,问题可能出在类型上
很多人第一次认真看 SQL 数据类型,是在一次线上事故之后。我印象最深的一次,是订单表按user_id查一条记录,明明建了索引,EXPLAIN却显示全表扫描。查了半天,发现user_id在表里是VARCHAR,而应用传进来的参数是整型,数据库做了一次隐式类型转换,索引直接失效。这类问题不是玄学,是数据类型没吃透。
SQL 数据类型详解这件事,表面看是背一张对照表,实际解决的是三类问题:存储该选什么类型才不浪费空间、查询时类型不匹配为什么让索引失效、跨库迁移和 ETL 时类型怎么映射才不丢精度。它适合后端开发、数据开发和做数据清洗的工程师——只要你写过CREATE TABLE,或者被慢 SQL 优化折磨过,这篇就值得往下看。下面按「选型 → 落地 → 踩坑 → 进阶」的顺序讲透。
2. 整数、字符串、时间三大类的选型逻辑与建表落地
2.1 整数类型:从 TINYINT 到 BIGINT 到底怎么选
整数类型的选择核心就一句话:按业务真实取值范围选,别一律INT或BIGINT。常见做法是,状态、类型这种枚举值用TINYINT(1 字节,-128~127 或 0~255),用户 ID、订单 ID 这种会持续增长的用BIGINT(8 字节),中间量级的计数用INT(4 字节)。
这里有个容易被忽略的点:INT UNSIGNED和INT的取值范围不同。无符号INT是 0~4294967295,有符号是 -2147483648~2147483647。如果你的自增主键永远为正,用UNSIGNED能多出一倍空间,但要注意跨库迁移时某些数据库对无符号支持不一致,可能翻车。
-- 建一张用户表,演示整数类型的合理选型 CREATE TABLE t_user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键,持续增长用 BIGINT', status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态枚举 0-255', age TINYINT UNSIGNED DEFAULT NULL COMMENT '年龄 0-255 足够', login_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '登录次数,中等量级', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:id用BIGINT UNSIGNED是因为自增主键迟早会突破INT上限,提前留余量比后期改表代价小得多。status和age用TINYINT UNSIGNED,一个字节搞定,比INT省 3 字节,行数上亿时差距明显。login_count用INT UNSIGNED,因为登录次数不太可能超过 42 亿。
参数说明:AUTO_INCREMENT只对整数类型生效;UNSIGNED会改变取值范围,迁移到不支持无符号的库时要评估;DEFAULT给默认值能避免NULL带来的三值逻辑问题。
2.2 字符串类型:CHAR、VARCHAR、TEXT 的边界在哪
字符串选型最常见的错误是无脑用VARCHAR(255)。CHAR是定长,适合长度固定的场景,比如 MD5 值(32 位)、国家代码(2 位);VARCHAR是变长,适合长度不固定的名称、地址;TEXT适合大段文本,但它不能有默认值,且排序时可能用到磁盘临时表。
一个关键细节:VARCHAR(n)里的 n 是字符数不是字节数。在utf8mb4下,一个汉字占 3~4 字节,所以VARCHAR(255)最大可能占 1020 字节。索引长度限制和这个直接相关——MySQL InnoDB 单列索引前缀默认最多 767 字节(老版本)或 3072 字节,VARCHAR(255)在utf8mb4下建完整索引可能超限。
-- 字符串类型选型演示 CREATE TABLE t_product ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, sku_code CHAR(32) NOT NULL COMMENT '固定长度编码,用 CHAR', product_name VARCHAR(128) NOT NULL COMMENT '名称变长,用 VARCHAR', description TEXT COMMENT '详情大文本,用 TEXT', PRIMARY KEY (id), KEY idx_sku (sku_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:sku_code长度固定,用CHAR(32)比VARCHAR少一个长度字节,且定长检索略快。product_name用VARCHAR(128),128 个字符对商品名足够,别动不动 255。description用TEXT,因为详情可能几千字,放VARCHAR会撑大行长度。
参数说明:CHAR会去掉尾部空格,存密码哈希这类不能丢空格的场景要小心;VARCHAR要按业务上限设,设太大浪费内存临时表;TEXT不能建普通索引,需要索引时得用前缀索引或额外字段。
2.3 时间类型:DATETIME、TIMESTAMP、DATE 怎么分工
时间类型选错,最典型的后果是时区问题。TIMESTAMP存储时转 UTC,读取时转当前时区,范围只到 2038 年;DATETIME存字面值,范围到 9999 年,但不带时区信息。常见做法是:需要跨时区展示的用TIMESTAMP,只记录业务本地时间的用DATETIME,只关心日期的用DATE。
-- 时间类型选型演示 CREATE TABLE t_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,业务本地时间', updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间,自动维护', order_date DATE NOT NULL COMMENT '下单日期,只到天', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:created_at用DATETIME记录业务时间,不受时区转换影响。updated_at用TIMESTAMP配合ON UPDATE CURRENT_TIMESTAMP,让数据库自动维护更新时间,省去应用层代码。order_date用DATE,按天统计时不用做函数转换,能直接走索引。
参数说明:CURRENT_TIMESTAMP是默认值表达式;ON UPDATE只在行真正变化时触发;TIMESTAMP的 2038 问题在长期系统里要提前规划,必要时全用DATETIME。
3. 类型转换、隐式转换与索引失效的排查实战
3.1 隐式类型转换为什么让索引失效
这是慢 SQL 优化里最高频的坑之一。当查询条件里的类型和列类型不一致时,数据库会做隐式转换。规则是:字符串列和数字比较时,字符串被转成数字。一旦列被函数包裹或发生转换,索引就用不上了。
-- 假设 user_id 是 VARCHAR 类型 -- 下面这条会全表扫描,因为 '123' 被转成数字,列也参与转换 SELECT * FROM t_user WHERE user_id = 123; -- 正确写法:参数类型和列类型一致 SELECT * FROM t_user WHERE user_id = '123';逻辑说明:第一条语句里,user_id是字符串列,123是数字,MySQL 会把user_id转成数字再比较,相当于对列做了函数操作,索引失效。第二条保持类型一致,索引正常生效。
参数说明:排查时用EXPLAIN看type列,出现ALL就是全表扫描;key列为NULL说明没走索引。应用层传参时统一类型,别让框架自动转换。
3.2 显式转换函数 CAST 和 CONVERT 的用法
有时候确实需要转换,比如把字符串日期转成日期比较。这时用显式转换,并且尽量把函数用在常量侧而不是列侧。
-- 把字符串转日期,函数用在常量侧,列侧保持原样 SELECT * FROM t_order WHERE created_at >= CAST('2024-01-01' AS DATETIME); -- CONVERT 写法,适合跨库兼容 SELECT * FROM t_order WHERE created_at >= CONVERT('2024-01-01', DATETIME);逻辑说明:CAST和CONVERT都能做类型转换,CONVERT还支持字符集转换。关键是把转换放在常量上,这样列created_at不被包裹,索引依然可用。
参数说明:CAST(expr AS type)里 type 支持DATETIME、DATE、SIGNED、CHAR等;CONVERT(expr, type)或CONVERT(expr USING charset)。跨库迁移时优先用标准CAST。
3.3 用 EXPLAIN 定位类型相关性能问题
排查类型导致的性能问题,EXPLAIN是第一工具。重点看三列:type(访问类型)、key(实际用的索引)、rows(预估扫描行数)。
EXPLAIN SELECT * FROM t_user WHERE user_id = 123; EXPLAIN SELECT * FROM t_user WHERE user_id = '123';逻辑说明:对比两条EXPLAIN结果,第一条type大概率是ALL,key为NULL;第二条type是ref或const,key显示索引名。这就是类型一致性的价值。
参数说明:type从好到差是system > const > eq_ref > ref > range > index > ALL;rows越小越好;如果Extra出现Using filesort或Using temporary,也要结合类型一起看。
4. 跨库迁移与数据清洗中的类型映射避坑
4.1 不同数据库类型映射对照
跨库迁移时,类型映射是最容易丢精度的地方。下面这张表是常见映射关系,迁移前务必逐列核对。
| 语义 | MySQL | PostgreSQL | Oracle | SQL Server |
|---|---|---|---|---|
| 小整数 | TINYINT | SMALLINT | NUMBER(3) | TINYINT |
| 整数 | INT | INTEGER | NUMBER(10) | INT |
| 长整数 | BIGINT | BIGINT | NUMBER(19) | BIGINT |
| 变长字符串 | VARCHAR | VARCHAR | VARCHAR2 | VARCHAR |
| 大文本 | TEXT | TEXT | CLOB | NVARCHAR(MAX) |
| 时间戳 | DATETIME | TIMESTAMP | TIMESTAMP | DATETIME2 |
| 布尔 | TINYINT(1) | BOOLEAN | NUMBER(1) | BIT |
逻辑说明:MySQL 没有原生布尔,常用TINYINT(1)模拟;Oracle 的NUMBER要指定精度,否则可能按浮点处理;SQL Server 的DATETIME精度只有 3.33 毫秒,要更高精度用DATETIME2。
参数说明:迁移前用information_schema.columns导出源库类型清单,和目标库逐列比对;字符串长度要按目标库字符集重新计算,utf8mb4下长度可能翻倍。
4.2 迁移时精度丢失的三种典型场景
第一种是浮点转定点。源库用FLOAT存金额,目标库用DECIMAL,转换时可能出现0.1 + 0.2 != 0.3的精度问题。金额一律用DECIMAL(m,2),别用浮点。
第二种是时间精度截断。源库TIMESTAMP(6)带微秒,目标库DATETIME只到秒,微秒直接丢。迁移前确认业务是否依赖微秒。
第三种是字符集不兼容。源库latin1存了中文,迁到utf8mb4时乱码。迁移前统一字符集,用CONVERT ... USING utf8mb4做转换。
-- 迁移前检查源库列类型和字符集 SELECT column_name, data_type, character_maximum_length, character_set_name FROM information_schema.columns WHERE table_schema = 'source_db' AND table_name = 't_order'; -- 金额字段统一用 DECIMAL ALTER TABLE t_order MODIFY amount DECIMAL(12,2) NOT NULL DEFAULT 0.00;逻辑说明:第一条查源库列定义,确认字符集和长度。第二条把金额改成DECIMAL(12,2),12 位总长度、2 位小数,能存到百亿级,精度不丢。
参数说明:DECIMAL(m,d)里 m 是总位数,d 是小数位;character_set_name为NULL表示非字符类型;迁移脚本里对每个字符列都要显式指定目标字符集。
4.3 数据清洗中的类型统一策略
数据清洗时,源数据往往类型混乱,比如日期列里混了'2024/01/01'、'2024-01-01'、'20240101'三种格式。策略是先统一成标准格式,再转目标类型。
-- 清洗日期列:先替换分隔符,再转 DATE UPDATE t_raw SET order_date = STR_TO_DATE( REPLACE(REPLACE(order_date_str, '/', '-'), '.', '-'), '%Y-%m-%d' ) WHERE order_date_str IS NOT NULL;逻辑说明:REPLACE把/和.统一成-,再用STR_TO_DATE按%Y-%m-%d解析。清洗后列类型改成DATE,后续查询才能走索引。
参数说明:STR_TO_DATE的格式串要和数据实际格式匹配,不匹配返回NULL;清洗前先备份原列,用WHERE限定非空,避免把NULL转成'0000-00-00'。
5. 类型相关的常见问题与排查清单
5.1 插入报错「Data too long for column」怎么定位
现象:插入或更新时报Data too long for column 'xxx',业务中断。
原因:目标列长度不够,常见于VARCHAR设太短,或者字符集从utf8换成utf8mb4后字节数变大。
解决:先查列定义SHOW COLUMNS FROM t_table LIKE 'xxx',确认Type里的长度;再查实际数据长度SELECT MAX(CHAR_LENGTH(col)) FROM t_table;按业务上限扩列,别直接改TEXT,扩到合理长度即可。
5.2 金额字段用 FLOAT 导致对账差几分钱
现象:订单金额和对账系统差 0.01 元,反复核对找不到原因。
原因:FLOAT和DOUBLE是近似值存储,累加时误差累积。
解决:金额一律用DECIMAL(m,2),Java 侧用BigDecimal,Python 侧用decimal.Decimal,别用float。已存在的FLOAT列用ALTER TABLE ... MODIFY amount DECIMAL(12,2)迁移,迁移前备份。
5.3 时间字段存了 '0000-00-00' 导致查询异常
现象:查询时间范围时结果不对,或者应用解析时间报错。
原因:老版本 MySQL 允许'0000-00-00'作为默认值,或者STR_TO_DATE解析失败返回零值。
解决:先查SELECT COUNT(*) FROM t WHERE created_at = '0000-00-00';把零值更新为NULL或合理默认值;建表时设NO_ZERO_DATE模式,从源头禁止。
5.4 隐式转换让唯一索引失效导致重复数据
现象:明明建了唯一索引,还是插入了重复数据。
原因:唯一索引列是VARCHAR,插入时传了数字,隐式转换后'123'和123被当成不同值,或者转换规则导致判断异常。
解决:应用层统一传字符串;检查已有数据SELECT col, COUNT(*) FROM t GROUP BY col HAVING COUNT(*) > 1;必要时用CAST统一后再建唯一索引。
5.5 跨库迁移后字符串尾部空格丢失
现象:迁移后密码哈希校验失败,或者编码比对不一致。
原因:源库用CHAR,CHAR会去掉尾部空格;目标库用VARCHAR,空格被保留,导致值不一致。
解决:迁移前确认源列是CHAR还是VARCHAR;对不能丢空格的字段改用VARCHAR或BINARY;迁移后用SELECT LENGTH(col) - CHAR_LENGTH(col)检查尾部空格差异。
6. 用类型信息反推表结构设计:一个可复用的检查脚本
前面讲了选型和踩坑,最后落到一个我平时常用的技巧:写一个脚本,把库里所有列的类型、长度、是否可空、索引情况导出来,做一次体检。这个习惯帮我提前发现过好几次隐患,比如某张表金额列是FLOAT、某列VARCHAR(255)在utf8mb4下索引超限。
-- 表结构体检:导出列类型、长度、可空、索引情况 SELECT t.table_name, c.column_name, c.data_type, c.character_maximum_length, c.numeric_precision, c.numeric_scale, c.is_nullable, c.column_default, CASE WHEN s.index_name IS NOT NULL THEN 'YES' ELSE 'NO' END AS has_index FROM information_schema.tables t JOIN information_schema.columns c ON t.table_name = c.table_name AND t.table_schema = c.table_schema LEFT JOIN information_schema.statistics s ON s.table_name = c.table_name AND s.column_name = c.column_name AND s.table_schema = c.table_schema WHERE t.table_schema = 'your_db' ORDER BY t.table_name, c.ordinal_position;逻辑说明:这条查询把列定义和索引信息拼在一起,一眼能看出哪些列类型可疑、哪些列没索引。numeric_precision和numeric_scale对DECIMAL列特别有用,能确认金额精度。has_index帮你快速定位该建索引却没建的列。
参数说明:table_schema换成你的库名;statistics表里同一列可能有多个索引,结果会重复,需要去重时加DISTINCT;character_maximum_length对非字符类型为NULL,判断时注意。
拿到这份清单后,我一般按三条规则过一遍:金额列不是DECIMAL的标红;VARCHAR长度超过 255 且建了索引的标黄;时间列是TIMESTAMP且业务要跨 2038 年的标黄。这套检查不复杂,但比出事后再改表省心得多。
血泪经验是,类型问题从来不是「建表时随便选选」的小事,它会在数据量上来、查询变复杂、跨库迁移时集中爆发。我现在建表前会先想清楚三件事:这列的取值范围、会不会参与索引、要不要跨库。想清楚再动手,比事后加后悔药强。希望帮到你。
本文还有配套的精品资源,点击获取