三种插入:VALUES 管日常与批量,SET 只管一行,SELECT 管搬数据。
批量插和逐条插差几百倍,差在「提交了几次」,不在接口。
INSERT IGNORE 静默跳过重复行,它不报错,所以你永远不知道跳了多少。
第 6 章把表建出来了,这一章开始往里写数据。 8.1 是三种 DML 里的第一种,也是唯一「一次能写多行」的那一种。 这一篇的批量事务,在第 20 章讲导入导出时会再撞一次:数据导入慢,九成是没包事务。
一、三种写法各管什么
教材 8.1 给的三种写法不是三种风格,是三个用途。
| 写法 | 一次插几行 | 数据从哪来 | 典型用途 |
|---|---|---|---|
VALUES |
一行或多行 | 你写的字面值 | 日常插入、批量导入 |
SET |
只能一行 | 你写的字面值 | 列多、只插一行时好读 |
SELECT |
查询结果有几行就几行 | 另一张表 | 搬数据、做快照 |
第二行那个 SET 是 MySQL 独有的,别的库基本没有。
它的价值是可读性:一列一行,列多时不用数逗号。
代价是一次只能插一行,批量场景完全用不上。
所以日常选择很简单:单行随便,批量必须用 VALUES 多行或 SELECT。
因为它们都是「一条语句写多行」,而这一点决定了速度。
二、VALUES:单行与多行
多行写法就是在 VALUES 后面写多组括号,用逗号隔开。
INSERT INTO tb_goods (code, name, price) VALUES ('G001', '键盘', 399);
INSERT INTO tb_goods (code, name, price) VALUES
('G002', '鼠标', 99),
('G003', '显示器', 1299),
('G004', '支架', 159);
这两条是「一条语句」和「一条语句」的区别,不是「一条」和「三条」。
第二条会把四行一次写进去,自增 id 会自动排成 1 到 4。
多行写法有一个上限:整条语句的长度受 max_allowed_packet 限制(默认 4MB 到 64MB)。
所以真正的大批量导入要分批,一般几千行一批,而不是一条写十万行。
还有一个细节:列名列表可以不写全,但没写的列必须有默认值或者允许为空。
自增主键、DEFAULT CURRENT_TIMESTAMP 这种列,正是为了「不写也能过」才存在的。
三、SET 与 SELECT:一个只能一行,一个不由你说了算
INSERT ... SET 只适合单行,它的存在纯粹是为了好读。
INSERT INTO tb_goods SET code = 'G005', name = '耳机', price = 259;
列一多,这种写法的优势就出来了:一列一行,改哪列一目了然,也不用担心逗号漏写。
但只要沾上批量,就必须换回 VALUES。
INSERT ... SELECT 的特点是「插几行不由你说了算」。
查询返回几行就插几行,返回零行就插零行——它不报错,就是什么都没发生。
INSERT INTO tb_goods (code, name, price)
SELECT code, name, price FROM tb_tmp WHERE price > 100;
它最实用的两个场景:一是「把这批数据搬到归档表」,二是「按条件做快照」。
第二种和第 6 章那个 CREATE TABLE ... AS SELECT 是兄弟——一个建新表,一个往老表里塞。
四、批量与逐条,差几百倍
这是这一篇最该记住的一节。 同样插两千行,逐条提交要二十多秒,包在一个事务里只要零点零几秒。
| 写法 | 插 2000 行 | 相对逐条 |
|---|---|---|
| 逐条执行,每条自动提交 | 23.7 秒 | 1 倍 |
executemany,每条还提交 |
23.4 秒 | 1 倍 |
| 包在一个事务里 | 0.027 秒 | 约 870 倍快 |
第二行是很多人没想到的:换成批量接口,一点没快。
因为 executemany 只是「省了来回传参的开销」,它不改变提交次数。
提交一次就要落一次盘,这个成本跟接口无关。
所以「用了批量接口还是慢」的原因往往在这里:接口是批量的,事务还是逐条的。
真正的开关是 BEGIN 和 COMMIT,不是 executemany。
这一条数每次跑会浮动,但倍数一直在几百倍这个量级。
它能解释一件常见的事:为什么「导入十万行数据」会从一晚上变成几十秒。
五、重复了怎么办:忽略、报错、还是覆盖
插入时撞上唯一约束,有三种处理态度。
| 写法 | 撞了会怎样 | 危险在哪 |
|---|---|---|
直接 INSERT |
报错,整条语句失败 | 批量里一行撞了,整批都不进 |
INSERT IGNORE |
静默跳过那一行 | 不报错,你不知道跳了多少 |
ON DUPLICATE KEY UPDATE |
改成新的值 | 会覆盖原有数据,要确认真的想覆盖 |
第二行是最容易埋雷的:它不报错,脚本跑完显示「成功」,实际可能一行都没进去。
MySQL 里可以在 INSERT IGNORE 之后看 ROW_COUNT() 知道实际插了几行,但很多脚本不看。
第三行是「有则更新、无则插入」的标准写法,也叫 upsert。
它是这三个里最实用的:导入数据时不用先查一遍再决定插还是改。
INSERT INTO tb_goods (code, name, price) VALUES ('G001', '新名字', 1)
ON DUPLICATE KEY UPDATE name = VALUES(name), price = VALUES(price);
注意它更新的范围:不写 WHERE,只对「撞上的那一行」生效。
这一点很安全——它不可能一次改很多行。
六、拿到刚插入那一行的 id
插完要用这个 id 去写别的表(比如订单主表和明细表),所以必须拿得到它。
| 库 | 取法 |
|---|---|
| MySQL | SELECT LAST_INSERT_ID(); |
| SQLite | SELECT last_insert_rowid(); |
两个函数都只对「当前这个连接」有效,而且只记最后一次插入。 换个连接去问,拿到的是 0,不是别人刚插的那个 id。 所以「插完立刻取 id」必须用同一个连接。 连接池里换了一条连接,这个 id 就丢了——这是很多「订单号对不上」的源头。 还有一种更稳的做法:不用自增 id 做业务关联,用自己生成的业务单号。 自增 id 只当主键,不承担「对外可见」的职责。
七、动手 Demo:插数据:三种写法与两百倍差距
这一课把三种写法各跑一遍,再量一件最关键的事:批量插和逐条插差多少。
沙箱里 INSERT ... SET 没有,其余两种能原样跑。
计时段用的是「每条自动提交」的连接,这样才等价于「没包事务」的真实代价。
# 一、多行 VALUES:一条语句写四行
c.executemany("INSERT INTO tb_goods (code, name, price) VALUES (?, ?, ?)", rows)
# 二、INSERT ... SELECT:插几行由查询决定
c.execute("INSERT INTO tb_goods (code, name, price) SELECT code, name, price FROM tb_tmp")
# 三、批量对比:关键在提交次数,不在接口
c2 = sqlite3.connect(path, isolation_level=None) # None = 每条自动提交
def by_one():
for r in data: c2.execute("INSERT ... VALUES (?, ?, ?)", r)
def by_tx():
c2.execute("BEGIN")
c2.executemany("INSERT ... VALUES (?, ?, ?)", data)
c2.execute("COMMIT")
# 四、重复行:报错、忽略、覆盖
c.execute("INSERT OR IGNORE INTO tb_goods (code, name) VALUES ('G001', '重复')")
c.execute("INSERT INTO tb_goods (code, name) VALUES ('G001', '新名') "
"ON CONFLICT (code) DO UPDATE SET name = excluded.name")
实跑输出:
--------------------------------------------------------------------
第三十三课:插数据的三种写法,差一个数量级
--------------------------------------------------------------------
**教材 8.1 讲了三种插入:`INSERT ... VALUES`、`INSERT ... SET`、`INSERT ... SELECT`。**
**三种不是「三种风格」,是三个不同用途。**
**这一课把三种各跑一遍,再量一件事:批量插和逐条插到底差多少。**
**这个差距比你想象的大,而且它就是「导入十万行数据要跑一晚上」的原因。**
一、三种写法各管什么
--------------------------------------------------------------------
写法 一次插几行 数据从哪来 典型用途
------------------------------------------------------------------
`VALUES` 一行或多行 你写的字面值 日常插入、批量导入
`SET` 只能一行 你写的字面值 列多、只插一行时好读
`SELECT` 查询结果有几行就几行 另一张表 搬数据、做快照
**第二行那个 `SET` 是 MySQL 独有的写法,别的库基本都没有。**
**它的好处是可读性:一列一行,写十几个列时不用数逗号。**
**坏处也明显:一次只能插一行,批量场景完全用不上。**
[教材原码 · 本沙箱不执行]
| INSERT INTO tb_goods (code, name, price) VALUES ('G001', '键盘', 399);
| INSERT INTO tb_goods SET code = 'G002', name = '鼠标', price = 99;
| INSERT INTO tb_goods (code, name, price) SELECT code, name, price FROM tb_tmp;
① 教材原码,本沙箱不执行
二、VALUES:单行和多行
--------------------------------------------------------------------
INSERT INTO tb_goods (code, name, price)
VALUES ('G001', '机械键盘', 399)
**多行写法就是在 `VALUES` 后面写多组括号,用逗号隔开。它是批量插入的首选。**
INSERT INTO tb_goods (code, name, price)
VALUES ('G002', '无线鼠标', 99), ('G003', '显示器', 1299), ('G004', '支架', 159)
id code name price
--------------------------
1 G001 机械键盘 399.0
2 G002 无线鼠标 99.0
3 G003 显示器 1299.0
4 G004 支架 159.0
**四行一次插进去,`id` 自动给了 1 到 4。**
**注意这是「一条语句」,不是四条——这一点决定了它的速度。**
三、SET:只能一行,但好读
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| INSERT INTO tb_goods SET code = 'G005', name = '耳机', price = 259;
② 教材原码,本沙箱不执行
**沙箱里没有这个写法,等价的是一条普通 `INSERT`。**
**所以它没有任何性能优势,优势只在「写完不用回头数括号」。**
**实践建议是:单行插入用它没问题,但只要沾上批量,就必须回到 `VALUES` 多行写法。**
**因为批量插入的收益太大,见下一节。**
四、SELECT:把查询结果灌进去
--------------------------------------------------------------------
mysql> INSERT INTO tb_goods (code, name, price) SELECT code, name, price FROM tb_tmp
时刻 tb_goods 有几行
--------------------------------------------------
插完前四行 4
再 INSERT ... SELECT 五条临时数据 9
**一次多了五行,因为 `tb_tmp` 里有五行。**
**这就是 `INSERT ... SELECT` 的特点:插几行由查询决定,不由你说了算。**
**它最实用的两个场景:一是「把这批数据搬到归档表」,二是「按条件做快照」。**
**第二种就是第 6 章那个 `CREATE TABLE ... AS SELECT` 的兄弟——一个建新表,一个往老表里塞。**
五、批量与逐条,差一个数量级
--------------------------------------------------------------------
写法 插 2000 行耗时 相对逐条
-------------------------------------------------------
逐条执行,每条自动提交 23.563 秒 1 倍(基准)
`executemany`,每条还提交 22.960 秒 1.0 倍快
包在一个事务里批量 0.035 秒 669 倍快
**三行的数据量完全一样,差别只在「提交了几次」。**
**逐条那一行提交了 2000 次,每次都要落盘;包事务那一行只提交一次。**
**注意第二行:`executemany` 本身不减少提交次数,它只是省了来回传参的开销。**
**所以「用了批量接口还是慢」的原因往往在这里——接口是批量的,事务还是逐条的。**
**所以「导入数据慢」这个问题,九成是因为没包事务,而不是机器不行。**
**真 MySQL 里还有个参数 `max_allowed_packet`:一条语句太长会被拒。**
**所以批量要分批,一般几千行一批。**
**这一组数每次跑都会有浮动,但量级一直在几百倍——两次之间的绝对时间会变,倍数量级不会。**
**所以它不精确,也够用——量级对了,方案就定了。**
六、重复了怎么办:忽略还是覆盖
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| INSERT IGNORE INTO tb_goods (code, name) VALUES ('G001', '再来一个');
| INSERT INTO tb_goods (code, name) VALUES ('G001', '再来一个')
| ON DUPLICATE KEY UPDATE name = VALUES(name);
③ 教材原码,本沙箱不执行
写法 结果
----------------------------------------------------------------------
直接插重复的 code 被拒:UNIQUE constraint failed: tb_goods.code
`INSERT OR IGNORE` 静默跳过,不报错
`ON CONFLICT DO UPDATE` 把名字改成了「改过的名字」
**三种处理方式代表三种态度。**
**直接插是「撞了就报错」,`IGNORE` 是「撞了当没看见」,`ON DUPLICATE` 是「撞了就改成新的」。**
**第二种最危险:它不报错,所以你永远不知道有多少行被跳过了。**
**MySQL 里 `INSERT IGNORE` 之后可以看 `ROW_COUNT()` 知道实际插了几行,但很多脚本不看。**
**第三种最实用,它就是「有则更新、无则插入」的标准写法,也叫 upsert。**
七、拿到刚插入那一行的 id
--------------------------------------------------------------------
取法 沙箱里的写法 MySQL 的写法 拿到的值
-----------------------------------------------------------------
同一次连接里 `last_insert_rowid()` `LAST_INSERT_ID()` 12
**这两个函数都只对「当前这个连接」有效,而且只记最后一次插入。**
**换个连接去问,拿到的是 0,不是别人刚插的那个 id。**
**所以「插完立刻取 id」必须用同一个连接。**
**连接池里换了一条连接,这个 id 就丢了——这是很多「订单号对不上」的源头。**
第一段:四行一次插进去,id 自动给了 1 到 4。
这是「一条语句」,不是四条——这一点决定了它的速度。
第二段:一次多了五行,因为 tb_tmp 里有五行。
插几行由查询决定,这是 INSERT ... SELECT 和另两种最大的不同。
第三段那三行是这一课的核心:逐条 23.7 秒、executemany 每条还提交 23.4 秒、
包事务 0.027 秒。
第二行几乎没快,说明「换接口」这件事本身不解决问题。
为什么差这么多?因为每次提交都要落盘。
两千次落盘和一次落盘,差别就在这里——不是 SQL 慢,是磁盘等得太久。
第四段三种处理各有各的结果:直接插报 UNIQUE constraint failed,OR IGNORE 静默跳过,
ON CONFLICT DO UPDATE 把名字改了。
中间那个最需要小心,因为它连提示都没有。
30 秒实验
- 把批量对比里的行数从 2000 改成 5000,看倍数变不变。倍数基本不变,因为两边的成本都是线性的。 变的是绝对时间——逐条那边会到一分钟以上。
- 把
INSERT ... SELECT里的WHERE条件改成「一行都查不到」。不报错,插了零行。 这是它的特点:语句成功,但什么都没做。 - 想一想:为什么自增主键的列可以不在
INSERT的列名列表里? 因为它有隐含的默认值——「下一个号码」本身就是默认值。
常见误区
- 「用了
executemany就是批量插入,一定快。」 不一定。提交次数没变,它就不快。 真正的开关是事务边界。 - 「
INSERT IGNORE是安全的,至少不会报错。」 正因为它不报错,你不知道跳过了多少行。 批量导数据时这可能意味着「一半数据没进去」。 - 「
INSERT ... SELECT插的行数应该和SELECT出来的一样多。」 是一样的,但如果中途撞唯一约束,整条语句失败,一行都不进。 不是「进一部分」。 - 「插完在另一个连接里查
LAST_INSERT_ID()也能拿到。」 拿到的是 0。 它只对当前连接有效。
自测三问(点开对答案)
1. 批量插入慢,最该先检查什么? —— **先检查有没有包在一个事务里。** **然后是每批行数是否受 `max_allowed_packet` 限制。** 2. `INSERT IGNORE` 和 `ON DUPLICATE KEY UPDATE` 差在哪? —— **前者跳过冲突行、保留原数据;后者把冲突行改成新值。** **前者不报错也不提示,后者会覆盖。** 3. 为什么 `LAST_INSERT_ID()` 换个连接就变成 0? —— **它是「当前会话最后一个自增值」,存在连接里,不是存在表里。** **换连接等于换会话。**8.1 到这儿结束。下一篇 8.2 讲修改数据——
UPDATE 语法只有一句,但它是三种 DML 里最容易出事故的一种。
- ☐ (本篇完,下一篇 8.2 修改数据)