数据类型与类型亲和性
本教程共 50 篇 · 第 8 篇 · 更新于 2026-07-31
08. 数据类型与类型亲和性
本节目标:学完本章你能说清 SQLite 为什么是「动态类型 + 类型亲和性」、把任意一列的值正确归到 5 种存储类之一、看懂
typeof()返回的结果、并且再也不会误以为 SQLite「有 DATE 类型」或「是弱类型数据库」。
先交代一个背景:SQLite 是 serverless(无服务进程)的嵌入式数据库,数据库就是磁盘上一个普通的文件,不需要启动什么服务、不需要用户名密码登录。这种「零配置、单文件」的特质,决定了它的类型设计也要尽量省心、尽量不挑刺——这正是本章要讲的「动态类型」之所以存在的根本原因。理解了这一点,下面的内容就顺理成章了。
很多刚从 MySQL、PostgreSQL 转来 SQLite 的朋友,上来第一反应就是:「SQLite 支持哪些数据类型?VARCHAR 多长?DATETIME 怎么定义?」如果你也这么想,说明你被「静态类型数据库」的思维先入为主了。这一章,我们就把这个最容易踩、也最容易被教程带偏的点彻底掰正。
一、最大的误区:SQLite 没有你以为的那种「数据类型」
在 MySQL、PostgreSQL、SQL Server 这些传统数据库里,类型是「跟容器走的」。你建表时声明 age INT,那么这一列就被钉死成整数,你塞进去一个字符串 '20',数据库会当场报错(或者按严格规则转换)。值的类型由「它待在哪个列」决定——这叫静态类型(rigid typing)。
SQLite 不一样。在 SQLite 里,值的类型跟值本身走,不跟容器走。你往 age 列塞一个字符串 '20',SQLite 通常不会报错,而是把它存成文本;你往同一列塞一个数字 20,它就存成整数。换句话说,同一个列里,不同行的同一列可以是不同的存储类型。这就是 动态类型系统(dynamic typing)。
常见坑
别再到处找「SQLite 的 DATE / TIME / DATETIME / TIMESTAMP 类型」了——SQLite 根本就没有这四种类型。官方的存储类只有 5 种(见第三节),里面没有任何日期/时间专用类型。很多二手教程(包括部分 runoob、tutorialspoint 文章)会列出含 DATE 的「数据类型清单」,那是照搬静态类型数据库的写法,对 SQLite 是错误表述。日期一律当成普通值存:用 TEXT 存 ISO8601 字符串、用 REAL 存儒略日、用 INTEGER 存 Unix 秒,再由内置日期函数负责解析。具体见第七节。
需要强调:动态类型不是「SQLite 偷懒」,而是它的设计特性(feature),而且它刻意保持和静态类型 SQL 语句的兼容——你写 CREATE TABLE t(a INT, b VARCHAR(10)) 照样能跑,只是含义和静态类型库不同。
二、动态类型系统:值自带类型,列只是「建议」
为了更直观,把 SQLite 的表想象成一个「大抽屉柜」:在静态类型数据库里,每个抽屉都贴着标签「只放整数」「只放文本」,放错了就不收;在 SQLite 里,抽屉上没有硬性标签,你爱放什么放什么,但每个抽屉有一个「偏好」——你放东西时,它会按偏好尽量帮你归置一下。这个「偏好」,就是下面要讲的类型亲和性(Type Affinity)。
关键点有三个,请刻进脑子里:
- 声明列时写的类型(如
VARCHAR(255)、INTEGER)只是一个「亲和性建议」,不是强制约束。 你写INTEGER不表示这一列只能装整数。 - 值真正以哪种存储类落盘,由值本身决定。 你塞数字它就存整数/浮点,塞文字就存文本。
- 任何列(除了
INTEGER PRIMARY KEY这个特例)都能装下任意存储类的值。INTEGER PRIMARY KEY是唯一被钉死成整数、且作为 ROWID 别名的列,后面「自增主键」章节会专门讲。
这跟「弱类型语言」也完全不同。弱类型通常指「类型之间随意隐式转换、经常丢信息」,而 SQLite 是有清晰规则的——它只是「不强制容器类型」,但每个值自身始终有一个明确的存储类,绝不模糊。所以准确说法是:SQLite 是动态类型(dynamic typing),不是弱类型(weak typing)。
三、五种存储类(Storage Class)
「存储类」这个词,指的是一个值最终在 SQLite 里以什么形式被保存和运算。SQLite 全部的值,逃不出下面这 5 种:
- NULL:空值,表示「没有数据」。注意它和空字符串
''、和数字 0 都不是一回事——NULL 就是「什么都没有」。 - INTEGER:有符号整数。SQLite 会很聪明地按数值大小用 1、2、3、4、6 或 8 字节存储,越小越省空间。取值范围是 64 位有符号整数。
- REAL:浮点数,固定用 8 字节的 IEEE 754 双精度格式存储。凡是带小数点、需要精度的数(金额、温度、比例)多用它。
- TEXT:文本字符串。以数据库的编码(一般是 UTF-8)保存。你写的名字、邮箱、地址都属于这一类。
- BLOB:二进制大对象(Binary Large Object)。原样存储你塞进去的字节串,不做任何转换。图片、压缩包、序列化后的数据可以放这里。
重点提示
SQLite 没有独立的布尔(Boolean)存储类。布尔值用整数表示:
0代表假(false),1代表真(true)。从 3.23.0 起,关键字TRUE和FALSE也只是整数1和0的「另一种写法」。所以判断真假时,直接当整数处理即可,别去找BOOLEAN类型。
另外,「存储类」比「数据类型」更宽泛:比如 INTEGER 这一个存储类,底层其实包含了 7 种不同长度的整数表示,但一旦读进内存运算,都会统一成 8 字节整数。所以在日常使用时,「存储类」和「数据类型」基本可以混着说。
四、类型亲和性(Type Affinity):为什么还能写 VARCHAR
既然类型是动态的,那为什么建表时还能写 VARCHAR(255)、DECIMAL(10,5) 这些「静态类型库里的名字」?答案就是类型亲和性。
SQLite 引入亲和性,是为了和别的 SQL 数据库「兼容」:你从 MySQL 抄来的建表语句,在 SQLite 里也能跑,而且行为尽量一致。亲和性(affinity)就是「这一列偏好用哪种存储类来存」。记住一句话——亲和性是建议,不是要求;任何列仍然能存任何类型的值,只是有偏好之分。
SQLite 给每一列分配的亲和性,只有这 5 种:TEXT、NUMERIC、INTEGER、REAL、BLOB。它们怎么从你写的「声明类型」推断出来?规则如下(按顺序匹配,命中即停):
- 声明类型里包含字符串
INT(不分大小写)→ 亲和性为 INTEGER。例如INT、INTEGER、TINYINT、BIGINT都归它。 - 声明类型里包含
CHAR、CLOB或TEXT任一字符串 → 亲和性为 TEXT。所以VARCHAR含CHAR,NVARCHAR含CHAR,全都是 TEXT 亲和性。 - 声明类型里包含
BLOB,或者根本没写类型 → 亲和性为 BLOB(即「不偏好任何类型,原样存」)。 - 声明类型里包含
REAL、FLOA(FLOAT 的前缀)或DOUB(DOUBLE 的前缀)→ 亲和性为 REAL。 - 以上都不命中 → 亲和性为 NUMERIC。典型的
DECIMAL、BOOLEAN、DATE、DATETIME都落在这里,注意它们都不是真正的日期/小数专用类型,只是 NUMERIC 亲和性。
有两点容易栽跟头:规则的顺序很重要。比如 CHARINT 同时命中规则 1 和规则 2,但规则 1 优先,所以它是 INTEGER 亲和性。再比如 FLOATING POINT 因为末尾有 INT,会被判成 INTEGER 而非 REAL——这就是「前缀匹配」的副作用,写类型名时别自创。
下面这张表列出了常见声明类型最终对应的亲和性,你能看到 SQLite「来者不拒」却又「各有归处」:
| 你写的声明类型 | 实际亲和性 |
|---|---|
| INT、INTEGER、TINYINT、BIGINT、INT2、INT8 | INTEGER |
| CHARACTER(20)、VARCHAR(255)、TEXT、CLOB、NCHAR(55) | TEXT |
| BLOB、或不写类型 | BLOB |
| REAL、DOUBLE、FLOAT、DOUBLE PRECISION | REAL |
| NUMERIC、DECIMAL(10,5)、BOOLEAN、DATE、DATETIME | NUMERIC |
实用技巧
既然类型名后面括号里的长度(如
VARCHAR(255))在 SQLite 里完全被忽略——SQLite 不对字符串、BLOB、数字做长度限制(只受全局SQLITE_MAX_LENGTH约束),那日常建表时就别写长度了,直接写TEXT、INTEGER、REAL最清爽,也最不容易误导后来读代码的人以为「超长会截断」。
五、typeof() 函数:看清值真正的存储类
想知道一个值到底以哪种存储类存着?用内置函数 typeof() 一查便知。它返回 'null'、'integer'、'real'、'text'、'blob' 之一。这是动态类型下你最该掌握的「照妖镜」。
沿用教程统一的示例表 users,我们建表并插入不同类型的值来验证:
sqlite> CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER, email TEXT);
sqlite> INSERT INTO users(id, name, age, email) VALUES(1, '张三', 28, 'zhang@ex.com');
sqlite> INSERT INTO users(id, name, age, email) VALUES(2, '李四', '30', 'li@ex.com');
sqlite> SELECT name, age, typeof(age) FROM users;
张三|28|integer
李四|30|text
注意第二行:我们往 age(INTEGER 亲和性)列塞的是字符串 '30',typeof(age) 返回的是 text——因为字符串字面量本就是文本,亲和性只是「偏好」,并不会强行把 '30' 转成整数(转换只在特定比较/运算时发生,详见下节)。这正说明:列声明的是亲和性,值自身携带的存储类才是真相。
六、亲和性如何「悄悄」改变你插入的值
亲和性虽然不强制,但它会在插入时按偏好做「尽量转换」。下面这个官方经典示例把 5 种亲和性摆在一起,最直观:
sqlite> CREATE TABLE t1(
...> t TEXT, -- 规则2:TEXT 亲和性
...> nu NUMERIC, -- 规则5:NUMERIC 亲和性
...> i INTEGER, -- 规则1:INTEGER 亲和性
...> r REAL, -- 规则4:REAL 亲和性
...> no BLOB -- 规则3:BLOB 亲和性,原样存
...> );
sqlite> INSERT INTO t1 VALUES('500.0', '500.0', '500.0', '500.0', '500.0');
sqlite> SELECT typeof(t), typeof(nu), typeof(i), typeof(r), typeof(no) FROM t1;
text|integer|integer|real|text
解读一下这一行结果:插入的全是字符串 '500.0',但各列「按自己偏好」动了手——
- TEXT 列:本来就是文本,原样存
text。 - NUMERIC 列:字符串
'500.0'是个「长得像整数」的字面量,于是被转成整数integer(500.0 没小数部分,归整数)。 - INTEGER 列:同样转成
integer。 - REAL 列:偏好浮点,于是转成
real。 - BLOB 列:不偏好任何类型,原样存
text。
再换插入浮点 500.0 和数字 500,typeof 又会不同:REAL 列始终是 real,BLOB 列对浮点存 real、对整数存 integer(因为 BLOB 亲和性「原样」,浮点字面量本来就是 real,整数本来就是 integer)。而 NULL 和 BLOB 字面量(如 x'0500')永远不受亲和性影响——NULL 存 null,BLOB 存 blob。
这里还有个细节:NUMERIC 亲和性下,像 '3.0e+5' 这种「本质是整数」的浮点字面量,会被存成整数 300000 而非 300000.0;但 REAL 亲和性不会做这种取整,它会保留浮点。所以「同一串字符,放在不同亲和性的列里,落盘类型可能不同」——这就是亲和性的真正威力,也是动态类型下最容易被忽略的行为。
七、日期到底怎么存:TEXT / REAL / INTEGER
回到开头那个 WARNING。SQLite 没有日期类型,但日期时间数据照样能存,方式有三种,由内置日期函数负责解析与互转:
- TEXT:按 ISO8601 字符串存,如
"2026-07-31 14:30:00.000"。可读性最好,排序也直观(字典序即时间序),最推荐日常使用。 - REAL:存儒略日(Julian day)——从公元前 4714 年 11 月 24 日格林尼治正午起算的天数,可带小数表示一天内的时刻。适合做跨日期的浮点运算。
- INTEGER:存 Unix 时间——从
1970-01-01 00:00:00 UTC起算的秒数(或毫秒数,看你约定)。最适合做时间戳、做差值计算。
sqlite> SELECT date('now'), julianday('now'), strftime('%s','now');
2026-07-31|2469227.1042|1785465600
上面一行分别演示了:用 date() 取今天的 TEXT 形式、用 julianday() 取儒略日 REAL 值、用 strftime('%s',...) 取 Unix 秒 INTEGER 值。三种形式之间可以用这些函数自由转换,选哪种纯看你的业务偏好——重点是:它们全都是普通的整数/浮点/文本,没有任何「日期类型」参与。
重点提示
把「日期类型误区」和「自增主键误区」一起记牢:正如 SQLite 没有 DATE 类型,它也不需要
AUTOINCREMENT关键字来让主键自增。直接写id INTEGER PRIMARY KEY,SQLite 就会让 id 作为 ROWID 的别名自动递增(详见第 12 章)。AUTOINCREMENT是可选的、有额外开销的「防 ROWID 复用」增强,绝不是默认写法。
八、为什么 SQLite 要这么设计,以及使用注意
动态类型 + 类型亲和性这套组合,给 SQLite 带来三个实在好处:第一,容错强——你从别的库导来的「脏数据」(数字写成字符串)不会一插入就崩,SQLite 先收下再说;第二,兼容好——照搬 MySQL 的建表语句基本能跑,迁移成本低;第三,省心——不用在建表时纠结「这个字段到底该定多长、定什么精确类型」。
但自由也带来责任,给你三条使用建议:
- 靠约定,不靠强制。 既然列不拦你乱存,团队里就要约定好「
age列只放整数、email放文本」,并在应用层做校验。需要硬性约束时,用CHECK约束或严格表(STRICT tables,3.37.0 起支持)来兜底。 - 别被「看起来像数字」骗了。 两个都显示
500的值,一个可能是integer、一个可能是text,比较和排序时行为不同(文本500比整数40「大」还是「小」要看亲和性转换)。存进去之前想清楚类型,必要时用CAST显式转换。 - 查询时用
typeof()自查。 当排序、分组、比较结果「不对劲」时,先SELECT typeof(列) ...看真实存储类,往往能定位问题。
最后做个类比收尾:把 SQLite 的列想象成「分类垃圾桶」——桶上写着「建议放塑料」,但你真扔个易拉罐进去它也不会弹回来(动态类型);只是下次清运时,它会按「建议」尽量把易拉罐归类到金属堆(类型亲和性)。而桶里每一件垃圾真实是什么材质(存储类),永远由垃圾自己决定,查一眼就知道。理解了「容器只建议、值自带类型」,你就算真正入门了 SQLite 的类型哲学,下一章我们就去看这 5 种存储类在磁盘和内存里是怎么安排的。