数值、字符、日期三大类,选型的顺序是「先保证装得下,再考虑省字节」。
char(n) 定长补空格、varchar(n) 变长;只有长度固定的字段才配用 char。
钱一律 decimal,不用 float——0.1 + 0.2 在浮点里等于 0.30000000000000004。
第 4 章建的是「库」,这一章建「表」。库是容器,表才是真正装数据的地方。
教材 5.1 只有三小节:基本概念、常用三大数据类型、数据类型的选择。
前两节是背诵内容,第三节才是要动脑的——选错的代价往往几个月后才显现。
演示里把三大类型逐个列了字节和范围,又真跑了一次 0.1 + 0.2。
还有一个必须提前说的差异:SQLite 是动态类型,往 int 列插字符串它不拦。
一、表、行、列:三个词要分清
关系库里,「表」是最小的数据容器。
一张表有若干列(字段),每列一个类型;有若干行(记录),每行是一条完整的数据。
这几句话教材上有,值得再抄一遍的理由是:后面所有的语法错误,八成是这三个概念的某一处搞混了。
「列的类型」是表的属性,「值」是行的属性。 所以表的类型信息只存一份,
每行只存值——这就是为什么一张两千万行的表,加一列不会让体积翻倍。
「行没有顺序」这件事必须记住。 关系库不保证查询结果的顺序,除非你写了 ORDER BY。
今天查出来是按主键排的,明天加了索引就可能变——这种「昨天还好好的」的问题,根源十有八九在这里。
二、数值类型:先看范围,再看字节
| 类型 | 字节 | 范围 | 典型用途 |
|---|---|---|---|
| tinyint | 1 | 0 ~ 255 | 状态位、年龄 |
| smallint | 2 | ±3.2 万 | 年份、数量 |
| int | 4 | 约 ±21 亿 | 主键、ID,最常用 |
| bigint | 8 | 约 ±9.2e18 | 流水号、时间戳 |
| decimal(p,s) | 5 ~ 17 | 精确小数 | 钱、单价 |
| float | 8 | 约 15 位有效 | 科学计算 |
选型的顺序是「先够用,再省字节」。 反过来做会出事:主键用 tinyint 省了 3 个字节,
写到第 256 行就插不进去了,而且那时候表里已经有数据,改类型要重写整张表。
int 是最常用的,因为它够大。 21 亿这个上限,一天一万条能写六千年。
真正需要 bigint 的场景是「流水号」——订单号、日志 ID 这种按毫秒累加的东西。
decimal(p,s) 的两个参数要理解:p 是总位数,s 是小数位数。
decimal(10,2) 表示「一共 10 位,其中 2 位是小数」,也就是最大 99999999.99。
金额字段一律这么写,不要用 float,理由下一段说。
三、字符类型:定长和变长的代价
| 类型 | 长度 | 存的时候 | 典型用途 |
|---|---|---|---|
| char(n) | 定长 n | 不足补空格 | 身份证、性别、地区码 |
| varchar(n) | 变长,最多 n | 按实际长度 | 姓名、地址 |
| nvarchar(n) | 变长 Unicode | 每字 2 字节 | 有中文的字段 |
| text | 大文本 | 不占行内 | 备注、文章 |
char(10) 存 'ab' 占 10 个字节,后面补 8 个空格。 这不只是浪费——补的空格在比较、拼接、
导出的时候都要额外处理,很容易出现「看着一样但不相等」。
所以只有长度真的固定的字段才用 char: 身份证 18 位、性别 1 位、地区编码 6 位、
统一社会信用代码 18 位。其余一律 varchar。
nvarchar 那个 n 是 national 的意思,指 Unicode。 每个字符占 2 字节,
所以 nvarchar(50) 最多能存 50 个汉字,而 varchar(50) 在 UTF-8 下大约只能存 16 个。
新库一律用 nvarchar。 用 varchar 存中文,在某些排序规则下会变成问号——这个坑很老了,
但每年还是有人踩。
四、日期类型:精度差在哪
| 类型 | 精度 | 占多少 | 什么时候用 |
|---|---|---|---|
| date | 只到天 | 3 字节 | 生日、账期 |
| time | 到 100 纳秒 | 5 字节 | 营业时间 |
| datetime | 到 3.33 毫秒 | 8 字节 | 老代码里到处都是 |
| datetime2 | 到 100 纳秒 | 6 ~ 8 字节 | 2008 之后推荐 |
datetime 的精度是 3.33 毫秒,不是 1 毫秒。 这个数很怪,
但它是真的——所以「相差 1 毫秒」的两条记录,用 datetime 存可能是同一个值。
datetime2 才是精确的。 2008 之后建的新表应该用它。
但教材里大量用 datetime,因为大量的存量系统就是那个习惯——看别人的库,先看它用的是哪个。
只存日期就用 date。 用 datetime 存生日,查出来会带着 00:00:00.000,
导出到 Excel 变成长长一串,然后有人手工去删——这种返工完全可以避免。
五、钱为什么不能用 float
演示里跑了一次 0.1 + 0.2:
float 结果是 0.30000000000000004
decimal 结果是 0.30
f == 0.3 吗:不是
round(d,2) == 0.3 吗:是
原因在二进制。 0.1 写成二进制是无限循环小数,float 只能截断存,
截断的那一点误差在加法里累积起来,就变成了 0.30000000000000004。
这个误差在钱上是致命的。 一万笔 0.1 元的累加,浮点可能算出 999.9999999999,
四舍五入到分是 1000.00 还行,但如果业务逻辑里有「大于等于 1000 就打折」,结果就反了。
所以钱一律 decimal(10,2)。 它是十进制定点,不会丢精度。
real 也不要用来存钱,它是单精度浮点,误差比 float 还大。
SQLite 没有真正的 decimal。 它只有 NUMERIC 亲和性,本质还是浮点加上转换。
所以上面那个 decimal 结果是手工 CAST 加截断造出来的——
SQL Server 的 decimal 是硬实现的,不会有这个问题。
六、必须提前说清的差异:SQLite 不查类型
演示里还有一段,值得单独提:往声明成 INT 的列插字符串,SQLite 不报错。
声明成 INT 的列插字符串:值=这不是数字 实际类型=text
声明成 TEXT 的列插数字: 值=123 实际类型=text
SQLite 的列类型只是「亲和性建议」,不是硬约束。 它按值自己判断类型,
所以「类型检查」这件事它根本不干。
SQL Server 完全相反:往 int 列插 'abc' 直接报转换失败,插 '123' 会自动转成 123。
这个差别意味着:本课所有靠数据库拦类型的演示,SQLite 只能演一半。
记住这一点,否则会得出错误结论。 比如「插错类型也能成功,
所以 SQL Server 不管类型」——恰恰相反,SQL Server 管得比谁都严。
七、动手 Demo:三大类型与 0.1 + 0.2
这一段把数值、字符、日期三类各列一张表,再真跑一次 0.1 + 0.2 看浮点和定点差多少,
最后演一次「SQLite 不拦类型」——这一点跟 SQL Server 完全相反。
import sqlite3
c = sqlite3.connect(u":memory:")
# 1) 浮点 vs 定点
c.execute(u"CREATE TABLE t (f REAL, d NUMERIC)")
c.execute(u"INSERT INTO t VALUES (0.1, 0.1)")
c.execute(u"UPDATE t SET f = f + 0.2, d = CAST(d + 0.2 AS NUMERIC)")
f, d = c.execute(u"SELECT f, d FROM t").fetchone()
print(u"float %.17f" % f)
print(u"定点 %.2f" % d)
print(u"f == 0.3 吗:%s" % (u"是" if f == 0.3 else u"不是"))
# 2) SQLite 不查类型
c.execute(u"CREATE TABLE ty (a INT, b TEXT)")
c.execute(u"INSERT INTO ty VALUES (?, 123)", (u"这不是数字",))
r = c.execute(u"SELECT a, typeof(a), b, typeof(b) FROM ty").fetchone()
print(u"INT 列存字符串:%s,实际类型 %s" % (r[0], r[1]))
print(u"TEXT 列存数字: %s,实际类型 %s" % (r[2], r[3]))
实跑输出:
--------------------------------------------------------------------
三大数据类型与怎么选
--------------------------------------------------------------------
① 数值类型:先看范围和字节
--------------------------------------------------------------------
| 类型 | 字节 | 范围 | 典型用途 |
|---|---|---|---|
| tinyint | 1 | 0 ~ 255 | 状态位、年龄 |
| smallint | 2 | -32768 ~ 32767 | 年份、数量 |
| int | 4 | 约 ±21 亿 | 主键、ID,最常用 |
| bigint | 8 | 约 ±9.2e18 | 流水号、时间戳 |
| decimal(p,s) | 5~17 | 精确小数 | 钱、单价 |
| float | 8 | 约 15 位有效数字 | 科学计算,不用于钱 |
选类型的顺序是「先够用,再省字节」。int 占 4 字节,bigint 占 8——
一个两千万行的表,主键从 int 换成 bigint,光主键就多 80 MB。
反过来,主键用 tinyint 省了字节,但 255 一到就写不进去了。
② 字符类型:定长和变长的差别
--------------------------------------------------------------------
| 类型 | 长度 | 存的时候 | 典型用途 |
|---|---|---|---|
| char(n) | 定长 n | 不足补空格 | 固定长度编码,如身份证号 |
| varchar(n) | 变长,最多 n | 按实际长度 | 姓名、地址,最常用 |
| nvarchar(n) | 变长 Unicode,最多 n 字 | 每字 2 字节 | 有中文的字段 |
| text | 大文本 | 不占行内空间 | 备注、文章 |
char(10) 存 'ab',占 10 个字节,后面补 8 个空格;
varchar(10) 存 'ab',占 2 个字节加一点长度开销。
所以只有「长度固定」的字段才用 char——身份证 18 位、性别 1 位、地区码 6 位。
其它一律 varchar,因为定长补的空格在比较时还要额外处理。
③ 日期类型:datetime 和 date 差在哪
--------------------------------------------------------------------
| 类型 | 精度 | 占多少 | 什么时候用 |
|---|---|---|---|
| date | 只到天 | 3 字节 | 生日、账期 |
| time | 到 100 纳秒 | 5 字节 | 营业时间 |
| datetime | 到 3.33 毫秒 | 8 字节 | 老代码里到处都是 |
| datetime2 | 到 100 纳秒 | 6~8 字节 | 2008 之后推荐的写法 |
datetime 的精度是 3.33 毫秒,不是 1 毫秒——所以它存不进 0.001 秒这种值。
datetime2 才是精确的。教材里大量用 datetime,因为很多老系统就那个习惯。
④ 钱为什么不能用 float
--------------------------------------------------------------------
0.1 + 0.2
float 结果是 0.30000000000000004
decimal 结果是 0.30
f == 0.3 吗:不是
round(d,2) == 0.3 吗:是
float 是二进制浮点,0.1 在二进制里是无限循环小数,存进去就被截断了。
所以钱一律用 decimal(10,2) 这种精确类型,不用 float,也不用 real。
SQLite 没有真正的 decimal,这里用 NUMERIC 加上手工截断来演示;
SQL Server 的 decimal 是硬实现的,不会有这个问题。
⑤ SQLite 是动态类型,这点和 SQL Server 不同
--------------------------------------------------------------------
声明成 INT 的列插字符串:值=这不是数字 实际类型=text
声明成 TEXT 的列插数字: 值=123 实际类型=text
SQLite 的列类型只是「建议」,值可以随便塞,它按值自己判断类型。
SQL Server 不是这样:往 int 列插 'abc' 直接报转换失败。
所以本课所有「类型检查」的演示,SQLite 只能演一半——它不拦你,这一点必须记住。
第一段那两行数就是整节的核心:
同一个 0.1 + 0.2,浮点给 0.30000000000000004,定点给 0.30。 误差只有 4e-17,
但在钱上它会累积。
f == 0.3 判出来是「不是」,这才是真正的问题——不是「显示得难看」,是「比较会失败」。
业务代码里写 if 金额 == 0.3 就永远不成立。
第二段那两行是与 SQL Server 的关键差异。 SQLite 的列类型只是亲和性建议,值可以随便塞;
SQL Server 会直接报「将 varchar 转换为 int 时失败」。
所以这一节的两张类型表,
要按 SQL Server 的规格记,不能按 SQLite 的行为记。SQLite 演的是「结构」,不是「约束」。
30 秒实验
- 在你自己的库上跑
SELECT 0.1 + 0.2,看返回什么。 SQL Server 里这行返回 0.3,因为字面量 0.1 被当成 decimal——但把它存进 float 列再取出来, 就不是了。 - 建一张表,
char(10)存'ab',然后SELECT LEN(列), DATALENGTH(列)。 两个数不一样:LEN 去掉尾空格算 2,DATALENGTH 算实际字节 10。 - 想一想:为什么
int是「最常用」而不是「够用就行」? 因为它 4 字节换 21 亿的范围,性价比最高。 用 smallint 省 2 字节,但写满就得改表。
常见误区
- 「类型随便选,反正能存进去」。能存进去和存对了是两件事。
用
varchar(20)存地址,超过 20 个字的会被截断或者报错,取决于设置。 而且这种错往往在数据进来半年后才被发现。 - 「float 也能存钱,只要四舍五入就行」。四舍五入能救显示,救不了比较。
WHERE 金额 = 0.3查不到的时候,你会怀疑数据,不会怀疑类型。 - 「char 比 varchar 快,所以都用 char」。只在长度真的固定时成立。 变长字段用 char,等于每行都浪费空间——一页装的行少了,查询反而变慢。
自测三问(点开对答案)
1. `decimal(10,2)` 最大能存多少? —— **99999999.99,也就是八位整数两位小数。** `p` 是总位数 10,`s` 是小数位 2,整数位就是 8 位。 **写金额时这个数要按业务估一下**,别等到「一亿的订单存不下」。 2. `0.1 + 0.2` 在 float 里为什么不等于 0.3? —— **因为 0.1 在二进制里是无限循环小数**,float 只能截断存,截断的误差在加法里累积。 定点类型(decimal)用十进制存,不经过二进制转换,所以不丢精度。 3. SQLite 往 INT 列插字符串为什么不报错? —— **因为 SQLite 是动态类型,列类型只是「亲和性建议」。** 它按值自己判断类型,不做强制检查。 **SQL Server 完全相反,会直接报类型转换失败**——这一点不能拿 SQLite 的行为去推 SQL Server。下一篇 5.2a 讲「在界面上建表」。 重点不是怎么点,是「点完之后生成的那条 SQL 长什么样」——看懂那条语句,才算真的会建表。
- ☐ (本篇完,下一篇 5.2a 以界面方式创建数据表)