🛢️ 第8章 表数据的增、删、改操作
📄 一句话概括

三种插入: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 秒实验

  1. 把批量对比里的行数从 2000 改成 5000,看倍数变不变。倍数基本不变,因为两边的成本都是线性的。 变的是绝对时间——逐条那边会到一分钟以上。
  2. 把 INSERT ... SELECT 里的 WHERE 条件改成「一行都查不到」。不报错,插了零行。 这是它的特点:语句成功,但什么都没做。
  3. 想一想:为什么自增主键的列可以不在 INSERT 的列名列表里? 因为它有隐含的默认值——「下一个号码」本身就是默认值。

常见误区

  1. 「用了 executemany 就是批量插入,一定快。」 不一定。提交次数没变,它就不快。 真正的开关是事务边界。
  2. 「INSERT IGNORE 是安全的,至少不会报错。」 正因为它不报错,你不知道跳过了多少行。 批量导数据时这可能意味着「一半数据没进去」。
  3. 「INSERT ... SELECT 插的行数应该和 SELECT 出来的一样多。」 是一样的,但如果中途撞唯一约束,整条语句失败,一行都不进。 不是「进一部分」。
  4. 「插完在另一个连接里查 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 里最容易出事故的一种。

相关笔记