表结构总会变,SQLite 大体上允许你修改它
表建好之后,迟早你会想重命名它、添加一列、删除一列,或者整体调整结构。SQLite 通过 DROP TABLE 和 ALTER TABLE 直接支持常见的情况,其他情况也有一套有文档说明的变通方法。
问题在于:SQLite 的 ALTER TABLE 比 Postgres 或 MySQL 的局限得多。知道它能做什么、不能做什么,以及在做不到时如何用重建表的方法解决,基本就是这里要掌握的全部技能。
DROP TABLE 删除表及其附带的一切
DROP TABLE 会删除表、表中的行、它的索引,以及定义在它上面的所有触发器。无法撤销:
表没了。现在查询它会报错 no such table: scratch。
如果你不确定表是否存在(这在初始化脚本中很常见),就用 IF EXISTS,让语句在表不存在时安静地什么也不做:
没有 IF EXISTS 的话,第二次删除会报错。加上它之后,两条语句都能顺利运行。
外键可能阻止 DROP
如果开启了外键约束(PRAGMA foreign_keys = ON;),而另一张表引用了你要删除的表,删除就会失败:
sqlite> PRAGMA foreign_keys = ON;
sqlite> DROP TABLE users;
Runtime error: FOREIGN KEY constraint failed
你有几种选择:先删除引用它的表,删除引用它的行,或者在创建外键时就定义 ON DELETE CASCADE。SQLite 不会悄悄地替你破坏引用完整性。
ALTER TABLE 能做的四件事
SQLite 的 ALTER TABLE 恰好支持四种操作:
每一种都是一条语句就能完成。前两种几乎没有开销,只是更新表结构。ADD COLUMN 也很快:SQLite 不会重写表,只是记录新列的定义。DROP COLUMN 的开销更大:SQLite 必须重写每一行,才能真正移除这一列的数据。
带默认值的 ADD COLUMN
给已有的表新增一列时,除非你指定默认值,否则每一行在这一列上的值一开始都是 NULL:
两行已有数据都被填上了 'active'。默认值必须是常量:在 ADD COLUMN 中,SQLite 不允许用 CURRENT_TIMESTAMP 或其他非常量表达式作为默认值,因为它需要一个无需逐行求值、就能应用到每一行已有数据上的值。
如果你需要 NOT NULL 又没有默认值,就只能先添加一个可为空的列,用 UPDATE 回填数据,再重建表来加上约束。这就引出了它的限制。
ALTER TABLE 做不到的事
在 Postgres 或 MySQL 中可以、但在 SQLite 中不行的操作:
- 修改列的类型(
ALTER COLUMN ... TYPE ...)。 - 就地修改列的默认值。
- 在已有的列上添加或移除
NOT NULL、CHECK、UNIQUE或PRIMARY KEY。 - 给已有的列添加外键。
- 调整列的顺序。
尝试其中任何一种都会得到语法错误。SQLite 根本没有 ALTER COLUMN 子句。对于所有这些情况,官方的答案都一样:重建表。
重建表的方法
当 ALTER TABLE 做不到你需要的事时,就用你想要的结构创建一张新表,把数据复制过去,删除旧表,再把新表重命名成原来的名字。把它放进一个事务,让整个过程要么全部完成,要么全部不做:
现在 users.age 是带 check 约束的整数,email 是 NOT NULL。数据也一起搬过来了。
实际操作时需要注意几点:
- 在整个过程中关闭外键约束。 如果其他表引用了你的表,就在事务之前运行
PRAGMA foreign_keys = OFF;,之后运行PRAGMA foreign_keys = ON;,否则DROP TABLE会失败。这条 pragma 不能在事务内部修改,所以要在事务外设置。 - 重新创建索引和触发器。 删除旧表时,它的索引和触发器也会被删除。重命名之后,在新表上把它们加回来。
- 检查视图。 引用了这张表的视图,在存储的 SQL 中仍然指向旧的名字。任何依赖于被修改列的视图都要重建。
重建表的方法写起来繁琐,但很可靠。Alembic 和 Rails 这类迁移工具在面向 SQLite 时,底层做的也正是这件事。
删除多张表
没有一条语句能同时删除多张表,你需要为每张表各运行一次 DROP TABLE。如果想把它们作为一组操作,就放进一个事务:
放进事务意味着三次删除要么全部成功,要么全部不做。当你拆除一组相互关联、可能在中途因外键而失败的表时,这很有用。
要点回顾
DROP TABLE删除表及其索引和触发器。在需要幂等的脚本中使用IF EXISTS。ALTER TABLE只能做四件事:重命名表、重命名列、添加列、删除列。- 其他任何操作(修改类型、添加新约束、给已有的列加外键)都要在事务中重建表。
- 重建表时要留意外键、索引、触发器和视图。它们不会自动跟着数据走。
下一篇:插入数据
你已经用一整章学习了表以及塑造表的约束。现在是时候往表里填数据了:下一章从 INSERT 开始,包括多行插入、默认值,以及 SQLite 如何处理与约束冲突的插入。
常见问题
如何在 SQLite 中删除表?
使用 DROP TABLE table_name;。加上 IF EXISTS 后,表不存在时语句什么也不做:DROP TABLE IF EXISTS users;。删除表时也会删除它的索引和触发器;如果开启了外键约束,而其他表仍在引用它,删除就会失败。
SQLite 中的 ALTER TABLE 能做什么?
四件事:RENAME TO(重命名表)、RENAME COLUMN ... TO ...(重命名列)、ADD COLUMN(追加新列)和 DROP COLUMN(删除列,从 SQLite 3.35 开始支持)。仅此而已:你不能修改列的类型,不能就地修改它的默认值,也不能给已有的列添加约束。
如何在 SQLite 中修改列的类型或约束?
SQLite 不直接支持这种操作。标准的变通方法是重建表:用你想要的结构创建一张新表,执行 INSERT INTO new SELECT ... FROM old,再 DROP TABLE old,最后 ALTER TABLE new RENAME TO old。把整个过程放进一个事务,保证它是原子的。