CREATE TABLE ... LIKE 只抄结构,AS SELECT 只抄数据,没有一种两样都抄全。
AS SELECT 抄出来的是一张裸表:约束、索引、默认值、自增全丢。
想复制一张一样的表用 LIKE,想把查询结果落成表用 AS SELECT。
6.4 讲的是「换个名字」,这一篇讲「再要一张」。两件事经常连着用。
它是「备份表」和「在线切换」两套手法的起点:先复制,再切。
这一篇的 AS SELECT,在第 20 章讲数据备份时会当成「逻辑备份」的一种用法。
一、只抄结构:CREATE TABLE ... LIKE
LIKE 抄的是「建表语句里除数据之外的一切」。
CREATE TABLE tb_bak LIKE tb_order;
具体抄哪些、不抄哪些,教材 6.5 列过,这里按「会不会踩坑」重排:
| 项 | LIKE 抄不抄 |
|---|---|
| 列名与类型 | 抄 |
NOT NULL、DEFAULT、CHECK |
抄 |
| 索引(含唯一索引) | 抄 |
AUTO_INCREMENT 的当前值 |
抄(接着源表往下走) |
| 外键约束 | 不抄 |
| 数据 | 不抄 |
第四行是很多人想不到的:连「下一行该给几号」都抄过来了。
所以抄出来的表第一行 id 不是 1,是源表的下一个值。
做测试数据时这反而添麻烦,想要从 1 开始得手动 ALTER TABLE ... AUTO_INCREMENT = 1。
第五行那个「不抄外键」也有道理:外键指向的是另一张表,抄过来还是指着原来那张,没意义。
真要抄外键得手工写。
二、只抄数据:AS SELECT
AS SELECT 抄的是「这条查询的结果」,顺带按结果集推一个表结构。
CREATE TABLE tb_bak AS SELECT * FROM tb_order;
CREATE TABLE tb_bak AS SELECT * FROM tb_order WHERE created < '2026-10-01';
第一句是整表抄,第二句是条件抄。两句的差别只是 WHERE,但第二句用处大得多。
它抄出来的表是这样的:列名和类型按结果集推,其余全没有。
| 项 | AS SELECT 抄不抄 |
|---|---|
| 列名 | 抄 |
| 类型 | 按结果集推,可能和源表不一致 |
NOT NULL/DEFAULT/CHECK |
不抄 |
| 索引与唯一约束 | 不抄 |
AUTO_INCREMENT |
不抄,从 1 开始 |
第二行那个「可能不一致」要举个例子:源表 DECIMAL(8,2) 的列,
抄过来可能变成 DECIMAL(10,2)。
因为结果集推类型时按实际值算,不照抄原定义。
这一点在做「数据快照」时没问题,在做「表结构复制」时就是坑。
三、两种抄法丢了什么
把两张表并排看,结论很清楚。
| 项 | LIKE |
AS SELECT |
|---|---|---|
| 数据 | 不抄 | 抄 |
| 列与类型 | 抄 | 抄(按结果集推) |
| 索引与约束 | 抄 | 不抄 |
| 自增计数器 | 抄 | 不抄 |
| 执行速度 | 快 | 取决于数据量 |
注意最后一行:LIKE 不碰数据,所以再大的表也是秒回。
而 AS SELECT 要把数据读一遍写一遍,百万行就是几秒到几十秒。
还有一句必须提醒:AS SELECT 抄出来的表没有任何约束,所以它可以塞进重复主键、空值、负价格。
如果这张表要留作对比、事后还要拿来对账,那它必须自带约束,否则脏数据会混进来。
四、复制表的四个真实用途
教材 6.5 只讲语法,实际用起来是这四件事。
| 用途 | 用哪种抄法 | 注意什么 |
|---|---|---|
| 改结构前留一份 | LIKE + 补数据,或 AS SELECT |
要能还原,别只抄结构 |
| 按日期做快照 | AS SELECT 带 WHERE |
快照表没约束,只当只读 |
| 在线切换的新表 | LIKE 再灌新数据 |
灌完用 RENAME TABLE 换名 |
| 给别人一份测试数据 | AS SELECT 带 WHERE 抽样 |
注意脱敏 |
第三行是「不停机改结构」的核心手法,值得记住顺序:
LIKE 建新表 → 灌数据 → 建索引 → RENAME TABLE 换名。
第四行那个「脱敏」是真事。
把生产数据抄一份给测试,手机号、身份证号都在里面,这属于数据泄露。
正确做法是先抄到临时表,再用 UPDATE 把敏感列打码,最后才给出去。
五、动手 Demo:抄一张表,两种抄法各丢了什么
这一课先建一张带六种约束的源表(50 行),再分别用两种抄法抄一遍,逐项对比。
沙箱里 AS SELECT 能原样跑,LIKE 没有,用「取建表语句换表名」来等价。
最后会看到一件吓人的事:AS SELECT 抄出来的表连重复主键都能塞进去。
# 源表:自增主键 + 唯一 + 非空 + 默认 + CHECK + 一个二级索引,50 行
c.execute("CREATE TABLE tb_src (id INTEGER PRIMARY KEY AUTOINCREMENT, "
"code TEXT NOT NULL UNIQUE, price REAL NOT NULL DEFAULT 0 CHECK (price >= 0))")
c.execute("CREATE INDEX i_price ON tb_src (price)")
# 一、只抄结构:取建表语句、换表名,索引要手工补
ddl = c.execute("SELECT sql FROM sqlite_master WHERE name = 'tb_src'").fetchone()[0]
c.execute(ddl.replace("CREATE TABLE tb_src", "CREATE TABLE tb_like"))
c.execute("CREATE INDEX i_like_price ON tb_like (price)")
# 二、只抄数据:一句就够,但约束全丢
c.execute("CREATE TABLE tb_as AS SELECT * FROM tb_src")
# 三、验证:往新表里插两条一模一样的主键
c.execute("INSERT INTO tb_as (id, code, price) VALUES (1, 'DUP', 1)")
c.execute("INSERT INTO tb_as (id, code, price) VALUES (1, 'DUP', 1)")
实跑输出:
--------------------------------------------------------------------
第二十九课:抄一张表,有两种抄法
--------------------------------------------------------------------
**教材 6.5 讲复制表。两种写法名字很像,抄到的东西差很远。**
**一种只抄结构,一种只抄数据,没有一种「两样都抄全」的。**
**这一课把两种都跑一遍,然后逐项对比新表里剩下什么。**
一、准备一张有约束的源表
--------------------------------------------------------------------
**源表有:自增主键、唯一约束、非空、默认值、CHECK、一个二级索引、50 行数据。**
**记住这七样,下面看两种抄法各自丢了多少。**
二、只抄结构:CREATE TABLE ... LIKE
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE TABLE tb_like LIKE tb_src;
② 教材原码,本沙箱不执行
**沙箱里没有 `LIKE` 这个写法,等价做法是取源表的建表语句、换个表名再执行。**
**我照这个做法抄了一遍:**
项 tb_src tb_like
----------------------------------------------------
列数 3 3
行数 50 0
索引 i_price i_like_price(我手工补的)
插一行拿到的 id 51 1
**第三行那个「手工补的」是一条真差别。**
**真 MySQL 的 `CREATE TABLE ... LIKE` 会把索引一起抄过去,这里不会,得自己补。**
**第四行也是真差别:MySQL 的 `LIKE` 会连 `AUTO_INCREMENT` 的当前值一起抄。**
**所以抄出来的表下一行不会是 1,而是源表的下一个值。**
**这一点在「复制一张表做测试」时反而会添麻烦。**
三、只抄数据:CREATE TABLE ... AS SELECT
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE TABLE tb_as AS SELECT * FROM tb_src;
③ 教材原码,本沙箱不执行
mysql> CREATE TABLE tb_as AS SELECT * FROM tb_src
**这一句在沙箱里能原样跑。它抄出来的是什么?先看行数和索引:**
项 tb_src tb_as
----------------------------
行数 50 50
索引个数 2 0
code 列 不能为空 可以为空
**索引个数是 0——一个都没有。**
**新表连唯一约束都没了,所以 `code` 列变成了「随便重复」。**
**这一点不是理论风险,我试了一下:往新表里插两条一样的 code。**
动作 结果
-----------------------------------------
插两条完全相同的行 两条都插进去了
现在 tb_as 里还剩几行同名 2 行
**这就是 `AS SELECT` 的本质:它只搬数据,所有约束、索引、默认值、自增全丢掉。**
**它抄出来的是一张「裸表」,什么都不管。**
四、两种抄法的四个差别
--------------------------------------------------------------------
项 `LIKE` `AS SELECT`
----------------------------------------------------------
数据 不抄 抄
列与类型 抄 抄(类型按结果集推)
索引与唯一约束 抄 不抄
自增计数器 抄(接着源表往下走) 不抄
**四行看完,结论就一句:想复制一张「一样的表」用 `LIKE`。**
**想把查询结果落成一张表,用 `AS SELECT`。**
**前者是建表,后者是存结果。**
**`AS SELECT` 真正的用处是配合 `WHERE` 做快照:**
**`CREATE TABLE tb_bak_1030 AS SELECT * FROM tb_order WHERE created < '2026-10-01'`。**
**这种「每月一号存一份上月数据」的用法,比导出成文件再导入省事得多。**
第一段那张对比表:列数都是 3,行数是 50 和 0。
LIKE 抄了结构不抄数据,所以 0 行。
索引那一行我要诚实说明:i_like_price 是我手工补的,SQLite 的等价做法不会自动带索引。
第四行「插一行拿到的 id」是 51 对 1。
这一格是真差别:真 MySQL 的 LIKE 会把 AUTO_INCREMENT 当前值一起抄,
所以抄出来的表下一行是 51,不是 1。
这里的表是空的、计数器是新的,所以拿到 1。
第二段:AS SELECT 抄出来 50 行,索引 0 个,code 列从「不能为空」变成「可以为空」。
一个约束都没剩。
第三段最吓人:往 tb_as 里插两条完全相同的 (1, 'DUP', 1),两条都进去,表里 2 行同名。
因为新表没有主键、没有唯一约束——它只是一堆列。
所以「抄一份做备份」如果用 AS SELECT,这份备份本身可能是脏的。
30 秒实验
- 给
AS SELECT那句加上WHERE price > 10,看新表有几行。源表 50 行里价格大于 10 的那些。 这就是「按条件做快照」的用法,比导出成文件再导入省事。 - 把
LIKE抄出来的表插满 50 行,再看下一行的 id 是 51 还是 101。 在这里是 51,因为计数器从头开始;真 MySQL 的LIKE会从源表的下一个值开始。 - 想一想:用
AS SELECT抄一张 1000 万行的表,磁盘和耗时会是什么量级? 数据要写一遍,索引一个都没有,所以比原表小;耗时主要是读加写,几分钟到十几分钟。
常见误区
- 「
AS SELECT就是复制表,抄完是一样的。」 不一样。它只抄数据,所有约束、索引、默认值、自增全丢。 抄出来的是一张裸表。 - 「
LIKE会把数据一起抄过来。」 不会。它只抄结构,抄完是 0 行。 要带数据得再一句INSERT ... SELECT。 - 「复制一份表给测试用,直接抄就行。」 手机号、身份证、地址都在里面,这属于数据泄露。 要先脱敏再给。
- 「
LIKE抄出来的表和原表完全一样。」 外键不抄,AUTO_INCREMENT的当前值会抄过来,这两处都和直觉不同。
自测三问(点开对答案)
1. 要一张「结构一样、数据不要」的空表,用哪一句? —— **`CREATE TABLE 新表 LIKE 旧表`。** **它抄结构、索引、约束,不抄数据。** 2. `AS SELECT` 抄出来的表为什么能插重复主键? —— **因为它没抄主键约束,连索引都没有。** **它就是一堆列,什么都不管。** 3. 在线给大表改结构,`LIKE` 在里面起什么作用? —— **它负责第一步:建一张结构一样的新表。** **后面灌数据、建索引,最后 `RENAME TABLE` 换名。**6.5 到这儿结束。下一篇 6.6 讲删除表—— 「删」有三种,代价和后果都不一样,最后那一篇会把三者摆在一起。
- ☐ (本篇完,下一篇 6.6 删除表)