MYSQL:从增删改查到高级查询
文章目录
- MYSQL:从增删改查到高级查询
- 1. 写在前面
- 2. CRUD 核心概念
- 3. Create:新增数据
- 3.1 语法结构
- 3.2 实践一下
- 3.2.1 单行全列插入
- 3.2.2 单行指定列插入
- 3.2.3 多行数据一次性插入
- 4. Retrieve:检索数据
- 4.1 语法概览
- 4.2 准备实验数据
- 4.3 `SELECT` 子句:选择你想要的列
- 4.3.1 全列查询
- 4.3.2 指定列查询
- 4.3.3 查询字段为表达式
- 4.3.4 为查询结果指定别名
- 4.3.5 结果去重查询
- 4.4 `WHERE` 子句:筛选你想要的行
- 4.4.1 比较运算符
- 4.4.2 逻辑运算符
- 4.4.3 实践一下
- 4.5 `ORDER BY` 子句:按规则排序
- 4.5.1 实践一下
- 4.6 `LIMIT` 子句:分页查询
- 4.6.1 语法结构
- 4.6.2 实践一下
- 5. Update:修改数据
- 5.1 语法结构
- 5.2 实践一下
- 6. Delete:删除数据
- 6.1 语法结构
- 6.2 实践一下
- 7. Truncate:清空表
- 7.1 语法结构
- 7.2 `TRUNCATE` vs. `DELETE`
- 8. 插入查询结果
- 8.1 语法结构
- 8.2 实践一下:数据去重
- 9. 聚合函数
- 9.1 实践一下
- 10. `GROUP BY` 分组查询
- 10.1 准备实验数据
- 10.2 实践一下
- 10.3 `HAVING` 子句:对分组结果进行过滤
- 10.4 `HAVING` 与 `WHERE` 的区别
MYSQL:从增删改查到高级查询
1. 写在前面
掌握数据库的操作是每一位后端开发者的基本功。这其中,最核心、最频繁的就是针对数据的“增、删、改、查”,我们通常称之为 CRUD (Create, Retrieve, Update, Delete)。
本文将从最基础的 CRUD 操作出发,逐步深入到查询中的排序、分页、聚合、分组等高级用法,帮助大家构建一个完整、扎实的 MySQL 数据操作知识体系。我们的目标是:
- 熟练运用 SQL 语句完成对数据的增删改查。
- 掌握
COUNT
,SUM
,AVG
等常用聚合函数。 - 理解分组查询
GROUP BY
的原理,并能结合HAVING
子句进行二次过滤。
2. CRUD 核心概念
CRUD 是四个操作的缩写,它们构成了与数据库交互的基础:
- Create (创建): 向数据库表中添加新的记录。
- Retrieve (读取): 从数据库表中查询并获取数据。
- Update (更新): 修改数据库表中的已有记录。
- Delete (删除): 从数据库表中移除记录。
3. Create:新增数据
我们使用 INSERT
语句向表中添加新数据。
3.1 语法结构
INSERT [INTO] table_name[(column [, column] ...)]
VALUES(value_list) [, (value_list)] ...
value_list
指的是与前面列定义相对应的值列表,例如(1, '张三')
。
3.2 实践一下
首先,我们创建一个用于演示的 users
表。
-- 创建一个简单的用户表
CREATE TABLE users (id BIGINT,name VARCHAR(20) COMMENT '用户名'
);
3.2.1 单行全列插入
这是最直接的插入方式,值的顺序和数量必须与表定义的列完全一致。
-- 插入第一条记录
INSERT INTO users VALUES (1, '张三');
-- 插入第二条记录
INSERT INTO users VALUES (2, '李四');
3.2.2 单行指定列插入
更推荐的方式是明确指定要插入的列,这样即使表结构发生变化(如增加新列),SQL 语句也无需修改。
-- 明确指定要为 id 和 name 列插入值
INSERT INTO users(id, name) VALUES (3, '王五');
3.2.3 多行数据一次性插入
为了提升效率,我们可以用一条 INSERT
语句插入多行数据。
-- 一次性插入两行数据
INSERT INTO users(id, name) VALUES (4, '赵六'), (5, '钱七');
小思考:单次插入多行 vs. 多次插入单行
为什么推荐一次性插入多行数据?这主要是出于性能考虑。
- 减少网络开销: 每次执行 SQL 都需要通过网络与数据库服务器通信。合并成一条语句可以显著减少通信次数。
- 降低事务开销: 数据库每执行一条
INSERT
语句,通常会隐式地开启和关闭一个事务。事务的创建和销毁本身是有资源消耗的。将多次插入合并为一次,意味着只在一个事务内完成,效率更高。- 优化磁盘 I/O: 数据库系统可以对批量插入进行优化,减少磁盘写入的次数。
因此,在业务允许的情况下,将多条插入操作合并成一条,是一个非常有效的性能优化手段。
4. Retrieve:检索数据
SELECT
是我们与数据库打交道最频繁的语句,它的功能也最为强大和复杂。
4.1 语法概览
SELECT[DISTINCT] select_expr [, select_expr] ...[FROM table_references][WHERE where_condition][GROUP BY {col_name | expr}, ...][HAVING where_condition][ORDER BY {col_name | expr } [ASC | DESC], ... ][LIMIT {[offset,] row_count | row_count OFFSET offset}]
4.2 准备实验数据
为了更好地演示查询操作,我们先创建一张学生成绩表 exam
并填充一些数据。
-- 创建表结构
CREATE TABLE exam (id BIGINT,name VARCHAR(20) COMMENT '同学姓名',chinese FLOAT COMMENT '语文成绩',math FLOAT COMMENT '数学成绩',english FLOAT COMMENT '英语成绩'
);-- 插入测试数据
INSERT INTO exam (id, name, chinese, math, english) VALUES
(1, '唐三藏', 67, 98, 56),
(2, '孙悟空', 87, 78, 77),
(3, '猪悟能', 88, 98, 90),
(4, '曹孟德', 82, 84, 67),
(5, '刘玄德', 55, 85, 45),
(6, '孙权', 70, 73, 78),
(7, '宋公明', 75, 65, 30);
4.3 SELECT
子句:选择你想要的列
4.3.1 全列查询
使用 *
通配符可以查询表中的所有列。
-- 查询 exam 表中的所有记录和所有列
SELECT * FROM exam;
在实际项目中应避免使用
SELECT *
。因为它会查询所有列,可能导致不必要的数据传输和性能开销。最佳实践是明确指定你需要的列。
4.3.2 指定列查询
明确列出你希望查询的字段。
-- 只查询学生的编号、姓名和语文成绩
SELECT id, name, chinese FROM exam;
此时返回列的顺序由你在
SELECT
语句中指定的顺序决定,与表结构的原始顺序无关。
4.3.3 查询字段为表达式
SELECT
的列表不仅可以是列名,还可以是包含计算的表达式。
-- 查询每个学生的总分
SELECT id, name, chinese + math + english FROM exam;
4.3.4 为查询结果指定别名
使用 AS
关键字可以为查询结果的列指定一个更具可读性的别名。
-- 为总分这一列表达式指定别名为“总分”
SELECT id, name, chinese + math + english AS '总分' FROM exam;
AS
关键字可以省略。如果别名中包含空格或特殊字符,需要用单引号或双引号包裹。
4.3.5 结果去重查询
使用 DISTINCT
关键字可以去除结果集中的重复行。
-- 查询所有出现过的数学成绩,并去除重复值
SELECT DISTINCT math FROM exam;
DISTINCT
会作用于所有指定的查询列,只有当多行的所有列值都完全相同时,才会被视为重复行。
4.4 WHERE
子句:筛选你想要的行
WHERE
子句用于设置查询的过滤条件,只有满足条件的行才会被返回。
4.4.1 比较运算符
运算符 | 说明 |
---|---|
> , >= , < , <= | 大于,大于等于,小于,小于等于 |
= | 等于。注意: NULL = NULL 的结果是 NULL ,而不是 TRUE 。 |
<=> | 安全等于。这个运算符可以安全地比较 NULL 值,NULL <=> NULL 的结果是 TRUE 。 |
!= , <> | 不等于 |
BETWEEN a AND b | 范围匹配,等价于 >= a AND <= b 。 |
IN (val1, val2, ...) | 匹配列表中的任意一个值。 |
IS NULL / IS NOT NULL | 判断值是否为 NULL 。 |
LIKE | 模糊匹配。% 代表零个或多个任意字符,_ 代表一个任意字符。 |
4.4.2 逻辑运算符
运算符 | 说明 |
---|---|
AND | 逻辑与,类似于 Java 中的 && 。 |
OR | 逻辑或,类似于 Java 中的 ` |
NOT | 逻辑非,类似于 Java 中的 ! 。 |
小思考:运算符优先级
AND
的优先级高于OR
。当一个查询中同时包含AND
和OR
时,为了避免逻辑混乱,强烈建议使用括号()
来明确指定运算的先后顺序。
4.4.3 实践一下
-
基本查询:查询英语不及格(小于60分)的同学。
SELECT name, english FROM exam WHERE english < 60;
-
AND
和OR
:查询语文成绩大于80分 且 英语成绩也大于80分的同学。SELECT * FROM exam WHERE chinese > 80 AND english > 80;
-
范围查询:查询语文成绩在 [80, 90] 分区间的同学。
-- 使用 BETWEEN AND SELECT name, chinese FROM exam WHERE chinese BETWEEN 80 AND 90; -- 使用 AND,效果相同 SELECT name, chinese FROM exam WHERE chinese >= 80 AND chinese <= 90;
-
IN
列表查询:查询数学成绩是 78、98 或 99 分的同学。SELECT name, math FROM exam WHERE math IN (78, 98, 99);
-
模糊查询
LIKE
:查询所有姓“孙”的同学。SELECT * FROM exam WHERE name LIKE '孙%';
-
NULL
值查询:查询没有英语成绩的同学。-- 准备一条包含 NULL 的数据 INSERT INTO exam VALUES (8, '张飞', 27, 0, NULL); -- 使用 IS NULL 进行查询 SELECT * FROM exam WHERE english IS NULL;
小思考:
WHERE
子句中为什么不能使用别名?这是一个非常经典的疑问。我们尝试执行
SELECT name, chinese + math + english AS total FROM exam WHERE total < 200;
会发现报错。这与 SQL 的执行顺序有关。一个查询语句的逻辑执行顺序大致如下:
FROM
:确定要查询的表。WHERE
:根据条件筛选行。GROUP BY
:对筛选后的行进行分组。HAVING
:对分组结果进行二次筛选。SELECT
:确定最终要显示的列。ORDER BY
:对最终结果进行排序。从这个顺序可以看出,
WHERE
子句在SELECT
子句之前执行。当WHERE
子句工作时,SELECT
中定义的别名(如total
)还不存在,因此无法使用。而ORDER BY
在SELECT
之后执行,所以它可以使用别名。
4.5 ORDER BY
子句:按规则排序
ORDER BY
用于对最终的查询结果进行排序。
ASC
:升序(默认值)。DESC
:降序。
4.5.1 实践一下
-
单列排序:按数学成绩从高到低(降序)排序。
SELECT name, math FROM exam ORDER BY math DESC;
-
多列排序:先按数学成绩降序排,如果数学成绩相同,再按英语成绩升序排。
SELECT name, math, english FROM exam ORDER BY math DESC, english ASC;
-
使用别名排序:按总分从高到低排序。
SELECT name, chinese + math + english AS '总分' FROM exam ORDER BY '总分' DESC;
小思考:
NULL
值的排序行为在排序时,
NULL
值被认为是最小值。因此,在升序(ASC)排列中,NULL
会出现在最前面;在降序(DESC)排列中,NULL
会出现在最后面。
4.6 LIMIT
子句:分页查询
当数据量很大时,一次性返回所有结果是不现实的。LIMIT
子句可以帮助我们实现分页,每次只查询一部分数据。
4.6.1 语法结构
-- 写法一:从第 start 行开始,取 num 行
LIMIT start, num;-- 写法二(推荐):取 num 行,从第 offset 行开始(offset 从 0 算起)
LIMIT num OFFSET offset;
4.6.2 实践一下
假设每页显示3条记录:
-
查询第一页:
SELECT * FROM exam ORDER BY id ASC LIMIT 3 OFFSET 0;
-
查询第二页:
SELECT * FROM exam ORDER BY id ASC LIMIT 3 OFFSET 3;
小思考:分页公式
假设当前页码为
page_num
(从1开始),每页大小为page_size
,那么OFFSET
的计算公式为:
offset = (page_num - 1) * page_size
5. Update:修改数据
我们使用 UPDATE
语句来修改表中的现有数据。
5.1 语法结构
UPDATE table_name
SET column1 = value1, column2 = value2, ...
[WHERE where_condition]
[ORDER BY ...]
[LIMIT row_count];
5.2 实践一下
-
将孙悟空同学的数学成绩变更为 80 分。
UPDATE exam SET math = 80 WHERE name = '孙悟空';
-
将总成绩倒数前三的同学的数学成绩加上 30 分。
UPDATE exam SET math = math + 30 ORDER BY chinese + math + english ASC LIMIT 3;
小提醒:
UPDATE
语句如果缺少WHERE
子句,将会更新表中的所有行! 这是一个极其危险的操作,在执行前务必再三确认。
6. Delete:删除数据
我们使用 DELETE
语句来删除表中的记录。
6.1 语法结构
DELETE FROM table_name
[WHERE where_condition]
[ORDER BY ...]
[LIMIT row_count];
6.2 实践一下
- 删除孙悟空同学的考试成绩。
DELETE FROM exam WHERE name = '孙悟空';
小提醒:
与
UPDATE
类似,DELETE
语句如果缺少WHERE
子句,将会删除表中的所有数据! 请务必谨慎操作。
小思考:物理删除 vs. 逻辑删除
DELETE
执行的是 物理删除,数据会从磁盘上被移除(虽然可以通过日志恢复,但过程复杂)。在生产环境中,对于核心业务数据,我们通常不建议直接使用DELETE
。更安全的做法是 逻辑删除。这通常通过在表中增加一个状态字段(如
is_deleted
,status
等)来实现。当需要删除一条记录时,我们执行的是UPDATE
操作,将该字段的值标记为“已删除”(例如is_deleted = 1
)。这样做的好处是数据依然保留在数据库中,可以随时追溯和恢复,业务代码在查询时只需增加一个
WHERE is_deleted = 0
的条件即可。
7. Truncate:清空表
TRUNCATE TABLE
语句用于快速删除一个表中的所有数据。
7.1 语法结构
TRUNCATE [TABLE] table_name;
7.2 TRUNCATE
vs. DELETE
虽然 TRUNCATE
和不带 WHERE
的 DELETE
都能清空表数据,但它们有本质区别:
特性 | DELETE FROM table_name | TRUNCATE TABLE table_name |
---|---|---|
操作类型 | DML (数据操作语言) | DDL (数据定义语言) |
执行速度 | 慢,逐行删除 | 非常快,直接释放数据页 |
事务支持 | 支持,可回滚 | 不支持,操作无法回滚 |
自增重置 | 不重置 AUTO_INCREMENT | 重置 AUTO_INCREMENT 计数器 |
触发器 | 会触发 DELETE 触发器 | 不会触发触发器 |
简单来说,如果你只是想彻底清空一个表并重置其状态,TRUNCATE
是更高效的选择。但如果需要可恢复的删除操作,则应使用 DELETE
。
8. 插入查询结果
INSERT ... SELECT ...
是一个非常实用的功能,它允许我们将一个 SELECT
查询的结果直接插入到另一个表中。
8.1 语法结构
INSERT INTO table_name [(column [, column ...])]
SELECT ...;
8.2 实践一下:数据去重
一个经典的应用场景是为已有的大量重复数据进行去重。
-
准备数据:假设
t_recored
表中存在大量重复记录。CREATE TABLE t_recored (id INT, name VARCHAR(20)); INSERT INTO t_recored VALUES (100, 'aaa'), (100, 'aaa'), (200, 'bbb'), (200, 'bbb'), (200, 'bbb'), (300, 'ccc');
-
创建新表并插入去重后的数据:
-- 创建一个与原表结构相同的新表 CREATE TABLE t_recored_new LIKE t_recored; -- 将原表的去重记录插入新表 INSERT INTO t_recored_new SELECT DISTINCT * FROM t_recored;
-
重命名表:最后,通过重命名表,用新表替换旧表,实现无缝切换。
RENAME TABLE t_recored TO t_recored_old, t_recored_new TO t_recored;
小思考:
这种“创建新表 -> 迁移数据 -> 重命名”的模式,是处理线上大表数据变更时一种相对安全和高效的策略。它避免了直接在原表上进行高风险、长时间的操作,保证了数据的完整性和服务的可用性。
9. 聚合函数
聚合函数用于对一组值进行计算,并返回单个值。它们通常与 GROUP BY
子句结合使用。
函数 | 说明 |
---|---|
COUNT(expr) | 统计行数。COUNT(*) 或 COUNT(1) 统计所有行,COUNT(column) 则忽略该列值为 NULL 的行。 |
SUM(expr) | 计算数值列的总和,忽略 NULL 值。 |
AVG(expr) | 计算数值列的平均值,忽略 NULL 值。 |
MAX(expr) | 找出列中的最大值。 |
MIN(expr) | 找出列中的最小值。 |
9.1 实践一下
-
COUNT
:统计有多少学生参加了英语考试(即英语成绩不为NULL
的人数)。SELECT COUNT(english) FROM exam;
-
SUM
:统计所有学生的数学成绩总分。SELECT SUM(math) FROM exam;
-
AVG
:统计英语成绩的平均分。SELECT AVG(english) FROM exam;
-
MAX
/MIN
:同时查询数学最高分和英语最低分。SELECT MAX(math), MIN(english) FROM exam;
10. GROUP BY
分组查询
GROUP BY
子句是数据分析的利器。它将具有相同值的行分组到一起,然后我们可以对每个分组使用聚合函数进行计算。
10.1 准备实验数据
我们创建一个职员表 emp
,用于演示分组查询。
CREATE TABLE emp (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(20) NOT NULL,role VARCHAR(20) NOT NULL,salary DECIMAL(10, 2) NOT NULL
);
INSERT INTO emp(name, role, salary) VALUES
('花味', '老板', 1500000.00), ('晓蜜', '老板', 1800000.00),
('秃头', '程序员', 10000.00), ('衬衫', '程序员', 12000.00),
('小牛', '测试', 9000.00), ('小马', '测试', 8000.00),
('孙悟空', '吉祥物', 956.8), ('猪悟能', '吉祥物', 700.5), ('沙和尚', '吉祥物', 333.3);
10.2 实践一下
-
按角色分组,统计每个角色的人数。
SELECT role, COUNT(*) AS '人数' FROM emp GROUP BY role;
-
按角色分组,统计每个角色的平均、最高和最低工资。
SELECTrole,ROUND(AVG(salary), 2) AS '平均工资',MAX(salary) AS '最高工资',MIN(salary) AS '最低工资' FROM emp GROUP BY role;
10.3 HAVING
子句:对分组结果进行过滤
如果我们想对 GROUP BY
之后的结果进行筛选(例如,只看平均工资大于10000的角色),就必须使用 HAVING
子句。
- 显示平均工资大于 10000 的角色及其平均工资。
SELECTrole,ROUND(AVG(salary), 2) AS '平均工资' FROM emp GROUP BY role HAVING AVG(salary) > 10000;
10.4 HAVING
与 WHERE
的区别
这是一个核心知识点:
WHERE
:在 分组前 对原始表中的行进行过滤。HAVING
:在 分组后 对GROUP BY
产生的结果集进行过滤。
简单来说:WHERE
作用于原始数据,HAVING
作用于分组后的统计结果。WHERE
子句中不能使用聚合函数,而 HAVING
子句中可以。
好了到这里MYSQL基础部分的增删改查+部分高级查询的内容结束了,希望对你有点帮助~