This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/02-SQL核心/07-DML 增删改.md
T
2026-05-21 12:20:51 +08:00

8.5 KiB
Raw Blame History

tags, create time
tags create time
MySQL
DML
INSERT
UPDATE
DELETE
2026-05-16 00:00

DML — 增删改

概述

DML(Data Manipulation Language)是日常使用最频繁的 SQL 类别——增、改、删构成了应用和数据库之间的数据交互主线。

[!QUESTION] 思考:为什么说「写」比「读」更复杂? SELECT 只需要找到匹配的数据,而 INSERT / UPDATE / DELETE 不仅要改变状态,还要处理并发冲突、约束校验、外键级联、触发器副作用。理解这些隐藏行为,才能写出既正确又高效的写入逻辑。

本节聚焦 MySQL 特有的高效写入技巧和常见陷阱。

INSERT

基础插入

INSERT INTO users (username, email, status)
VALUES ('alice', 'alice@example.com', 1);

-- 批量插入(性能远高于逐条插入)
INSERT INTO users (username, email, status) VALUES
('bob',   'bob@example.com',   1),
('charlie','charlie@example.com', 0),
('david', 'david@example.com', 1);

[!TIP] MySQL 8.0.19+:INSERT ... RETURNING 和 PostgreSQL 一样,MySQL 从 8.0.19 起支持 RETURNING 子句,免去二次查询:

INSERT INTO users (username, email, status)
VALUES ('eve', 'eve@example.com', 1)
RETURNING id;  -- 直接拿到自增 ID

典型场景:插入后需要立即获取新记录的自增主键做级联操作。替代了老方案中 INSERT ... ; SELECT LAST_INSERT_ID(); 两条语句的组合。

INSERT ... ON DUPLICATE KEY UPDATE

处理"存在则更新,不存在则插入"的场景:

INSERT INTO user_stats (user_id, total_orders, total_spent)
VALUES (1001, 5, 299.50)
ON DUPLICATE KEY UPDATE
    total_orders = total_orders + VALUES(total_orders),
    total_spent  = total_spent  + VALUES(total_spent);

[!TIP] 常见陷阱

  • 多唯一键冲突:如果有多条唯一键匹配同一行,只更新一次(返回 Row Count = 2)
  • LAST_INSERT_ID():INSERT 时返回新 ID;ON DUPLICATE KEY UPDATE 时自动改写为旧行的主键 ID(方便级联操作)
  • VALUES() 废弃警告:MySQL 8.0.20+ 对 VALUES(col) 发出 DEPRECATION WARNING。对于单行插入无需改动,多行批量插入需改用子查询或分段处理。
flowchart TD
    A["INSERT 语句"] --> B{"主键或唯一键\n是否存在"}
    B -->|不存在| C["执行 INSERT"]
    B -->|已存在| D["执行 UPDATE\nON DUPLICATE KEY UPDATE 子句"]
    C --> E["返回 Row Count = 1"]
    D --> F["返回 Row Count = 2\n匹配行加实际更新"]
    
    style B fill:#FF9F43,color:#000
    style C fill:#00D866,color:#fff
    style D fill:#00B6BC,color:#fff

[!NOTE] Row Count 的含义

  • Row Count = 1:正常插入新记录
  • Row Count = 2:唯一键冲突,走了 UPDATE 路径(但 UPDATE 没有实际改变值也算 2)
  • Row Count = 0:唯一键冲突,且 UPDATE 后的值与原来相同

REPLACE INTO

当唯一键冲突时,先删除旧行再插入新行。

REPLACE INTO users (id, username, email) VALUES (1, 'alice_new', 'new@email.com');
sequenceDiagram
    participant S as Server
    participant DB as Database

    S->>DB: REPLACE INTO users ...
    Note over S,DB: Step 1: DELETE 已有记录
    S->>DB: DELETE FROM users WHERE pk = 1
    Note over S,DB: Step 2: INSERT 新记录
    S->>DB: INSERT INTO users ...
    
    Note over S,DB: 如果外键引用了被删行\n会触发 ON DELETE CASCADE

[!WARNING] REPLACE vs ON DUPLICATE KEY UPDATE

  • REPLACE:本质是 DELETE + INSERT,会导致自增 ID 变化、触发 BEFORE/AFTER DELETE 钩子、级联外键删除
  • ON DUPLICATE KEY UPDATE:原地更新,不影响其他字段和自增值

优先用 ON DUPLICATE KEY UPDATE,除非你确实需要完整的「删+插」语义。

UPDATE

UPDATE 的核心原则只有一条:精准定位、最小影响。先思考「哪些行需要改」,再写 SET 子句。

-- 基础更新:定位到单行
UPDATE users SET status = 0 WHERE id = 100;

