08 表的增删改查下

表的增删改查下

1. update(修改)

1. 语法

UPDATE 用于修改已有数据,必须配合 WHERE 精确更新;并发场景下要用原子更新和事务,避免先查再改。

1
2
3
4
UPDATE 表名
SET1 =1,
2 =2
WHERE 条件;

注意:UPDATE 会对 WHERE 条件匹配到的「所有行」,统一执行同一套修改,匹配多少行,就改多少行。没有 WHERE = 全表修改(极其危险)。

2. 示例

1. 按主键更新(常用、安全)

1
2
3
4
5
# 按主键更新(常用、安全):通过主键id精准定位,只改1
UPDATE users
SET name = '张三', # 把name改成“张三”
age = 20 # 把age改成20
WHERE id = 1; # 只改id=1的所有行

2. 更新单个字段

1
2
3
4
# 更新单个字段:只修改一个字段的值
UPDATE users
SET age = 18 # 仅把age改成18
WHERE id = 100; # 只改id=100的所有行

3. 条件更新多行

1
2
3
4
# 找出 所有最后登录时间早于 2025-01-01 的用户,把它们的 id 全部改成 0
UPDATE users
SET id = 0
WHERE last_login < '2025-01-01';

4. 自增 / 自减更新(高频)

1
2
3
4
5
6
7
UPDATE t1
SET cnt = cnt - 1 # 用当前cnt值减1,直接更新
WHERE id = 10;

UPDATE t2
SET new_count = new_count + 1 # 用当前new_count值加1,直接更新
WHERE id = 5;

注意:不要先 SELECT 查值再 UPDATE(并发场景会出错)。

5. UPDATE + 表达式(能用但要小心)

1
2
3
UPDATE users
SET age = age + 1
WHERE birthday < '2005-01-01';

注意:SET 中写表达式没问题,WHERE 中尽量别对列做计算

3. UPDATE + 子查询(常见)

1. 用查询结果更新

1
2
3
4
5
UPDATE users
SET val = 2 # 把val改成2
WHERE id IN ( # 只改id在子查询结果里的行
SELECT id FROM t3 # 子查询:取t3表中的id
);

2. UPDATE JOIN(非常常用)

1
2
3
4
5
6
# UPDATE JOIN:关联两张表,基于关联条件更新
UPDATE orders o # 给orders表起别名o
JOIN users u ON o.user_id = u.id # 关联orders和users表(通过user_id匹配)
SET o.status = 1 # 把orders表的status改成1
WHERE u.vip = 1; # 只改“关联到的用户是VIP(u.vip=1)”的订单
# 作用:把所有VIP用户的订单,状态设为1

2. Delete(删除)

  • DELETE:按条件删“行”,可回滚,可触发器,慢但安全。
  • TRUNCATE:一刀 清空 整张表,不可回滚,极快,危险。

1. DELETE 删除数据

1. 语法

1
DELETE FROM 表名 WHERE 条件;

注意:没有 WHERE = 清空全表数据!

2. 示例

1. 按条件删除(常用)
1
DELETE FROM users WHERE id = 1;	# 删除users表中所有id=1的行(如果id是主键,只会删1行)
2. 批量删除多行
1
DELETE FROM t1 WHERE age = 18; # 删除t1表中所有年龄等于18的行
3. DELETE + LIMIT(MySQL 特有)
1
2
3
DELETE FROM logs       # 操作logs表
ORDER BY created_at # 按创建时间排序(默认升序,即最早的记录在前)
LIMIT 1000; # 只删除前1000

用途:分批清理数据、防止一次删太多(锁表)。

2. TRUNCATE 截断表(清空表数据)

1. 语法

1
TRUNCATE TABLE 表名;

TRUNCATE 用于想要 快速清空全表、不要数据(清空表数据,但不删除表) 的场景。

对比TRUNCATE(截断表)DELETE(删除)
操作类型DDL(数据定义语言)DML(数据操作语言)
能否回滚不支持(执行后无法恢复数据)支持(事务中可回滚)
清空效率极高(直接重置表,不记录单行操作)低(逐行删除,记录日志)
自增主键重置为初始值(如自增 id 回到 1)保留原自增最大值(如 id 从 100 继续)
适用场景彻底清空表(无需恢复、追求速度)条件删除 / 部分删除(需可控、可回滚)

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 创建t2表,id为自增主键,name为字符串字段
mysql> create table t2(
-> id int auto_increment primary key,
-> name varchar(20));

