跳到主要内容

数据操作语言(DML)

问题

SQL 中 INSERT、UPDATE、DELETE 怎么用?MERGE 是什么?如何安全地修改数据?

答案

DML(Data Manipulation Language,数据操作语言) 用于对表中的数据进行增删改操作。虽然数据分析师的日常以 SELECT 查询为主,但理解 DML 对于数据清洗、ETL、临时表操作等场景非常重要。


INSERT 插入数据

基本语法

-- 方式 1:指定列名(推荐,即使列顺序变化也不受影响)
INSERT INTO users (name, email, age) VALUES ('张三', 'zhang@example.com', 25);

-- 方式 2:省略列名(必须按照表定义的列顺序提供所有值)
INSERT INTO users VALUES (1, '张三', 'zhang@example.com', 25, NOW());

-- 方式 3:插入多行(一次性插入,性能远好于逐行插入)
INSERT INTO users (name, email, age) VALUES
('张三', 'zhang@example.com', 25),
('李四', 'li@example.com', 28),
('王五', 'wang@example.com', 30);
批量插入性能

逐行 INSERT 需要每次都和数据库通信一次。批量 INSERT 只需要一次通信,性能差距可达 10~100 倍。在做数据导入时,务必使用批量插入。

INSERT ... SELECT

从查询结果中插入数据,常用于数据迁移和 ETL:

-- 将查询结果插入另一张表
INSERT INTO user_archive (name, email, age)
SELECT name, email, age FROM users WHERE created_at < '2023-01-01';

-- 创建汇总表
INSERT INTO monthly_stats (month, total_orders, total_revenue)
SELECT
DATE_FORMAT(order_date, '%Y-%m'),
COUNT(*),
SUM(amount)
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m');

INSERT ... ON DUPLICATE KEY UPDATE(MySQL)

插入数据时,如果主键或唯一键冲突,则更新已有记录:

-- 如果 email 已存在,更新 name 和 age
INSERT INTO users (email, name, age) VALUES ('zhang@example.com', '张三', 26)
ON DUPLICATE KEY UPDATE name = VALUES(name), age = VALUES(age);

-- 常见场景:统计表的 upsert(有则更新,无则插入)
INSERT INTO daily_stats (stat_date, page_views, unique_visitors)
VALUES ('2024-01-15', 1000, 500)
ON DUPLICATE KEY UPDATE
page_views = page_views + VALUES(page_views),
unique_visitors = VALUES(unique_visitors);

INSERT ... ON CONFLICT(PostgreSQL)

PostgreSQL 的等价写法:

INSERT INTO users (email, name, age) VALUES ('zhang@example.com', '张三', 26)
ON CONFLICT (email)
DO UPDATE SET name = EXCLUDED.name, age = EXCLUDED.age;

-- 冲突时什么都不做
INSERT INTO users (email, name, age) VALUES ('zhang@example.com', '张三', 26)
ON CONFLICT (email) DO NOTHING;

UPDATE 更新数据

基本语法

-- 更新单个字段
UPDATE users SET age = 26 WHERE id = 1;

-- 更新多个字段
UPDATE users SET name = '张三丰', age = 30, updated_at = NOW() WHERE id = 1;

-- 基于表达式更新
UPDATE products SET price = price * 1.1 WHERE category = '电子'; -- 涨价 10%
没有 WHERE 的 UPDATE 会更新所有行!
-- ⚠️ 危险操作:没有 WHERE 条件
UPDATE users SET status = 'inactive';
-- 这会把所有用户的状态都改成 inactive!

安全措施

  1. 先用 SELECT 验证 WHERE 条件
  2. 在事务中执行,确认无误后再提交
  3. MySQL 开启 sql_safe_updates 模式

UPDATE ... JOIN(多表更新)

-- MySQL:根据另一张表的数据更新当前表
UPDATE orders o
JOIN users u ON o.user_id = u.id
SET o.user_name = u.name
WHERE o.user_name IS NULL;