-- 多字段同时更新
UPDATE users SET 
    email = 'new@email.com',
    updated_at = NOW()
WHERE email = 'old@email.com' AND status = 1;

-- JOIN 批量更新(从关联表同步数据)
UPDATE articles a
JOIN categories c ON a.category_id = c.id
SET a.category_name = c.name
WHERE a.category_name IS NULL;

[!QUESTION] 为什么 UPDATE 忘记带 WHERE 这么危险? UPDATE users SET status = 0; 会把所有用户的状态设为禁用。MySQL 有一个安全措施叫 sql_safe_updates:

SET sql_safe_updates = 1;
-- 此时不带 WHERE 或不带主键条件的 UPDATE 会被拒绝
-- 生产环境建议在连接层开启此选项

LIMIT 与 ORDER BY

MySQL 特有的扩展:UPDATE 可以加 LIMIT 控制影响行数。

-- 只更新符合条件的前 100 条(配合 ORDER BY 可控)
UPDATE users SET status = 0 
WHERE status = 1 AND created_at < '2024-01-01'
ORDER BY created_at ASC
LIMIT 100;

DELETE vs TRUNCATE

DELETE 和 TRUNCATE 都能清空或减少表数据,但底层机制完全不同。选错可能导致性能灾难或数据意外丢失。

特性 DELETE TRUNCATE
类型 DML DDL
可回滚 ✅ 事务内可 ROLLBACK ❌ 隐式提交,不可回滚
WHERE 过滤 ✅ 可以 ❌ 全删
AUTO_INCREMENT 保留当前值 重置为 1
触发器 触发 DELETE 触发器 不触发
速度 逐行删除较慢 直接重建表极快
返回值 影响的行数 无返回值
锁粒度 行锁(可带 WHERE) 表级元数据锁

DELETE 进阶用法

-- DELETE 带 JOIN(MySQL 特有语法)
DELETE u FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- 删除所有没有订单的用户

-- 多表联合删除(同时删用户 + 订单)
DELETE t1, t2 FROM users t1
INNER JOIN orders t2 ON t1.id = t2.user_id
WHERE t1.status = 0;

[!NOTE] ORDER BY + LIMIT 在 DELETE 中同样可用

-- 只删除符合条件的前 50 条(按创建时间最早的优先)
DELETE FROM users WHERE status = 0 
ORDER BY created_at ASC
LIMIT 50;

高效写入策略

当需要处理大量数据时,写入方式的选择直接影响性能和稳定性。下面按数据量级给出分级方案:

flowchart TD
    A["大量数据写入"] --> B{数据量}
    B -->|小于 1 万条| C["单条 INSERT + 事务包裹"]
    B -->|1 万至 100 万条| D["分批 INSERT\n每批 500 到 2000 条"]
    B -->|大于 100 万条| E["LOAD DATA INFILE"]
    
    C --> F["BEGIN; INSERT... COMMIT;"]
    D --> G["每批独立事务\n降低锁竞争"]
    E --> H["绕过 SQL 解析\n直接写数据文件"]
    
    style E fill:#00D866,color:#fff
    style D fill:#FF9F43,color:#000
-- LOAD DATA INFILE 示例(最快的批量导入方式)
LOAD DATA LOCAL INFILE '/tmp/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
(username, email, @status)  -- @variable 用于预处理
SET status = CASE @status WHEN 'active' THEN 1 ELSE 0 END;

分批 INSERT 代码示例

对于中等规模数据,事务 + 分批次 是最实用的方案。以下 Go 伪代码展示了核心思路:

const batchSize = 1000

rows := generateRecords() // 假设返回 50000 条记录
tx := db.Begin()          // 外层可开启大事务

for i := 0; i < len(rows); i += batchSize {
    end := min(i+batchSize, len(rows))
    batch := rows[i:end]
    
    // 构建动态 INSERT
    cols := "(username, email, status)"
    placeholders := strings.Repeat("(?, ?, ?),", len(batch))
    sql := fmt.Sprintf("INSERT INTO users %s VALUES %s", cols, placeholders[:len(placeholders)-1])
    
    var args []any
    for _, r := range batch {
        args = append(args, r.Username, r.Email, r.Status)
    }
    
    tx.Exec(sql, args...) // 单批提交
}
tx.Commit()

[!CAUTION] 批量插入注意事项

  • 包大小限制:MySQL 默认 max_allowed_packet 为 64MB,超大批次会报 Packet too large 错误
  • 长事务锁表:单个大事务持有锁的时间越长,死锁概率越高 → 每批独立 commit 更安全
  • 自增 ID 碎片:大批量 INSERT 会导致自增 ID 跳跃,不影响功能,但会影响 AUTO_INCREMENT 当前值的准确性

关联笔记