# 插入3条数据,id自动从1开始递增
mysql> insert into t2(name) values('张三'),('李四'),("王五");

# 查看t2表所有数据,id依次为1、2、3
mysql> select *from t2;
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王五 |
+----+--------+

# 截断t2表:清空所有数据,保留表结构,自增主键重置
mysql> truncate table t2;

# 查看截断后的表,无数据
mysql> select *from t2;
Empty set (0.00 sec)

# 重新插入2条数据,自增id从1重新开始
mysql> insert into t2(name) values ("赵六"),("孙奇");

# 查看重新插入后的数据,id为1、2
mysql> select *from t2;
+----+--------+
| id | name |
+----+--------+
| 1 | 赵六 |
| 2 | 孙奇 |
+----+--------+

3. 插入查询结果

插入查询结果用 INSERT INTO ... SELECT ...,意思是:把 SELECT 查出来的结果,直接插入到另一张表,效率高,常用于数据迁移和批量插入,但要求列数和类型严格匹配!而且 INSERT SELECT 是“一条原子操作”,中途出错、整条语句失败、不会插一半!

1. 语法

插入查询结果的基本用法模板:

1
2
3
4
INSERT INTO 目标表 (列1, 列2, ...)
SELECT1, 列2, ...
FROM 来源表
WHERE 条件;

插入查询结果表示 SELECT 查什么,INSERT 就插入什么。

2. 示例

1. 把一张表的数据复制到另一张表

1
2
3
INSERT INTO x (id, name, age)
SELECT id, name, age
FROM y;

把 x 表里的 id、name、age 的数据全部复制到 y 表,不过需要注意:两边列的 数量和类型要对得上、顺序按 INSERT 的列顺序来。

2. 按条件插入

1
2
3
4
INSERT INTO x (id, name)
SELECT id, name
FROM users
WHERE age = 18;

把 age = 18 的用户插入到 x 表中。

3. 插入“计算后的结果”

1
2
3
INSERT INTO x (id, score)
SELECT id, age * 10
FROM users;

说明:SELECT 里可以是表达式,插入的是 计算结果。所以,这条 sql 语句的意思是:从 users 表中读取所有行的 id,并把 age×10 得到新值,把这些新值插入到 x 表中:users.id 对应 x.idage×10 对应 x.score

4. 从多表查询结果插入(JOIN、进阶)

1
2
3
4
5
# 关联查询+批量插入:从y表和users表关联查询数据,再插回y表
INSERT INTO y (id, name) # 往y表的id、name字段插入数据
SELECT o.id, u.name # 取y表(别名o)的id、users表(别名u)的name
FROM y o # 数据源1:y表,起别名o
JOIN users u ON o.user_id = u.id; # 关联users表(别名u):y.user_id = users.id

3. INSERT SELECT 和普通 INSERT 的区别

1
2
INSERT INTO x VALUES (1, 'test', 20);	# 手写值、一次一行(少量)
INSERT INTO y SELECT * FROM users; # 来源是查询结果、可一次插 成千上万行、效率高

4. 和主键 / 唯一键冲突怎么办?

1. 冲突就失败(默认)

1
INSERT INTO x (id, name) SELECT id, name FROM users;	# 如果id已存在,直接报错,整条失败

2. 忽略冲突行(不推荐滥用)

1
INSERT IGNORE INTO x (id, name) SELECT id, name FROM users;	# 冲突的行被跳过、不报错、容易悄悄丢数据

3. 冲突就更新(常用)

1
2
3
4
5
# 有就更新,没有就插入:
INSERT INTO x (id, name)
SELECT id, name FROM users
ON DUPLICATE KEY UPDATE
name = VALUES(name);

4. 聚合函数

聚合函数可以把多行数据计算成一个结果,常与 GROUP BY 和 HAVING 配合使用,WHERE 负责聚合前过滤,HAVING 负责聚合后过滤。

