列约束与表约束
本教程共 50 篇 · 第 11 篇 · 更新于 2026-07-31
11. 列约束与表约束
本节目标:学完本章你能区分列级约束和表级约束,会用 NOT NULL、UNIQUE、CHECK、PRIMARY KEY 给表立规矩,并知道什么约束只能写在列上、什么约束能跨多列。
上一章我们把表的”骨架”搭起来了,但骨架只是形状,还没有”规矩”。比如:用户名能不能为空?邮箱能不能重复?年龄能不能写负数?这些规矩在 SQLite 里靠**约束(constraint)**来表达。约束是建表时写在列定义或表定义里的规则,数据插入或更新时 SQLite 会自动检查,违规就拒绝并抛 SQLITE_CONSTRAINT 错误。约束的价值很简单:让脏数据在进门之前就被拦下,而不是等程序跑飞了才去查”怎么有空名字”。
两种摆放位置:列级 vs 表级
约束可以出现在两个地方,这是本章第一个要分清的概念:
- 列级约束(column constraint):紧跟在某一列的声明类型后面,只管这一列。例如
name TEXT NOT NULL。 - 表级约束(table constraint):写在所有列定义之后、括号收尾之前,独立成行,可以管多列组合。例如
PRIMARY KEY(id, seq)或UNIQUE(col_a, col_b)。
绝大多数约束两种写法都行,但有例外:NOT NULL 只能写成列级约束;而需要跨多列唯一或跨多列作主键时,只能用表级约束。下面逐个看。
NOT NULL:这一列不许空
NOT NULL 规定该列必须有值,插入或更新时给 NULL 直接报错。回想第 09 章:SQLite 的列默认都允许 NULL,所以”必填”必须显式声明。
CREATE TABLE users(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER,
email TEXT
);
如果试图塞一个空名字:
sqlite> INSERT INTO users(name) VALUES(NULL);
Error: NOT NULL constraint failed: users.name
Note重点提示:标准 SQL 里主键列应当隐含 NOT NULL,但 SQLite 出于历史兼容,允许普通主键列出现 NULL(除非是
INTEGER PRIMARY KEY或WITHOUT ROWID表)。所以别依赖隐含规则,该写 NOT NULL 就写,规矩写清楚最稳。
PRIMARY KEY:行的唯一身份证
主键(primary key)是唯一标识一行数据的列(或列组合)。一张表有且只有一个主键。它有两个含义:值不能重复、且通常不可空。
单列的写法(列级):
CREATE TABLE users(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
如果主键由多列组成(比如”用户 id + 序号”才能唯一确定一行),就必须用表级写法:
CREATE TABLE order_items(
user_id INTEGER,
seq INTEGER,
note TEXT,
PRIMARY KEY(user_id, seq)
);
这里 (user_id, seq) 合起来才是主键,单独一个 user_id 重复没关系。注意:关于 INTEGER PRIMARY KEY 为什么特殊、为什么它自带”自增”——留到第 12 章专门讲,那是 SQLite 最容易被误解的点之一。
UNIQUE:值不能重复,但能多个空
UNIQUE 约束保证某列(或某几列组合)的值彼此不重复。典型用途:邮箱、手机号、用户名这类”业务上唯一”的字段。
单列唯一(列级):
CREATE TABLE users(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
重复插入同一个邮箱会被拦:
sqlite> INSERT INTO users(name, email) VALUES('张三','a@b.com');
sqlite> INSERT INTO users(name, email) VALUES('李四','a@b.com');
Error: UNIQUE constraint failed: users.email
多列组合唯一(表级):
CREATE TABLE seats(
id INTEGER PRIMARY KEY,
row TEXT,
col TEXT,
UNIQUE(row, col)
);
这样 (row, col) 这一对不能重复,相当于”一个座位只能卖一次票”。
Tip实用技巧:UNIQUE 和主键的区别在于——一张表只能有一个主键,但可以有多个 UNIQUE 列;并且 UNIQUE 允许出现多个 NULL(SQLite 认为两个 NULL 互不相等),而主键列不允许 NULL。所以”允许空、但不允许重复非空值”的场景,用 UNIQUE 而不是主键。
CHECK:自定义的业务校验
CHECK 是最灵活的约束:你写一个表达式,插入/更新时只要表达式结果为”非 0 且非 NULL”就通过,结果为 0(假)就拒绝。它用来表达”年龄必须 ≥ 0""折扣不能高于原价”这类业务规则。
列级写法,要求电话至少 10 个字符:
CREATE TABLE contacts(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT NOT NULL CHECK(length(phone) >= 10)
);
表级写法,要求折扣不超过原价且两者都非负:
CREATE TABLE products(
id INTEGER PRIMARY KEY,
list_price REAL NOT NULL,
discount REAL NOT NULL DEFAULT 0,
CHECK(list_price >= discount AND discount >= 0 AND list_price >= 0)
);
插入一个折扣高于原价的商品会被拦:
sqlite> INSERT INTO products(list_price, discount) VALUES(900, 1000);
Error: CHECK constraint failed: products
Note重点提示:CHECK 表达式里不能写子查询(subquery),只能用当前行的列和函数做判断。另外 SQLite 不支持用
ALTER TABLE给已存在的表直接追加表级 CHECK 约束,要加这类约束通常只能新建表、搬数据、再改名(这是 ALTER TABLE 的局限,后续章节会讲)。
约束还能起名字(CONSTRAINT 关键字)
当报错信息只说 UNIQUE constraint failed: users.email 时其实够用了,但大型项目里你可能想给约束起个名字,方便维护和定位。用 CONSTRAINT 名字 前缀即可,表级和列级都支持:
CREATE TABLE users(
id INTEGER PRIMARY KEY,
email TEXT,
CONSTRAINT uq_users_email UNIQUE(email)
);
起名后报错会带上 uq_users_email,一眼知道是哪条规矩炸了。
约束失败会怎样
所有约束违规的结果是一致的:SQLite 中止当前这条语句(默认是 ABORT 策略),什么也不写入,并返回 SQLITE_CONSTRAINT 类错误。换句话说,约束是”原子级守门员”——一条脏数据想进门,整条插入/更新直接作废,不会只存一半。这对保持数据干净非常关键。
把四类约束组合到一张真实表
学完单个约束,来看它们如何在一张表里”合体”。给 users 同时上规矩:主键 id、名字必填、邮箱唯一且必填、年龄非负:
CREATE TABLE users(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER CHECK(age >= 0),
email TEXT NOT NULL UNIQUE
);
插入一条合法数据没问题;下面几种都会当场被拦:
sqlite> INSERT INTO users(name, age, email) VALUES('张三', -5, 'a@b.com');
Error: CHECK constraint failed: users
sqlite> INSERT INTO users(name, email) VALUES('李四', 'a@b.com');
Error: UNIQUE constraint failed: users.email
sqlite> INSERT INTO users(id, email) VALUES(1, 'b@b.com');
Error: NOT NULL constraint failed: users.name
注意不同约束的报错信息会点名是哪一种(CHECK / UNIQUE / NOT NULL),方便你定位是哪条规矩炸了。一张表可以同时挂多个 UNIQUE、多个 CHECK,但主键只有一个——这正是”身份证只能有一张,但手机号、邮箱可以各自唯一”的现实映射。
类比小结
把约束想成”收纳柜的使用须知”:NOT NULL 是”这个格子必须放东西,不许空”;PRIMARY KEY 是”每件物品贴的唯一条形码,扫得出身份”;UNIQUE 是”这类标签全柜只能出现一次”;CHECK 是”不符合规格(比如太短、负数)直接拒收”。列级须知贴在单个格子上,表级须知贴在柜门上管”几个格子组合”的规矩。规矩立得早,后面查数据才省心——这正是”约束前置”的价值。
Warning常见坑:约束只在”插入/更新时”检查,对已经存在的旧脏数据不会自动清洗。如果一张老表原本没加 NOT NULL,后来你
ALTER TABLE加约束,表里既有的 NULL 行并不会被自动改掉——新插入才会被拦。迁移老数据时记得先手工清理历史脏数据。