SQLite以轻量著称,但它提供的约束机制并不简陋。主键、外键、唯一、非空和检查五类约束可以在数据写入前拦截大量异常,避免脏数据进入存储层。如果建表时忽略约束,后续清理数据的成本会成倍增加,因此约束设计应当前置。接下来从约束类型和实际语法出发,逐一说明SQLite的约束用法。

一、SQLite约束类型全景:哪些规则可以定义在表上
SQLite支持五类核心约束:PRIMARY KEY、NOT NULL、UNIQUE、CHECK和FOREIGN KEY。其中PRIMARY KEY隐含了NOT NULL和UNIQUE的语义,但并不是所有主键列都自动具备NOT NULL,只有整数主键在SQLite中有特殊行为。约束可以写在列定义后面,称为列级约束;也可以写在所有列定义之后,称为表级约束。表级约束主要用于复合主键、复合唯一键以及外键。
约束的执行时机是数据写入时,也就是INSERT或UPDATE语句执行的过程中。如果数据不满足约束条件,SQLite会立即返回错误,除非显式指定了ON CONFLICT冲突处理策略。理解约束的类型和生效条件,是设计可靠表结构的基础。下面是一张包含多种约束的建表示例:
CREATE TABLE employees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER CHECK (age >= 18 AND age <= 65),
department_id INTEGER,
FOREIGN KEY (department_id) REFERENCES departments(id)
);
上述语句中,id列是整数主键且启用了AUTOINCREMENT;name列不允许为空;email列必须唯一;age列通过CHECK约束限制在18到65之间;department_id列作为外键引用departments表的id列。这些约束在数据写入时逐条校验,任何一条不满足都会导致整个写入操作失败。
二、PRIMARY KEY主键约束:rowid机制与自增选择
主键的作用是唯一标识表中的每一行数据。在SQLite中,所有非WITHOUT ROWID表都有一个隐藏的64位整数rowid,除非表定义了一个INTEGER PRIMARY KEY列,否则rowid不会自动暴露。当某列被声明为INTEGER PRIMARY KEY时,该列会成为rowid的别名,因此在插入数据时如果该列为NULL,SQLite会自动分配一个唯一的整数。这种机制使得INTEGER PRIMARY KEY与AUTOINCREMENT存在重要区别。
INTEGER PRIMARY KEY自动分配的原则是:如果表从未插入过数据,则从1开始递增;如果插入过数据并且最高rowid没有被删除,则继续递增;如果最高rowid被删除,新插入的行可能会重用被删除的rowid。而INTEGER PRIMARY KEY AUTOINCREMENT会在内部维护一张sqlite_sequence表,保证新生成的rowid严格大于该表历史最大值,不会重用已删除的ID。动态分配的性能更好,但对删除后不重用ID有严格要求的场景,选择AUTOINCREMENT更合适。
-- 无AUTOINCREMENT,可能重用已删除的最大rowid
CREATE TABLE logs_no_autoincrement (
id INTEGER PRIMARY KEY,
content TEXT
);
-- 带AUTOINCREMENT,ID严格单调递增
CREATE TABLE logs_with_autoincrement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT
);
复合主键由多列组成,使用表级约束定义。复合主键中的每一列都不能为NULL,且组合起来必须唯一。例如在订单明细表中,一个订单可能包含多个商品,可以用order_id和product_id共同作为主键。需要注意的是,SQLite中复合主键不会自动分配rowid别名,插入时必须显式提供所有主键列的值。
CREATE TABLE order_items (
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id)
);
主键列的选择通常有两种思路:使用业务有意义的自然键,或者使用无业务含义的代理键。SQLite的整数代理键由于rowid机制在存取效率上更优,因此大多数场景推荐使用INTEGER PRIMARY KEY作为单列主键。如果业务上存在稳定的唯一标识(如订单号),可以配合UNIQUE约束使用,而不是直接作为主键,这样既能保持主键简单,又能保证业务唯一性。
三、FOREIGN KEY外键约束:为什么默认不生效,如何开启
外键用于维护表与表之间的引用完整性,确保子表中引用的父表记录真实存在。一个容易让人困惑的地方是,SQLite默认不强制外键约束。原因在于SQLite的嵌入式定位和向后兼容考虑,外键检查必须在每个连接上显式开启。如果连接时没有执行PRAGMA foreign_keys = ON;,即使建表时声明了FOREIGN KEY,删除或更新父表记录时也不会触发外键动作,子表可以插入指向不存在父记录的外键值。
开启外键约束的方法是在每次建立数据库连接后立即执行:
PRAGMA foreign_keys = ON;
如果使用编程接口,可以在连接初始化阶段执行该语句。例如Python的sqlite3模块可以通过conn.execute("PRAGMA foreign_keys = ON")开启。外键约束生效后,SQLite会在插入、更新、删除时检查引用关系。创建外键时,被引用的父表列必须拥有PRIMARY KEY或UNIQUE约束,否则SQLite会报错。
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
dept_name TEXT NOT NULL UNIQUE
);
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department_id INTEGER,
FOREIGN KEY (department_id) REFERENCES departments(id)
ON DELETE SET NULL
ON UPDATE CASCADE
);
外键约束支持多种级联操作。ON DELETE CASCADE表示删除父表记录时自动删除所有关联子表记录;ON DELETE SET NULL将子表外键列更新为NULL;ON DELETE SET DEFAULT将外键列设置为默认值;ON DELETE RESTRICT和NO ACTION行为类似,都会阻止删除有依赖的父记录,区别在于检查时机略有不同。实际操作中,CASCADE和SET NULL使用较多,能简化关联数据的清理逻辑。
如果需要排查现有数据库中的外键违规问题,可以使用PRAGMA foreign_key_check;命令。它会扫描所有启用了外键的表,返回违反引用完整性的行。这个工具在数据迁移或清理历史数据时非常有用。另外,外键约束开启后会带来一定的性能开销,因为每次写入都要检查父表索引,所以对于高频写入且能通过应用层保证完整性的场景,可以权衡是否开启。
四、NOT NULL、UNIQUE与CHECK:细粒度数据校验
NOT NULL约束是最常用的列级约束,它确保列在插入或更新时不能为NULL。如果业务上某些字段必须有值,例如用户名、订单金额等,就应在建表时声明NOT NULL。需要注意的是,SQLite中的空字符串''不等于NULL,所以NOT NULL约束不会阻止空字符串的写入。如果希望同时禁止空字符串,可以结合CHECK约束实现。
UNIQUE约束保证一列或多列的值在表中唯一。与PRIMARY KEY不同,UNIQUE约束允许NULL值,并且SQLite将多个NULL视为互不相同,因此可以插入多行含有NULL的唯一列数据。这一点与某些其他数据库的行为不同,需要特别注意。复合唯一约束使用表级约束定义,例如确保同一用户的邮箱类型唯一:
CREATE TABLE user_contacts (
user_id INTEGER NOT NULL,
contact_type TEXT NOT NULL,
contact_value TEXT NOT NULL,
UNIQUE (user_id, contact_type)
);
CHECK约束可以定义任意的布尔表达式,只要表达式结果为真或NULL就算通过。表达式为NULL时不被视为违反约束,这与SQL标准一致。列级CHECK约束只能引用当前列,表级CHECK约束可以引用多个列。常见用途包括范围检查、枚举校验和跨列比较:
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
discount_price REAL,
CHECK (discount_price IS NULL OR discount_price <= price)
);
上述建表语句中,price和stock分别通过列级CHECK限制非负,表级CHECK则确保discount_price不超过原价。SQLite的CHECK约束不能包含子查询,也不能引用其他表的列,这使得它的表达能力有限,但足以覆盖大多数字段级别的校验需求。若需要更复杂的跨表校验,可以考虑使用触发器实现。
五、约束修改与冲突处理:从建表到调试
SQLite的ALTER TABLE能力相对有限,仅支持重命名表、重命名列和增加列。不能直接删除列或修改已有列的约束,也不能直接添加PRIMARY KEY约束。如果需要修改表结构中的约束,通常的做法是创建一张新表,将数据迁移过去,然后替换旧表。例如给现有表添加UNIQUE约束,可以先建新表并复制数据:
CREATE TABLE users_new (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
nickname TEXT
);
INSERT INTO users_new (id, email, nickname)
SELECT id, email, nickname FROM users;
DROP TABLE users;
ALTER TABLE users_new RENAME TO users;
约束冲突时的处理策略由ON CONFLICT子句控制,可以在建表、创建索引或执行INSERT/UPDATE时指定。SQLite提供五种策略:ROLLBACK、ABORT、FAIL、IGNORE和REPLACE。其中ABORT是默认行为,发生冲突时回滚当前语句并返回错误;IGNORE会跳过冲突行继续执行;REPLACE会删除冲突行后插入新行。不同策略适合不同场景,例如批量导入时使用IGNORE可以跳过重复数据,但要注意它可能会掩盖真正的问题。
调试约束问题时,可以使用PRAGMA integrity_check;检查整个数据库的物理和逻辑完整性,包括主键唯一性、外键引用等。PRAGMA foreign_key_check;则专门用于外键检查。如果怀疑某个表的数据违反了约束,这两个命令能快速定位异常。此外,在开发阶段建议开启PRAGMA foreign_keys = ON;并将SQLite的busy_timeout设置一个合理值,避免并发写入时因为外键检查导致死锁或超时。
合理使用约束能显著降低数据异常的概率,但约束并非越多越好。过多的CHECK和UNIQUE会影响写入性能,尤其是针对大表的唯一索引。设计约束时应优先考虑业务数据完整性要求,将关键规则落在数据库层,而复杂的业务校验则放在应用层处理,二者配合才能构建稳健的数据存储体系。
SQLite约束PRIMARY KEYFOREIGN KEY修改时间:2026-09-26 13:13:43