1. 常见聚合函数一览

函数作用解释
COUNT()计数有多少行
SUM()求和数字加起来
AVG()平均值求平均
MAX()最大值找最大的
MIN()最小值找最小的

1. COUNT(计数、常用)

1
2
3
4
SELECT COUNT(*) FROM users;	# 统计users表一共有多少行数据(*会统计NULL)

SELECT COUNT(列名) FROM users; # 不会统计NULL
SELECT COUNT(email) FROM users; # 只统计email不为NULL的行数

2. SUM(求和)

1
2
SELECT SUM(列名) FROM users;
SELECT SUM(id) FROM users; # 把users表中所有的id加和

注意:列必须是数字、NULL 会被自动忽略。

3. AVG(平均值)

1
2
SELECT AVG(列名) FROM users;
SELECT AVG(age) FROM users; # 统计users表中所有年龄的平均值

注意:NULL 不参与计算、返回值可能是小数。

4. MAX / MIN(最大最小)

1
SELECT MAX(age), MIN(age) FROM users;	# 找出users表中最大和最小年龄

5. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 查看t1表所有数据
mysql> select *from t1;
+-----+--------------+------+
| id | name | age |
+-----+--------------+------+
| 1 | 张三 | 20 |
| 2 | 李四 | 20 |
| 4 | 李四 | 25 |
| 5 | 王五 | 18 |
| 7 | 小明 | 18 |
| 99 | 小红红 | 18 |
| 100 | 我已修改 | 88 |
| 101 | 李四 | 111 |
| 102 | 李四 | 111 |
+-----+--------------+------+

# 统计t1表中id字段的非空行数(即表的总记录数)
mysql> select count(id) from t1;
+-----------+
| count(id) |
+-----------+
| 9 | # 共9条记录
+-----------+

# 计算t1表中所有id字段值的总和
mysql> select sum(id) from t1;
+---------+
| sum(id) |
+---------+
| 421 | # 1+2+4+5+7+99+100+101+102=421
+---------+

# 计算t1表中age字段的平均值(保留4位小数)
mysql> select avg(age) from t1;
+----------+
| avg(age) |
+----------+
| 47.6667 | # (20+20+25+18+18+18+88+111+111)/9 ≈ 47.6667
+----------+

# 同时查询t1表中age字段的最大值和最小值
mysql> select max(age),min(age) from t1;
+----------+----------+
| max(age) | min(age) |
+----------+----------+
| 111 | 18 | # age最大值111,最小值18
+----------+----------+

2. 聚合 + WHERE(常见)

1
2
3
SELECT COUNT(*) 
FROM users
WHERE age = 18;

5. 分组查询

分组查询就是把查询结果按某个字段分成一组一组的,然后每组算一次。

1. 语法

1
2
3
4
SELECT 分组列, 聚合函数
FROM
WHERE 条件
GROUP BY 分组列;

执行顺序:WHERE(先筛选行)→ GROUP BY(再分组)→ 聚合函数(每组算一次)→ HAVING(对分组后的结果筛选)→ ORDER BY(排序) → LIMIT(分页)。

2. 示例

1. 按某个字段分组统计数量

1
2
3
4
# 按age字段分组,统计每个年龄的人数
SELECT age, COUNT(*) AS cnt # 查age值,用COUNT(*)统计每组的行数,别名cnt
FROM users # 查的是users表
GROUP BY age; # 按age字段分组(相同age的行是一组)

2. 分组 + WHERE(常见)

1
2
3
4
5
# 先找age=18的用户,再按id分组统计
SELECT id, COUNT(*) AS my_id # 查id,统计每组行数,别名my_id
FROM users # 查的是users表
WHERE age = 18 # 先找出age=18的行
GROUP BY id; # 再按id分组

3. 分组 + 多个聚合函数

1
2
3
4
5
6
SELECT class_id,  # 按班级分组,展示班级ID
COUNT(*) AS student_count, # 统计每个班级的学生总数(别名student_count)
SUM(age) AS total_age, # 计算每个班级学生的年龄总和(别名total_age)
MAX(age) AS max_age # 找出每个班级学生的最大年龄(别名max_age)
FROM students # 学生表
GROUP BY class_id; # 按班级ID分组(同一班级的学生归为一组)