-- PostgreSQL:使用 FROM 子句
UPDATE orders o
SET user_name = u.name
FROM users u
WHERE o.user_id = u.id AND o.user_name IS NULL;

UPDATE ... 子查询

-- 将每个用户的订单总数更新到用户表中
UPDATE users u
SET order_count = (
SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id
);

-- 只更新特定用户
UPDATE users
SET vip_level = 'gold'
WHERE id IN (
SELECT user_id FROM orders
GROUP BY user_id
HAVING SUM(amount) > 10000
);

CASE WHEN 批量条件更新

-- 根据不同条件设置不同的值
UPDATE products
SET status = CASE
WHEN stock = 0 THEN 'out_of_stock'
WHEN stock < 10 THEN 'low_stock'
ELSE 'in_stock'
END
WHERE category = '电子';

-- 批量更新不同的值(避免多条 UPDATE)
UPDATE users SET age = CASE id
WHEN 1 THEN 25
WHEN 2 THEN 28
WHEN 3 THEN 30
END
WHERE id IN (1, 2, 3);

DELETE 删除数据

基本语法

-- 删除满足条件的行
DELETE FROM users WHERE id = 1;

-- 删除符合子查询条件的行
DELETE FROM users WHERE id IN (
SELECT user_id FROM blacklist
);
没有 WHERE 的 DELETE 会删除所有数据!
-- ⚠️ 极其危险
DELETE FROM users;
-- 删除 users 表的所有数据!

-- 如果要清空表,用 TRUNCATE 更快
TRUNCATE TABLE users;

DELETE vs TRUNCATE

对比DELETETRUNCATE
WHERE 条件✅ 支持❌ 不支持(删除所有)
事务✅ 可回滚⚠️ DDL 操作,MySQL 隐式提交
触发器✅ 触发❌ 不触发
自增 ID继续递增重置为 1
性能慢(逐行删除)快(直接释放数据页)
适用场景按条件删除部分数据清空整张表

软删除(推荐)

实际开发中,很少直接 DELETE 数据,通常使用软删除

-- 不是真的删除,而是标记为已删除
UPDATE users SET is_deleted = 1, deleted_at = NOW() WHERE id = 1;

-- 查询时过滤已删除的数据
SELECT * FROM users WHERE is_deleted = 0;
软删除的好处
  1. 数据可恢复,不怕误操作
  2. 可以追踪删除时间和删除人
  3. 关联数据不会出现孤立记录
  4. 符合数据合规要求(GDPR 等)

DELETE ... JOIN(多表关联删除)

-- MySQL:删除黑名单用户的所有订单
DELETE o FROM orders o
JOIN blacklist b ON o.user_id = b.user_id;

-- PostgreSQL:使用 USING
DELETE FROM orders o
USING blacklist b
WHERE o.user_id = b.user_id;

MERGE / UPSERT

MERGE 语句(也叫 UPSERT)可以在一条 SQL 中同时执行 INSERT 和 UPDATE:有则更新,无则插入。

SQL Server / Oracle 的 MERGE

MERGE INTO target_table t
USING source_table s ON t.id = s.id
WHEN MATCHED THEN
UPDATE SET t.name = s.name, t.amount = s.amount
WHEN NOT MATCHED THEN
INSERT (id, name, amount) VALUES (s.id, s.name, s.amount);

MySQL 的 REPLACE INTO

-- 如果主键冲突,先 DELETE 再 INSERT(注意:会重置自增 ID)
REPLACE INTO users (id, name, email) VALUES (1, '张三', 'new@example.com');
REPLACE INTO 的陷阱

REPLACE INTO 实际上是 DELETE + INSERT,会导致:

  1. 自增 ID 变化
  2. 触发 DELETE 和 INSERT 的触发器
  3. 关联的外键记录可能被级联删除

推荐使用 INSERT ... ON DUPLICATE KEY UPDATE 替代。


安全操作最佳实践

1. 先 SELECT 再操作

-- 第一步:用 SELECT 验证条件
SELECT * FROM users WHERE created_at < '2023-01-01' AND is_deleted = 0;
-- 确认结果是否符合预期

