首页 / SQLite 入门教程 / 数据类型与类型亲和性

SQLite 入门教程

数据类型与类型亲和性

本教程共 50 篇 · 第 8 篇 · 更新于 2026-07-31

sqlite数据类型类型亲和性存储类动态类型

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)

关键点有三个,请刻进脑子里:

  1. 声明列时写的类型(如 VARCHAR(255)INTEGER)只是一个「亲和性建议」,不是强制约束。 你写 INTEGER 不表示这一列只能装整数。
  2. 值真正以哪种存储类落盘,由值本身决定。 你塞数字它就存整数/浮点,塞文字就存文本。
  3. 任何列(除了 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 起,关键字 TRUEFALSE 也只是整数 10 的「另一种写法」。所以判断真假时,直接当整数处理即可,别去找 BOOLEAN 类型。

另外,「存储类」比「数据类型」更宽泛:比如 INTEGER 这一个存储类,底层其实包含了 7 种不同长度的整数表示,但一旦读进内存运算,都会统一成 8 字节整数。所以在日常使用时,「存储类」和「数据类型」基本可以混着说。

四、类型亲和性(Type Affinity):为什么还能写 VARCHAR

既然类型是动态的,那为什么建表时还能写 VARCHAR(255)DECIMAL(10,5) 这些「静态类型库里的名字」?答案就是类型亲和性

SQLite 引入亲和性,是为了和别的 SQL 数据库「兼容」:你从 MySQL 抄来的建表语句,在 SQLite 里也能跑,而且行为尽量一致。亲和性(affinity)就是「这一列偏好用哪种存储类来存」。记住一句话——亲和性是建议,不是要求;任何列仍然能存任何类型的值,只是有偏好之分。

SQLite 给每一列分配的亲和性,只有这 5 种:TEXT、NUMERIC、INTEGER、REAL、BLOB。它们怎么从你写的「声明类型」推断出来?规则如下(按顺序匹配,命中即停):

  1. 声明类型里包含字符串 INT(不分大小写)→ 亲和性为 INTEGER。例如 INTINTEGERTINYINTBIGINT 都归它。
  2. 声明类型里包含 CHARCLOBTEXT 任一字符串 → 亲和性为 TEXT。所以 VARCHARCHARNVARCHARCHAR,全都是 TEXT 亲和性。
  3. 声明类型里包含 BLOB,或者根本没写类型 → 亲和性为 BLOB(即「不偏好任何类型,原样存」)。
  4. 声明类型里包含 REALFLOA(FLOAT 的前缀)或 DOUB(DOUBLE 的前缀)→ 亲和性为 REAL
  5. 以上都不命中 → 亲和性为 NUMERIC。典型的 DECIMALBOOLEANDATEDATETIME 都落在这里,注意它们都不是真正的日期/小数专用类型,只是 NUMERIC 亲和性

有两点容易栽跟头:规则的顺序很重要。比如 CHARINT 同时命中规则 1 和规则 2,但规则 1 优先,所以它是 INTEGER 亲和性。再比如 FLOATING POINT 因为末尾有 INT,会被判成 INTEGER 而非 REAL——这就是「前缀匹配」的副作用,写类型名时别自创。

下面这张表列出了常见声明类型最终对应的亲和性,你能看到 SQLite「来者不拒」却又「各有归处」:

你写的声明类型实际亲和性
INT、INTEGER、TINYINT、BIGINT、INT2、INT8INTEGER
CHARACTER(20)、VARCHAR(255)、TEXT、CLOB、NCHAR(55)TEXT
BLOB、或不写类型BLOB
REAL、DOUBLE、FLOAT、DOUBLE PRECISIONREAL
NUMERIC、DECIMAL(10,5)、BOOLEAN、DATE、DATETIMENUMERIC

实用技巧

既然类型名后面括号里的长度(如 VARCHAR(255))在 SQLite 里完全被忽略——SQLite 不对字符串、BLOB、数字做长度限制(只受全局 SQLITE_MAX_LENGTH 约束),那日常建表时就别写长度了,直接写 TEXTINTEGERREAL 最清爽,也最不容易误导后来读代码的人以为「超长会截断」。

五、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 和数字 500typeof 又会不同: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 的建表语句基本能跑,迁移成本低;第三,省心——不用在建表时纠结「这个字段到底该定多长、定什么精确类型」。

但自由也带来责任,给你三条使用建议:

  1. 靠约定,不靠强制。 既然列不拦你乱存,团队里就要约定好「age 列只放整数、email 放文本」,并在应用层做校验。需要硬性约束时,用 CHECK 约束或严格表(STRICT tables,3.37.0 起支持)来兜底。
  2. 别被「看起来像数字」骗了。 两个都显示 500 的值,一个可能是 integer、一个可能是 text,比较和排序时行为不同(文本 500 比整数 40「大」还是「小」要看亲和性转换)。存进去之前想清楚类型,必要时用 CAST 显式转换。
  3. 查询时用 typeof() 自查。 当排序、分组、比较结果「不对劲」时,先 SELECT typeof(列) ... 看真实存储类,往往能定位问题。

最后做个类比收尾:把 SQLite 的列想象成「分类垃圾桶」——桶上写着「建议放塑料」,但你真扔个易拉罐进去它也不会弹回来(动态类型);只是下次清运时,它会按「建议」尽量把易拉罐归类到金属堆(类型亲和性)。而桶里每一件垃圾真实是什么材质(存储类),永远由垃圾自己决定,查一眼就知道。理解了「容器只建议、值自带类型」,你就算真正入门了 SQLite 的类型哲学,下一章我们就去看这 5 种存储类在磁盘和内存里是怎么安排的。