3. GROUP BY

  • 非聚合列:比如 idage(是直接从表中取的字段,不是聚合计算的结果)。
  • 聚合列:比如 COUNT(*)(是对一组数据计算后的结果)。

SELECT 里出现的非聚合列,必须出现在 GROUP BY 中: SELECT 中出现的 非聚合列(直接取的学生 / 班级字段,如 class_idgender),必须出现在 GROUP BY 里,只有 聚合列(COUNT/SUM/AVG 等计算结果),可以不出现在 GROUP BY 里。这句话可能不要好理解,说白了就是:只要 SELECT 里所有 “直接抄的字段”(非聚合列),在后面 GROUP BY 中出现就合法,不管聚合列有 1 个还是 10 个,都绝对安全,反之,哪怕只漏 1 个非聚合列,就会报错/出脏数据。

1. 错误示例

1
2
3
4
5
6
7
# 错误写法:SELECT里有class_id、gender(两个非聚合列),但GROUP BY只写了class_id
SELECT
class_id, # 非聚合列1
gender, # 非聚合列2(未出现在GROUP BY
COUNT(*) AS student_num # 聚合列(统计人数)
FROM student
GROUP BY class_id; # 仅按班级分组

原因:同一个 class_id 可能有多条 gender,MySQL 不知道该选哪一个。

2. 正确使用

1
2
3
4
5
# class_id是分组依据(非聚合列),出现在GROUP BY里;COUNT(*)是对每个班级的学生数做聚合计算,不需要GROUP BY
SELECT class_id, # 非聚合列(出现在GROUP BY,作为分组依据)
COUNT(*) AS student_num # 聚合列(无需在GROUP BY
FROM student
GROUP BY class_id; # 分组依据:班级ID

或者:

1
2
3
4
5
6
7
# class_id和gender都是非聚合列,且都出现在GROUP BY里,MySQL会按班级+性别组合分组(比如1+男为一组)
SELECT
class_id, # 非聚合列1(加入GROUP BY
gender, # 非聚合列2(加入GROUP BY
COUNT(*) AS student_num # 聚合列
FROM student
GROUP BY class_id, gender; # 所有非聚合列都在GROUP BY

4. GROUP BY 多列分组(实用)

1
2
3
SELECT id, age, COUNT(*)
FROM users
GROUP BY id, age; # 先按id分再按年龄分

5. HAVING —— 分组后的 WHERE

1. 为什么需要 HAVING?

HAVING对「分组后的结果」进行筛选(过滤掉不满足条件的分组),搭配 GROUP BY 使用。

1
2
3
4
5
6
7
8
9
10
11
SELECT class_id,
COUNT(*) AS student_num
FROM student
WHERE age >= 18 # 分组前:只保留年龄≥18的学生
GROUP BY class_id;

SELECT class_id,
COUNT(*) AS student_num
FROM student
GROUP BY class_id
HAVING COUNT(*) >= 3; # 分组后:只保留学生数≥3的班级

2. WHERE vs HAVING

对比WHEREHAVING
筛选时机分组 之前 筛选行分组 之后 筛选分组
作用对象原始行(比如单个学生)分组结果(比如整个班级)
是否依赖 GROUP BY不需要(可单独用)必须搭配 GROUP BY 使用
能否用聚合函数不能(只能用原始字段,如 age < 18能(可以用聚合函数,如 COUNT(*) > 2

3. 示例

1
2
3
4
5
6
SELECT class_id,
COUNT(*) AS student_num
FROM student
GROUP BY class_id # 步骤1:按班级分组统计人数
HAVING COUNT(*) >= 2 # 步骤2:过滤出人数≥2的班级
ORDER BY student_num DESC, class_id ASC; # 步骤3:对过滤后的结果排序
1
2
3
4
5
6
SELECT class_id, gender,
COUNT(*) AS student_num
FROM student
GROUP BY class_id, gender # 步骤1:按“班级+性别”分组统计
HAVING COUNT(*) >= 2 # 步骤2:过滤出人数≥2的分组
ORDER BY student_num DESC, class_id ASC; # 步骤3:对过滤后的结果排序