-- 第二步:确认无误后执行操作
DELETE FROM users WHERE created_at < '2023-01-01' AND is_deleted = 0;

2. 使用事务

-- 开启事务
START TRANSACTION;

-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 检查结果
SELECT * FROM accounts WHERE id IN (1, 2);

-- 确认无误后提交(或回滚)
COMMIT; -- 提交
-- ROLLBACK; -- 如果有问题就回滚

3. MySQL 安全模式

-- 开启安全更新模式:禁止没有 WHERE 或没有用到索引的 UPDATE/DELETE
SET sql_safe_updates = 1;

-- 此时以下操作会报错
DELETE FROM users; -- 没有 WHERE → 报错
UPDATE users SET status = 'inactive'; -- 没有 WHERE → 报错
UPDATE users SET age = 20 WHERE name = '张三'; -- name 没有索引可能也报错

4. LIMIT 限制影响行数

-- 限制更新/删除的行数,防止意外影响过多数据
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 10000;
-- 分批删除,避免长时间锁表

常见面试问题

Q1: DELETE、TRUNCATE、DROP 有什么区别?

答案

操作作用事务回滚触发器保留表结构
DELETE删除行(可带条件)✅ 可回滚✅ 触发✅ 保留
TRUNCATE清空所有行⚠️ 不可回滚❌ 不触发✅ 保留
DROP删除整张表⚠️ 不可回滚❌ 不触发❌ 不保留

速度:DROP > TRUNCATE > DELETE

Q2: 如何高效删除大表中的大量数据?

答案

-- ❌ 一次删除百万行,会长时间锁表
DELETE FROM logs WHERE created_at < '2023-01-01';

-- ✅ 分批删除
WHILE 还有待删除的行:
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 10000;
SLEEP(0.1); -- 留出时间给其他查询

更好的方案:

-- MySQL 分批删除脚本
DELIMITER $$
CREATE PROCEDURE batch_delete()
BEGIN
DECLARE done INT DEFAULT 0;
REPEAT
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 10000;
SET done = ROW_COUNT(); -- 本次删除的行数
SELECT SLEEP(0.1);
UNTIL done = 0 END REPEAT;
END$$
DELIMITER ;
CALL batch_delete();

Q3: INSERT INTO ... SELECT 和 CREATE TABLE ... AS SELECT 的区别?

答案

-- INSERT INTO ... SELECT:向已存在的表插入数据
INSERT INTO archive_users SELECT * FROM users WHERE created_at < '2023-01-01';
-- 前提:archive_users 表必须已经存在且结构匹配

-- CREATE TABLE ... AS SELECT (CTAS):创建新表并填充数据
CREATE TABLE archive_users AS SELECT * FROM users WHERE created_at < '2023-01-01';
-- 自动创建表,但不会复制索引、约束、自增设置等

Q4: 什么是幂等性?DML 操作如何保证幂等?

答案

幂等性:同一个操作执行一次和执行多次,结果相同。

-- ❌ 非幂等:多次执行结果不同
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
-- 第一次:balance 从 1000 变为 1100
-- 第二次:balance 从 1100 变为 1200

-- ✅ 幂等:多次执行结果相同
UPDATE accounts SET balance = 1100 WHERE id = 1;
-- 不管执行几次,balance 都是 1100

-- ✅ INSERT 保证幂等:使用 ON DUPLICATE KEY
INSERT INTO configs (key, value) VALUES ('max_retry', '3')
ON DUPLICATE KEY UPDATE value = '3';

Q5: UPDATE 返回值中 Rows matchedRows changed 的区别?

答案

UPDATE users SET name = '张三' WHERE id = 1;
-- Query OK, 0 rows affected (0.00 sec)
-- Rows matched: 1 Changed: 0 Warnings: 0
  • Rows matched:满足 WHERE 条件的行数(找到了 1 行)
  • Rows changed:实际被修改的行数(值没变化就不算修改)

如果 name 本来就是"张三",matched=1 但 changed=0。这个区别在判断"操作是否生效"时很重要。


相关链接