🔥 本文专栏:MySQL
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录 :
真正的强大,是看清了自己的平庸之后,依然决定去创造不平庸的生活。自知之明不该是自卑的借口,而应是精准发力的杠杆。
引入
在上一篇博客中,我已经对表的约束以及 DDL(Data Definition Language)进行了详细讲解。我们知道,一张表在逻辑上由两部分构成:一是表中实际存储的数据,二是表结构的定义,即表的元数据(metadata)。其中,DDL 语句主要围绕表的元数据展开,用于定义、修改或删除表结构等内容。
而本文将要介绍的 DML(Data Manipulation Language),则是针对表中实际存储的数据进行操作。换言之,DML 关注的是"数据本身"这一层面,而非结构定义。
具体而言,对表中数据的操作可以抽象为四类基本操作:增(INSERT)、删(DELETE)、改(UPDATE)以及查(SELECT),通常也被统称为 CRUD 操作。因此,本文的内容也将围绕这四个方面展开,逐一进行分析与讲解,以帮助读者建立对数据操作体系的整体认知。
DML
增INSERT
本文首先介绍的第一个操作是增(INSERT) 。我们知道,对于表中存储的数据,其逻辑结构 可以类比为一个一维的 struct 结构体数组。数组中的每一个结构体实例对应一条记录,而结构体中的各个属性字段,则对应表结构中定义的各个列。
需要强调的是,这种"结构体数组"仅是一种逻辑抽象模型 ,用于帮助理解表数据的组织方式。在实际的物理存储层面(以 InnoDB 引擎为例),数据是以B+ 树索引结构进行组织和管理的,而非简单的线性数组。
既然插入的是一条记录,而一条记录由多个字段(即多个列)组成,那么插入操作可以类比为在 C++ 中创建并初始化一个对象 :
在创建对象时,可以为所有成员变量赋值,也可以只对部分成员变量进行初始化------前提是构造函数为未提供的参数定义了缺省值(默认参数)。表的插入操作在语义上与此类似。
基于此,表的插入可以分为两类:全列插入 与指定列插入。
首先来看全列插入的语法:
sql
INSERT INTO 表名 VALUES (属性值1, 属性值2, ...);
全列插入要求按照表定义中列的顺序,提供所有列对应的值。
如果某些列定义了缺省值(DEFAULT),则可以不为所有列显式提供值,此时需要使用指定列插入的方式,仅为部分列赋值:
sql
INSERT INTO 表名 (列名1, 列名2, ...) VALUES (属性值1, 属性值2, ...);
需要注意的是,并非所有列在指定列插入时都可以被省略。DDL 阶段定义的约束将直接决定 DML 操作的自由度,具体规则如下:
- 存在
DEFAULT值:未显式赋值时,将自动使用默认值。 - 允许
NULL:未赋值时,默认填充为NULL。 - 自增列(
AUTO_INCREMENT):未赋值时,由数据库自动生成递增值。 - 反例 :若某列被定义为
NOT NULL且未设置DEFAULT,则插入时必须显式提供该列的值,否则会报错。
进一步地,在 C++ 中,如果需要创建多个对象,通常需要多次调用构造函数;类似地,在 MySQL 中,如果需要插入多条记录,可以多次执行 INSERT 语句。但实际上,MySQL 支持单条 SQL 语句插入多行记录:
sql
INSERT INTO student (name, age) VALUES
('张三', 18),
('李四', 19),
('王五', 20);
相较于多次执行单行插入,批量插入的效率更高。这主要体现在以下几个方面:
首先,MySQL 是一个基于客户端-服务端架构的网络服务进程 。客户端会将 SQL 语句封装为请求报文,通过网络(通常基于 TCP 协议)发送至 MySQL 服务端。服务端在接收到请求后,会对请求报文中的 SQL 语句进行解析与执行处理,其过程主要包括:
首先进行语法分析(Syntax Analysis) ,用于检查 SQL 语句的语法是否符合规范;随后进行语义分析(Semantic Analysis) ,用于校验相关对象是否存在,例如表、列等;在此基础上,生成对应的执行计划(Execution Plan),最后由执行器按照执行计划完成 SQL 语句的执行。
如果每插入一条记录就发送一条 SQL,那么上述流程将被重复执行多次。而批量插入可以将多条记录合并到一次请求中,从而减少网络往返开销(RTT)以及 SQL 解析与执行的次数。
其次,从存储引擎层面来看,插入操作涉及 CPU 的参与。需要注意的是:
CPU 无法直接操作磁盘数据,数据必须先加载到内存中才能参与计算。
在 InnoDB 存储引擎中,MySQL 数据库服务进程(更具体地说是 InnoDB 存储引擎实例)启动时会分配一块内存区域,即 Buffer Pool,用于缓存数据页。MySQL 以"页"(Page,默认 16KB)作为磁盘与内存之间的数据交换单位。
在批量插入的场景下,连续插入且主键连续或接近连续的多条记录很可能落在同一个数据页中,这意味着:
- 同一页只需加载一次即可被多次复用
- 显著减少磁盘 I/O 次数
- 提升整体写入吞吐性能
因此,从网络层、SQL 执行层以及存储引擎层三个维度来看,批量插入都具有明显的性能优势。
这里还需要注意,对于表中设置了主键的字段,由于主键约束要求该字段唯一且非空,如果插入的记录中主键值已经存在,则插入操作会被拒绝。
但在实际场景中,经常会有这样的需求:
- 如果记录不存在,则执行插入;
- 如果记录已存在,则基于新数据对原记录进行更新。
MySQL 提供了对应的语法支持,即 ON DUPLICATE KEY UPDATE。从语义上看,这种行为类似于 C++ 中 std::map 的 operator[]:当 Key 不存在时插入,存在时则更新对应值。
sql
INSERT INTO student (id, name, score)
VALUES (1, '张三', 60)
ON DUPLICATE KEY UPDATE score = 90, name = '张三-已调分';

其执行流程可以理解为:
- Step 1 :尝试插入记录(如
id = 1)。 - Step 2:检测到主键或唯一索引冲突。
- Step 3 :放弃插入操作,转而执行等价的
UPDATE。
执行完成后,MySQL 会返回 affected rows,用于指示实际发生的行为:
- 1 row affected:表中不存在该主键值,执行的是纯插入(INSERT)。
- 2 rows affected:发生唯一键冲突,执行了更新(UPDATE)(MySQL 用 2 来区分该情况)。
- 0 rows affected:发生冲突,但更新值与原值完全一致,存储引擎判定无需修改数据页。
从实现角度来看,也可以先执行查询再决定插入或更新(即先 SELECT,再 UPDATE 或 INSERT)。但这种方式存在两个问题:
首先是性能层面:
需要执行多条 SQL 语句,会带来多次网络传输(TCP 往返)以及重复的 SQL 解析与执行开销。
更关键的是并发一致性问题 :
单条 SQL 语句在执行层面具有原子性,而将逻辑拆分为多条语句后,中间会出现时间窗口。在 MySQL 的客户端-服务端模型下,每个连接通常对应一个线程,多线程并发访问同一张表时可能产生竞态条件。例如:
- 线程 A 查询发现记录存在;
- 在线程 A 执行更新之前,线程 B 将该记录删除;
- 此时线程 A 再执行 UPDATE,就会出现"目标行不存在"的问题。
其次,如果将"查询是否存在"和"更新"这两个步骤拆分为独立执行,根据上文,我们认识到中间必然会存在一个时间窗口。在并发环境下,这会引入一致性风险。
例如:
在查询阶段判断某条记录存在,但在后续执行更新之前,该记录已被其他事务删除。此时再执行 UPDATE,实际上是对一条已不存在的记录进行操作。
需要注意的是,在 MySQL 中:
- 对不存在的记录执行
UPDATE不会报错 - 而是正常返回执行结果,其
affected rows = 0,表示没有任何行被修改
示例:
sql
UPDATE student SET score = 90 WHERE id = 999; -- 假设 id=999 不存在
-- Query OK, 0 rows affected

这种行为在语法层面是完全合法的,但在业务语义上可能产生误导。如果应用层未对 affected rows 进行检查,可能会误判操作成功,从而引发更严重的业务一致性问题。
更隐蔽的一种风险是:
- 应用层原本的意图是"更新一条已存在的记录"
- 但由于并发删除,该记录已不存在
UPDATE执行成功(无异常),但实际上没有任何数据被修改
因此,从工程实践角度来看:
在依赖"更新是否成功"来驱动业务逻辑时,必须显式检查
affected rows,否则可能产生逻辑错误。
这也是为什么在需要"存在则更新,不存在则插入"的场景下,更推荐使用单条语句(如
ON DUPLICATE KEY UPDATE)来实现 UPSERT,从而避免多语句带来的竞态条件问题。
所以这里INSERT ... ON DUPLICATE KEY UPDATE在执行过程中,当检测到唯一键冲突时,存储引擎会对目标记录加上排他锁(X Lock) ,并在锁机制的保护下完成从冲突检测到更新的整个流程。即使在高并发场景下,多个线程针对同一主键操作,也会通过锁队列进行有序调度,从而保证结果的正确性。INSERT ... ON DUPLICATE KEY UPDATE 是单条语句完成的原子操作,由数据库内核保证一致性。
此外,MySQL 还提供了另一种类似语义的语句:REPLACE INTO。
sql
CREATE TABLE student (
id INT PRIMARY KEY, -- 主键,唯一标识
name VARCHAR(20) NOT NULL,
score INT DEFAULT 0,
gender CHAR(1) DEFAULT 'M' -- 默认为男
);
-- 先插入一条原始数据
INSERT INTO student (id, name, score, gender) VALUES (1, '张三', 60, 'M');
REPLACE INTO student (id, name, score) VALUES (1, '张三', 99);
-- 结果:Query OK, 2 rows affected (0.01 sec)

其执行逻辑与 ON DUPLICATE KEY UPDATE 不同,可以概括为:
- 尝试插入新记录;
- 若发生主键或唯一索引冲突;
- 删除原有记录(Delete);
- 再执行一次插入(Insert)。
因此,REPLACE INTO 的本质是"先删除再插入",而不是在原记录上进行更新。
因此,相比之下,REPLACE INTO 通常被认为是一个更重的操作 ;而 ON DUPLICATE KEY UPDATE 是在原记录上进行更新,代价更低,也更符合大多数"upsert(UPDATE + INSERT)"场景。
查SELECT
在介绍了如何向表中插入一条新记录之后,接下来需要讨论表操作中的一个核心环节------查询(Query)。查询是表操作中最复杂、也是最关键的部分,它对开发者理解 SQL 语义以及表结构设计的能力提出了较高要求。查询的对象,自然是表中已经存在的记录。
sql
SELECT 列名1, 列名2, ... FROM 表名;
该语句构成了查询的最基本语法结构:SELECT 后指定需要获取的列属性,FROM 指定数据来源的表。熟悉我之前文章的读者应该知道,我经常使用 SELECT * FROM 表名 这样的写法,其中 * 为通配符,表示查询表中的所有列。
但需要明确的是,这种写法更多用于教学或调试场景。在真实业务环境中,通常不建议直接使用 SELECT *。原因在于:
- 实际生产环境中的表往往数据规模巨大,动辄数十万甚至上百万行;
- 查询过程需要 CPU 参与,而 CPU 无法直接处理磁盘数据,必须先将数据页从磁盘加载到内存(如 InnoDB 的 Buffer Pool);
- 全表扫描会触发大量磁盘 I/O,将所有数据页逐一读入内存;
- 同时,所有符合条件(或全部)的行数据还需要经过序列化后从服务端传输到客户端,这在数据量大时会带来显著的网络开销(可能达到 GB 级别)。
因此,在实际应用中,查询往往不是"获取全部数据",而是基于条件筛选出所需的子集 。从这个角度看,SELECT 更像是一个"行过滤器(row filter)"或"数据筛选器"。
where子句
既然涉及筛选,就必须提供筛选条件,这正是 WHERE 子句的作用:
sql
SELECT 列名 FROM 表名 WHERE 条件;
WHERE 后跟随的是一个逻辑表达式,该表达式会对每一行记录进行逐条计算。只有当表达式结果为真(TRUE)时,该行才会被保留,否则会被过滤掉。
既然是表达式,就不可避免地涉及各种操作符。常见操作符如下:
| 操作符 | 含义 | C++ 类比 |
|---|---|---|
=, <>, != |
等于、不等于 | ==, != |
BETWEEN ... AND ... |
在某个范围内(闭区间) | val >= min && val <= max |
IN (...) |
在给定集合中 | `val == 1 |
IS NULL |
是否为空 | ptr == nullptr |
LIKE |
模糊匹配 | std::string::find |
这些操作符大多与 C/C++ 中的比较运算符语义类似,但有一个关键差异点------NULL 的处理机制。
在 C/C++ 中,null(或 nullptr)本质上是一个确定的值(通常为地址 0),表示不指向任何有效内存。而在 MySQL 中,NULL 表示"未知(unknown)"或"缺失的值",它并不是一个具体值。
这一语义差异导致:
- 只要表达式中参与运算的任一操作数为 NULL,其结果通常也是 NULL(未知);
- SQL 的逻辑系统是三值逻辑(three-valued logic) ,即结果可能为
TRUE、FALSE或NULL; - 在
WHERE过滤阶段,只有 TRUE 会保留记录 ,FALSE和NULL都会被过滤掉。
需要特别说明的是:
<> 是 SQL 标准定义的不等于运算符,而 != 是 MySQL 的扩展写法,两者在 MySQL 中完全等价。但从可移植性(跨数据库兼容)的角度,更推荐使用 <>。
使用示例:
sql
-- 找出价格正好等于 100 的产品
SELECT name FROM products WHERE price = 100;
-- 找出库存不足 10 件的产品 (小于)
SELECT name, stock FROM products WHERE stock < 10;
对于区间判断:
sql
BETWEEN min AND max
其语义等价于:
cpp
val >= min && val <= max
属于闭区间判断,逻辑相对直观。
使用示例:
sql
-- 找出分数在 60 到 90 之间的学生(包含 60 和 90)
SELECT name, score FROM student WHERE score BETWEEN 60 AND 90;
-- 找出入职日期在 2023 年全年的员工
SELECT name FROM employees WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31';
而对于 IN 操作符,其后会给定一个有限的取值集合。在执行判断时,会检查某一列的属性值是否属于该集合,如果属于,则表达式结果为真。
从语义上来看,IN 本质上是一种集合成员判定(membership test),其逻辑可以等价展开为多个等值比较的"或"运算,例如:
sql
col IN (1, 2, 3)
等价于:
cpp
col = 1 || col = 2 || col = 3
如果类比到 C++,可以理解为对一个集合进行查找操作,例如:
cpp
std::unordered_set<int> s = {1, 2, 3};
if (s.count(col)) {
// 命中集合
}
因此,从实现角度来看,IN 可以理解为"对一个有限集合进行成员检测",而数据库在执行时,也可能基于具体情况将其优化为哈希查找或多重比较。
需要注意的是,IN 与 NULL 结合时要格外谨慎。例如:
sql
col IN (1, 2, NULL)
其中 NULL 并不会参与正常匹配,因为任何与 NULL 的比较结果都为 NULL(未知),而不是 TRUE,因此不会命中该条件。这一点体现了 SQL 的三值逻辑特性,在实际使用中需要特别注意。
使用示例:
sql
-- 找出 ID 为 1, 3, 5, 7 的学生
SELECT name, score FROM student WHERE id IN (1, 3, 5, 7);
接下来是最容易出错的一点:NULL 的判断。
如果尝试使用普通比较运算符:
sql
col = NULL
这是错误的,因为任何与 NULL 的比较结果都是 NULL,而不是 TRUE,最终该行不会被选中。
针对 NULL 的判断,有两种方式:
使用 NULL 安全等于运算符 <=>
该运算符是 MySQL 提供的扩展,用于解决 NULL 比较问题:
- 两个操作数都为非 NULL:行为与
=一致; - 两个操作数都为 NULL:返回
TRUE; - 一个为 NULL,一个不为 NULL:返回
FALSE。
可以类比为一个"增强版"的相等运算符:
cpp
bool safe_equal(int* a, int* b) {
if (a == nullptr && b == nullptr) return true;
if (a == nullptr || b == nullptr) return false;
return *a == *b;
}
使用示例:
sql
-- 这条语句能精准找出 score 确实是 NULL 的记录
SELECT name FROM student WHERE score <=> NULL;
使用 IS NULL / IS NOT NULL(推荐)
更推荐的写法是:
sql
col IS NULL
col IS NOT NULL
原因在于:在 SQL 语义中,NULL 更像是一种"状态"而不是"值"。因此,使用 IS NULL 更符合 SQL 的设计哲学,也更具可读性。
在复杂查询中,这种写法能够清晰表达"字段是否缺失"的语义,使代码的意图更加明确,也更利于团队协作与维护。
使用示例:
sql
--假设我们有一个学生表,有些学生还没来得及录入电子邮箱(email 字段为 NULL),我们现在要给所有留了邮箱的学生发通知
SELECT name, email FROM student WHERE email IS NOT NULL;
接下来介绍 LIKE 操作符 。LIKE 的操作数是字符串类型,主要用于进行模式匹配(pattern matching),即所谓的模糊查询。在使用时,通常需要借助通配符来描述匹配规则。
在 LIKE 表达式中,有两个最核心的占位符:
%(百分号) :匹配任意数量(包括 0 个)的字符。_(下划线) :匹配恰好一个字符。
sql
-- 1. 匹配所有姓张的学生(张三、张小凡等)
SELECT * FROM student WHERE name LIKE '张%';
-- 2. 匹配名字中包含"志"的学生(如:李志、王志强、志存高远)
SELECT * FROM student WHERE name LIKE '%志%';
-- 3. 匹配姓张且名字为两个字的学生(如:张三、张飞)
SELECT * FROM student WHERE name LIKE '张_';
LIKE 的具体执行效率与所在列是否有索引、以及模式的写法密切相关,相关细节会在索引章节展开。
最后就是关于 AND 和 OR 逻辑运算符。它们的作用是连接多个逻辑表达式,其行为与 C/C++ 中的 && 和 || 运算符基本一致。
对于 AND,只有当参与运算的所有表达式结果均为真 时,整体结果才为真;而对于 OR,只要任意一个表达式结果为真,整体结果即为真。
在运算优先级方面,C++ 中 && 的优先级高于 ||,SQL 同样遵循这一规则。在 SQL 中,AND 的优先级高于 OR。然而,在实际编写较为复杂的 WHERE 子句时,这一默认优先级往往容易引发逻辑错误,尤其是对于初学者而言。
逻辑规则 :AND 的优先级高于 OR。
示例陷阱:假设需求是查询"1 班或 2 班中,数学成绩及格的学生"。
sql
-- 错误写法:
SELECT * FROM student WHERE class_id = 1 OR class_id = 2 AND math >= 60;
- 实际执行逻辑 :
class_id = 1 OR (class_id = 2 AND math >= 60) - 查询结果:返回"2 班中数学及格的学生"以及"1 班的所有学生"(即使 1 班中存在不及格的情况也会被包含)
最佳实践:始终使用括号显式地表达逻辑意图,例如:
sql
SELECT * FROM student
WHERE (class_id = 1 OR class_id = 2) AND math >= 60;
这样不仅可以避免优先级带来的歧义,也能显著提升 SQL 语句的可读性和可维护性。
此外,还需要特别注意的是:由于 NULL 表示"未知",在 SQL 中逻辑运算并非简单的二值逻辑(TRUE / FALSE),而是三值逻辑(TRUE / FALSE / NULL) 。因此,当 AND 和 OR 与 NULL 共同参与运算时,其结果需要额外关注。
AND 运算规则:
TRUE AND TRUE= TRUETRUE AND FALSE= FALSETRUE AND NULL= NULLFALSE AND NULL= FALSENULL AND NULL= NULL
OR 运算规则:
TRUE OR FALSE= TRUETRUE OR NULL= TRUEFALSE OR NULL= NULLNULL OR NULL= NULL
可以总结出一个更具指导意义的规律:
-
当
NULL与一个足以决定结果的值 组合时,NULL会被"吞掉":FALSE AND NULL = FALSETRUE OR NULL = TRUE
-
当
NULL与一个**无法单独决定结果的值(如 NULL 本身)**组合时,结果仍为NULL
从直觉上理解也较为自然:
- "未知 AND 假"必然为假(因为
AND只要有一个为假即可确定结果) - "未知 OR 真"必然为真(因为
OR只要有一个为真即可确定结果)
因此,在涉及 NULL 的条件判断时,应格外谨慎,必要时结合 IS NULL 或 IS NOT NULL 显式处理,以避免隐式逻辑带来的歧义。
总结来看,SELECT + WHERE 的组合,本质上是在对表数据执行一次"基于条件的投影与过滤"。理解其执行语义(尤其是三值逻辑与 NULL 处理),是写出正确且高效 SQL 的基础。
在认识了 WHERE 子句之后,需要特别强调一个关键细节:不要将 SQL 语句的执行过程误解为类似 C/C++ 那样"从左到右逐行执行"。
对于 SELECT 语句而言,我们确实可以只提取需要的列,而不是返回整行的所有列。但有些读者容易产生一个误区,认为 SQL 的执行顺序就是按照书写顺序从左到右进行,例如"先执行 SELECT 提取列,再执行 WHERE 进行行过滤"。这种理解实际上是不成立的。
我们可以通过一个简单的逻辑推导来说明这一点的不合理性:
WHERE 子句是以行为单位 进行判断的,它在执行时能够访问该行的所有列属性,并且支持跨列计算与条件校验。例如,若要查询"学生表中数学成绩大于 80 的学生姓名和学号",如果按照"先选列再过滤"的顺序执行,那么在只保留"姓名和学号"两列之后,WHERE 子句将无法获取"数学成绩"这一列的数据,从而导致条件无法计算,这显然是矛盾的。
因此,SQL 的实际执行顺序并非书写顺序,而是遵循一套更符合计算逻辑与执行效率的流程:
FROM:确定数据来源,即从哪张表(或哪些数据源)获取数据;WHERE:对数据进行行级过滤,基于条件表达式筛选出符合要求的记录;SELECT:在已经筛选出的结果集上,提取需要返回的列。
从执行机制的角度来看,这一顺序也是必然的:
WHERE 子句的计算需要 CPU 参与,而 CPU 无法直接处理磁盘中的数据,必须先将数据加载到内存中。在 InnoDB 存储引擎中,数据是以页(page)为基本单位进行管理的,每页大小为 16KB。因此,数据库需要对表对应的 B+ 树叶子节点进行扫描(在 WHERE 条件列没有索引的前提下),将数据页逐步加载到 Buffer Pool 中,然后逐行执行 WHERE 条件判断,筛选出符合条件的记录集合,最后再对这些结果提取指定的列。
总结来看:
- SQL 的书写顺序是面向人的,强调表达意图:我要什么 → 从哪里获取 → 满足什么条件;
- SQL 的执行顺序是面向机器的,强调效率:从哪里获取 → 过滤数据 → 提取结果。
SQL 的书写顺序是为了让人类读懂(我要什么 -> 从哪拿 -> 满足什么条件),而它的执行顺序是为了让机器高效(从哪拿 -> 满足什么条件 -> 提取我要的)。
其本质上遵循的是一个核心原则:
先选行(减少数据量),再选列(减少数据宽度)
正是这种执行策略,使得数据库在面对海量数据时,依然能够保持较高的执行效率与良好的性能表现。
group by
根据上文,我们已经认识到,SELECT 语句不仅可以进行行级筛选,还可以对符合条件的行提取指定列的数据。但在实际应用中,还存在更复杂的需求。
例如,假设有一张学生表,用于记录学生的个人信息及学习情况。此时我们可能需要执行如下查询:统计某个班级的数学平均成绩,以及计算该班级中数学成绩大于 60 分(即及格)的人数。
在这种场景下,仅依赖前文介绍的 WHERE 子句是无法满足需求的。其根本原因在于:WHERE 的作用范围是单行级别 ,即针对每一行记录,将该行的列值代入逻辑表达式进行判断,结果为真则保留,否则过滤。因此,WHERE 本质上是"行过滤器",只能处理逐行判断的问题。
而上述需求(如平均值、总数等)本质上是对整个数据集合 进行计算。例如,计算班级的数学平均成绩,需要将表中所有非 NULL 的数学成绩进行求和,再除以对应的人数。这一过程显然已经超出了"单行处理"的范畴,而是需要对整张表(或一个数据子集)进行整体计算。
因此,这里就需要引入聚合函数(Aggregate Functions)。
所谓聚合函数,可以理解为:将多行数据压缩为一个结果值的函数。这一描述可能略显抽象,可以结合上述例子理解:
在计算平均成绩时,系统会遍历所有符合条件的记录,将其中的数学成绩进行累加并参与计算,最终只返回一个结果值(平均分),而不是多行数据。这个过程,本质上就是将"多行输入"压缩为"单值输出"。
由此可以总结:
- WHERE子句:作用于单行,输出仍为多行(子集)
- 聚合函数(Aggregate):作用于多行,输出为单值(标量)
MySQL 提供了一系列常用的聚合函数,用于支持常见的统计分析需求:
-
COUNT():计数类似于:
count++ -
SUM():求和类似于:
total += value -
AVG():平均值本质为:
SUM(col) / COUNT(col) -
MAX()/MIN():最大值 / 最小值类似于遍历比较:
if (val > max) max = val
关于各个聚合函数的具体行为,需要注意以下细节:
COUNT(列名):统计该列中非 NULL 的行数COUNT(*):统计表中所有行数(与列是否为 NULL 无关)COUNT(1):语义上等价于COUNT(*)(在大多数引擎中执行效果一致)
各个聚合函数的底层逻辑可以理解为:
COUNT(col):仅当该列值不为NULL时才计数SUM(col):对所有非NULL值进行累加AVG(col):基于SUM(col) / COUNT(col)实现MAX()/MIN():遍历所有值,维护当前最值
此外,还有一个常见误区需要澄清:
很多读者会认为,SELECT 后面只能跟表中定义的列名。但实际上,SELECT 子句可以包含:
- 列名
- 表达式(如常量表达式或包含列的运算表达式)
- 函数(包括聚合函数)
如果是表达式,那么会对每一行 计算一次,并在结果集中新增一列(派生列);
如果是聚合函数,则会对整个数据集合进行计算,并返回单个结果。
对于生成的派生列,其列名默认是表达式或函数本身。为了增强可读性,通常使用 AS 为其指定别名,例如:
sql
SELECT AVG(score) AS avg_score FROM student;
进一步地,我们还需要考虑一个问题:聚合函数的计算对象,是否只能是"整张表"?
答案是否定的。实际上,我们可以对参与统计的数据集合进一步细分。具体而言,可以基于某一列的取值,将原始数据划分为多个子集(即分组)。
仍以前述学生表为例,该表记录了学生的个人信息及学习情况,并包含"班级编号"这一列。此时,可以以"班级编号"作为分组依据,将整张表划分为多个子集:每一个子集对应一个班级编号相同的记录集合。从逻辑上看,这些子集可以理解为若干"虚拟子表"。
在完成分组之后,聚合函数将作用于每一个分组内部,对该分组中的多行数据进行计算,并最终输出一个标量结果。因此,整个过程可以理解为:先分组,再在组内进行聚合计算,从而实现"对多个子集分别统计"的目的。
需要特别注意的是,在 SELECT 子句中,一旦出现聚合函数,则必须满足如下约束:
除聚合函数之外,其余出现在
SELECT列表中的列,必须是分组列(即出现在GROUP BY子句中的列)。
其根本原因在于"标量与集合"的不匹配问题:
- 聚合函数的输出是一个标量值(每个分组对应一个结果)
- 而普通列(未参与分组)在一个分组内可能对应多个不同的值(集合)
例如:如果按照"班级编号"进行分组,但在 SELECT 中同时包含"姓名"字段,那么对于同一个班级,通常会存在多个不同的姓名。此时数据库无法确定应返回哪一个姓名,因此该查询在语义上是不确定的。
这里需要特别注意一个容易产生误解的点:有些读者会认为,当我们在 SELECT 中提取非聚合列(例如"姓名")时,由于该列在一个分组内可能对应多行数据(即一个"向量"),而聚合函数的结果是单一的标量值,这就导致数据库无法确定应当从该向量中选择哪一个值与标量进行对应,从而引发查询失败。
基于这一理解,进一步可能会推导出一个结论:查询失败的根本原因在于非聚合列在分组内存在多个不同的值;如果该列在每个分组中实际上只有一个取值(即"退化"为标量),那么查询在逻辑上就是成立的,也应当可以执行。
例如:假设学生表中某个分组内的所有记录,其"姓名"字段的取值全部相同(例如均为"张三"),从结果语义上来看,此时该字段在分组后实际上是一个唯一确定的标量值,因此提取该非聚合列在逻辑上是没有歧义的。
然而,需要强调的是:MySQL 并不会在执行阶段去验证"该列在分组内是否唯一"这一条件。相反,它采用的是一种更严格且确定性的语法约束:
一旦
SELECT列表中出现聚合函数,那么除聚合列之外的其他列,必须全部出现在GROUP BY子句中,否则查询将被判定为非法(在默认启用ONLY_FULL_GROUP_BY模式下)。
也就是说,MySQL 的判定依据并不是"该列是否只有一个值",而是"该列是否满足分组语义约束"。只要某个列既不是分组列,也没有被聚合函数包裹,即使在实际数据中它恰好只有一个取值,查询仍然会失败。
因此,可以将这一问题总结为:
- 从语义层面:多值(向量)会导致结果不确定,这是规则存在的根本原因
- 从实现层面:数据库并不做值唯一性的判断,而是通过语法规则一刀切地约束查询写法
需要补充说明的是,MySQL 5.7 及以前版本默认不强制此约束,写法上更宽松但易引发问题,5.7 起默认启用 ONLY_FULL_GROUP_BY 与 SQL 标准一致,新版本下直接按规则写即可。
从执行模型的角度来看,GROUP BY 的过程可以抽象为一个"分治(Divide and Conquer)"过程:
-
分流(Split)
按分组键(如班级编号)将原始行集合划分为多个子集,每个子集对应一个分组,逻辑上可理解为一张"虚拟子表"。
-
局部计算(Apply)
聚合函数在每个分组内部执行。其输入是该分组中的所有行,输出是一个单一标量值(如平均值、总和等)。
-
合并结果(Combine)
将各个分组的计算结果汇总,形成最终的结果集。结果集中每一行对应一个分组。
整体来看,GROUP BY 并不是简单的"语法附加项",而是将"全表聚合"推广为"分组聚合"的核心机制,它与聚合函数共同构成了 SQL 中处理统计分析问题的基础模型。
查询示例:
sql
SELECT class_id, AVG(math_score) FROM student GROUP BY class_id;
Having
认识了分组聚合之后,结合前文可知,SELECT 语句的执行并不是从左到右逐行执行的。其执行过程遵循一套固定的逻辑顺序:首先执行 FROM 子句。由于 MySQL 存储数据的基本单位是页(Page),默认大小为 16KB,而查询过程需要 CPU 参与计算,因此存储引擎会将相关数据页按需加载到内存缓冲区中。
随后进入行级过滤阶段,即执行 WHERE 子句。此时系统会将每一行记录代入 WHERE 中的逻辑表达式进行判定:若结果为 TRUE,则保留该行;若为 FALSE 或 NULL,则将该行剔除。
在此基础上,引入分组聚合( GROUP BY )。可以将其理解为:按照分组键将原始数据集划分为多个子集合(逻辑分组),每个分组在逻辑上相当于一张"子表",仍然保留完整的表结构(包括分组列与非分组列)。至于聚合函数的调用,并不发生在 GROUP BY 阶段,而是在后续的 HAVING 判定或 SELECT 输出阶段按需执行------每次调用时,针对某个分组内的多行数据计算出一个标量结果。
如果此时我们希望对"参与聚合计算的数据"进行进一步限制,即只允许满足某些条件的数据进入聚合函数,那么本质上需要在"聚合计算之前"进行一次筛选。换言之,是对分组后的数据再进行过滤,然后再执行聚合运算。
有些读者可能会认为,这种"行过滤"完全可以通过 WHERE 子句实现。这一理解在部分场景下是正确的,但需要特别强调执行顺序:WHERE 的执行早于 GROUP BY。也就是说,WHERE 是对"原始数据集"进行过滤,然后再对过滤后的结果进行分组。
然而,如果需求发生变化,例如:
希望先分组,再基于"每个分组的聚合结果"进行筛选
此时 WHERE 就无法满足需求,因为它无法访问聚合结果。在这种情况下,就必须使用 HAVING 子句。
HAVING 子句是在 GROUP BY 之后执行的,其本质也是一个逻辑表达式,用于对"分组后的结果"进行过滤。对于每一个分组,系统会计算相关表达式(通常包含聚合函数),若结果为 TRUE,则保留该分组;否则(FALSE 或 NULL)将其丢弃。
例如:
sql
SELECT class_id FROM student GROUP BY class_id HAVING AVG(math_score) > 60;
其逻辑执行过程可以抽象为:
text
原始数据(FROM + WHERE 之后):
class_id | name | math
1 | 张三 | 70
1 | 李四 | 80
1 | 王五 | 90
2 | 赵六 | 50
2 | 孙七 | 60
↓ GROUP BY class_id(仅分组,不做压缩)
分组 1: { (1,张三,70), (1,李四,80), (1,王五,90) }
分组 2: { (2,赵六,50), (2,孙七,60) }
↓ HAVING 阶段(基于分组计算聚合结果)
- 分组1:AVG = 80
- 分组2:AVG = 55
→ 仅保留分组1
↓ SELECT 阶段(输出阶段,对每组进行压缩)
分组 1: { (1,张三,70), (1,李四,80), (1,王五,90) }
↓ 剔除非聚合列,仅保留分组列与聚合列,并在此阶段对每个分组调用聚合函数,将多行数据压缩为一个标量值。
class_id | AVG(math_score)
1 | 80
这里需要澄清一个常见疑问:在 GROUP BY 执行之后,但在 SELECT 最终输出之前,系统内部到底保存的是什么结构?
关键点在于:GROUP BY 并不会立即"压缩数据",而是构建一个分组结构(逻辑分组),每个分组中仍然保留完整的原始行数据。
聚合函数并不是在
GROUP BY阶段统一完成计算,而是在后续阶段(如 HAVING 判定或 SELECT输出时)基于分组结果进行计算并产生标量值。
因此,在 HAVING 执行时:
-
每个分组内部的原始行(包括非聚合列)仍然存在
-
可以对分组整体应用聚合函数(如
AVG、COUNT等) -
HAVING可以引用:- 分组列(
GROUP BY中的列) - 聚合函数
- 分组列(
但需要注意:
HAVING不能直接引用未分组的非聚合列(除非被聚合函数包裹)
这一约束来源于 ONLY_FULL_GROUP_BY 模式,其本质是避免"标量与向量不匹配"的语义错误。
从数据形态角度来看,可以这样理解:
- 分组前:列是"向量"(多行数据)
- 分组后(逻辑上):每个分组仍然持有一个"向量"
HAVING阶段:会调用聚合函数计算得到标量,然后利用标量的值代入逻辑表达式进行校验,根据校验的结果即为真,保留该分组,不满足则剔除该分组,即这里是以组为单位进行校验而不是行为单位SELECT阶段:提取聚合列以及调用聚合函数得到标量,最终输出必须是"标量级别的一行数据"
因此,在 SELECT 阶段,每个分组会被"压缩"为一行:
- 分组列:天然是标量(每组唯一)
- 聚合函数结果:标量
- 非聚合列:由于对应多个值(向量),无法映射到单个输出位置,因此必须被排除
这也是为什么:
SELECT和HAVING中都只能出现"分组列 + 聚合函数"的组合
最后,SELECT 阶段不仅负责列的选择与结果输出,还承担结果的表达优化,例如通过 AS 对列进行重命名,提高结果的可读性:
sql
SELECT class_id, AVG(math_score) AS avg_math_score FROM student GROUP BY class_id;
整体来看,WHERE 与 HAVING 的本质区别可以归纳为:
WHERE:作用于分组之前的原始数据HAVING:作用于分组之后的聚合结果
而二者的差异,本质上来源于 SQL 执行顺序与"是否能够访问聚合结果"。
order by
接下来介绍 ORDER BY 子句。ORDER BY 的作用是对查询结果进行排序。需要注意的是,排序规则会受到列的**校对规则(collation)**影响,尤其是在涉及字符串比较时,不同的校对规则可能会导致不同的排序结果。
在实际业务场景中,排序操作非常常见。例如,在一个学生表中,记录了学生的基本信息以及学习情况,我们可能需要按照"数学成绩"字段对所有学生进行排序,并得到一个降序排列的结果,此时就需要使用 ORDER BY 子句来完成这一需求。
ORDER BY 子句后面跟随的是用于排序的列名(或表达式)。
在 SQL 中,排序方向由两个关键字控制:
ASC(Ascending):升序,从小到大(默认值)。DESC(Descending):降序,从大到小。
示例需求:查询学生的姓名和数学成绩,并按数学成绩降序排列:
sql
SELECT name, math FROM student ORDER BY math DESC;
接下来需要重点关注 ORDER BY 在整个查询过程中的执行时机。根据前文分析,SELECT 语句的执行并不是按照书写顺序自左向右逐行执行,而是遵循一套固定的逻辑执行流程:
首先执行 FROM 阶段。MySQL 的数据以页(通常为 16KB)为基本存储单位,查询时需要在存储引擎的配合下,将相关数据页按需加载到内存(Buffer Pool)中,以供 CPU 进行后续处理。
随后执行 WHERE 子句,对每一行记录进行条件判断:若逻辑表达式为 TRUE,则保留该行;若为 FALSE 或 NULL,则将其过滤掉。至此完成行级过滤。
接着进入 GROUP BY 阶段。系统会根据分组列,将过滤后的结果集划分为多个分组(可以理解为若干子集)。需要强调的是,在这一阶段仅完成"分组(分流)",并不会立即执行聚合函数;每个分组内部仍然保留原始的行结构(包含聚合列与非聚合列)。
在分组完成之后,执行 HAVING 子句。与 WHERE 不同,HAVING 的作用对象是"分组"而非"单行记录"。在该阶段通常会涉及聚合函数的计算,或者基于聚合结果进行条件判断。如果某个分组的判断结果为 FALSE 或 NULL,则整个分组都会被丢弃。
完成 HAVING 过滤之后,进入 SELECT 阶段。此时系统会对保留下来的分组执行聚合函数计算,并根据 SQL 标准规则,生成最终的结果列(通常表现为:每个分组对应一行结果)。
最后才是 ORDER BY 阶段。该阶段会基于前面生成的结果集,对其按照指定列进行统一排序(升序或降序)。
由此可以看出,ORDER BY 在整个 SELECT 执行流程中处于较靠后的位置。这种设计是合理的:原始数据量通常较大,如果在未过滤、未分组的情况下直接排序,开销会非常高;而先通过 WHERE 和 GROUP BY 缩小数据规模,再进行排序,可以显著降低排序成本,提高查询效率。
需要注意的是,排序并不局限于单一列------ORDER BY 支持基于多列进行排序。排序规则为:首先按第一列进行排序;当出现第一列值相同的多行时,再按第二列进行排序;以此类推。
sql
SELECT name, class_id, math FROM student ORDER BY class_id ASC, math DESC;
上述语句表示:先按班级编号升序排列,若同一班级内有多条记录,则按数学成绩降序排列。
还需要留意一个细节:ASC 和 DESC 只作用于紧挨着的那一列,并不会扩散到后续列。例如 ORDER BY a, b DESC 中,a 默认为升序、只有 b 是降序;若希望两列都降序,需要对每列分别显式声明 ORDER BY a DESC, b DESC。
这里还需要注意一个关键细节:在 SELECT 子句中,我们不仅可以提取表中已有的列属性,还可以在其后使用表达式 或聚合函数。这些表达式或聚合函数的计算结果,会作为**派生列(derived column)**被添加到结果集中。
需要从"计算粒度"的角度区分两类情况:
- 表达式:其输入是当前结果集中的"逐行数据"(即表或分组后的结果中的每一行记录的列值),因此表达式的计算结果也是一个"向量"(一行对应一个结果)。
- 聚合函数 :其输入是一个"分组"(如果没有
GROUP BY,则视为单一整体分组),即多行记录的某一列值集合,其输出是一个标量值(每个分组产生一个结果)。
对于这些派生列,如果不显式指定名称,系统通常会使用表达式本身或聚合函数名作为列名。但在实际开发中,这种默认命名往往可读性较差,因此通常会通过 AS 为其指定别名(alias),以提升结果集的可理解性。
需要特别强调的是:WHERE 子句中不能直接使用这些别名 。原因在于执行顺序------表达式计算、聚合函数调用以及别名的绑定,都发生在 SELECT 阶段,而 WHERE 的执行早于 SELECT。因此,在 WHERE 执行时,这些别名尚未生成,自然无法引用。

与之相对的是,ORDER BY 子句可以使用别名 。这是因为 ORDER BY 的执行时机位于 SELECT 阶段之后(更准确地说,是在结果集生成之后进行排序),此时派生列及其别名已经确定,可以直接用于排序逻辑。
总结来说,这里体现的是 SQL 执行顺序对语义可见性的直接影响:WHERE 看不到 SELECT 中定义的别名,而 ORDER BY 可以访问这些已经生成的结果列。
LIMIT
接下来介绍 LIMIT 子句。LIMIT 的核心作用是对结果集进行分页或截断展示。
在实际应用场景中,表中往往存储的是百万甚至更大规模的数据 。即便已经通过 WHERE 子句进行了行级过滤,或者通过 GROUP BY 对数据进行了分组(将整体数据集划分为多个子集),并进一步通过 HAVING 对分组结果进行筛选,最终保留下来的结果集规模仍可能较大。这意味着在展示阶段,通常无法一次性将全部数据输出到终端或客户端。
因此,需要通过分页机制来控制结果集的输出规模,而 LIMIT 正是实现这一目的的关键子句。
用法 A:LIMIT n
- 含义 :返回结果集中的前
n条记录。 - 本质上是对结果集做一个"截断",只保留前若干条。
用法 B:LIMIT offset, n
- 含义 :从第
offset条记录之后开始(注意不包含该位置,且从 0 开始计数),向后返回n条记录。 - 常用于分页场景,例如"第 k 页,每页 n 条"。
标准写法与 MySQL 扩展
LIMIT n OFFSET offset是更符合 SQL 标准的写法(例如 PostgreSQL 采用该形式)。LIMIT offset, n是 MySQL 提供的扩展语法。
需要注意的是,LIMIT offset, n 的参数顺序(先偏移量,后数量)在语义上不够直观,初学者容易混淆。因此,在强调可读性和可维护性的场景中,更推荐使用:
sql
LIMIT n OFFSET offset
示例:
sql
-- 取第 11 到第 20 条记录(跳过前 10 条,取 10 条)
SELECT * FROM student LIMIT 10, 10;
-- 等价写法(推荐)
SELECT * FROM student LIMIT 10 OFFSET 10;
SQL 的"逻辑执行顺序"再对齐
为了准确理解 LIMIT 的作用位置,有必要再次梳理 SQL 查询的逻辑执行顺序:
- FROM:数据来源确定(如表扫描或基于 B+ 树的页读取)。
- WHERE:行级过滤(筛选满足条件的记录)。
- GROUP BY:分组(将记录划分为多个组)。
- HAVING:组级过滤(对分组结果进行筛选)。
- SELECT:投影与表达式计算(生成结果列及别名)。
- ORDER BY:全局排序(对结果集进行有序化)。
- LIMIT :最终结果截断(控制输出窗口)。
为什么 LIMIT 必须在最后?
LIMIT 操作的对象,是已经完成筛选、分组、计算以及排序之后的"最终结果集"。换言之,它作用于一个已经"加工完成"的数据流,其职责只是裁剪输出规模,而非参与数据加工过程。
整体而言,可以将 LIMIT 理解为 SQL 执行流水线中的"出口限流器":它不改变数据的内容或结构,仅控制最终输出的规模。
这里可能有读者会产生一个疑问:既然我们已经知道 SQL 的执行顺序和书写顺序并不一致(执行时并非从左到右逐子句执行),那么在书写 SQL 时,是否可以打乱子句的顺序?例如,能否将 ORDER BY 写在 GROUP BY 之前?
答案是否定的。需要明确区分两个层面的"顺序"概念:
书写顺序 :SQL 语法对各个子句的词法位置有硬性规定,必须严格遵循:
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...
如果打乱这一顺序,例如:
sql
-- 语法错误:ORDER BY 不能写在 GROUP BY 之前
SELECT class_id, AVG(math) FROM student ORDER BY class_id GROUP BY class_id;
MySQL 在解析阶段就会直接报错,SQL 甚至还未进入实际执行流程。
执行顺序 :这是另一套独立的固定规则,遵循数据处理的内在逻辑(FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT),由 SQL 的语义决定。
这两套顺序并非"一个固定、一个任意"的关系,而是两套独立的、各自固定的规则:
- 书写顺序:面向人的表达习惯。SQL 子句的书写顺序贴近人类描述需求的自然语言表达:"选出哪些列、从哪张表、满足什么条件、按什么分组、过滤哪些组、排序、截断"------读起来接近一句完整的英语陈述,便于人类理解与书写。
- 执行顺序 :面向机器的处理逻辑。数据必须先被读取(
FROM)才能过滤(WHERE);必须先分组(GROUP BY)才能进行组级过滤(HAVING);必须先形成结果集(SELECT)才能对其排序(ORDER BY);必须先排好序才能截取指定条数(LIMIT)。这是数据处理的内在依赖决定的,无法调整。
因此,"书写顺序与执行顺序不一致" 并不意味着书写可以随意,而是意味着这两套顺序各自独立存在、各自受约束------书写顺序由语法规则强制,执行顺序由语义逻辑强制。理解 SQL 的关键,正是在于接受并区分这两个维度。
改UPDATE
根据上文,我们已经学习了 CRUD 操作中的插入(INSERT)以及查询(SELECT)。其中,查询操作由于涉及筛选、投影、分组、排序等多种语义,是 CRUD 中最复杂、学习成本最高的一部分。接下来需要关注的是更新(UPDATE)操作。
所谓对表的更新,本质上是对表中已存在记录的数据进行修改。更具体地说,是对某一行记录中的一个或多个列属性值进行重新赋值,而不是新增或删除记录。
从执行机制上看,UPDATE 语句在底层通常会隐式地完成一个"定位过程",这一过程在逻辑上类似于一次带条件的 SELECT:数据库引擎首先根据 WHERE 条件定位目标记录所在的数据页,将对应页加载到 Buffer Pool(缓冲池)中;随后在内存中对目标记录的指定列进行修改。修改后的数据页不会立即写回磁盘,而是通过数据库的持久化机制在合适的时机刷盘,从而在性能与数据安全之间取得平衡。
sql
UPDATE student SET math_score = 95 WHERE id = 1;

如果从 C++ 的底层视角来理解,这一过程可以类比为:先通过指针找到目标对象在内存中的地址,然后对该地址所对应的结构体成员进行重新赋值。这种类比有助于理解 UPDATE 的本质------它并不是"重建数据",而是对已有数据进行就地修改(in-place update,具体是否原地修改还取决于存储引擎实现)。
需要特别强调的是 UPDATE 语句中的 WHERE 子句。如果省略 WHERE 条件,UPDATE 将作用于整张表的所有记录,即对每一行数据都执行相同的修改操作。这种行为在语法上是合法的,但在实际生产环境中往往意味着严重的业务风险:原本具有区分度的数据可能被统一覆盖,导致数据语义丢失,而且通常无法通过简单手段自动恢复。
因此,在实际开发中编写 UPDATE 语句时,应当养成以下基本习惯:
- 明确指定 WHERE 条件,确保只作用于目标记录
- 在执行前通过 SELECT 语句先验证筛选范围是否符合预期
例如:
sql
UPDATE student SET math_score = 95, english_score = 90, status = 'excellent' WHERE id = 1;
这一语句表示对同一行记录的多个字段进行同时更新。从程序设计角度来看,这类似于在 C++ 中对一个 struct 的多个成员变量进行连续赋值操作,本质上仍然是对同一数据对象的状态进行修改,只不过涉及多个属性字段而已。
删DELETE
接下来要讲述的最后一个 CRUD 操作是删除(DELETE) ,即从表中移除已经存在的记录。需要注意的是,删除操作在执行过程中,与更新操作类似,实际上也隐含了一次"查询"过程:数据库首先需要通过条件(通常是 WHERE 子句)定位目标记录,本质上相当于执行了一次筛选,然后再对这些记录施加删除行为。
需要强调的是,这里的"删除"并不意味着立即释放该记录所占用的物理空间。
从存储层面来看,MySQL 以 16KB 的页(page) 作为基本的空间分配单位。在页内部,单条记录所占用的空间无法被单独归还给操作系统。这一点可以类比为 C++ 中通过 new 分配了一段内存后,无法对其中的部分字节进行独立释放。因此,"页级分配"只决定了空间回收的粒度,但并不能完整解释 DELETE 为什么采用"标记删除"的机制。
具体而言,DELETE 操作并不会立即物理删除记录,而是先在该记录所在数据页中对应的数据块的元数据区域中打上一个删除标记(delete mark),将其标识为"逻辑上已删除"。后续在有新记录插入时,这些被标记的空间可以被复用。
这种"先标记、后清理"的机制与存储引擎的事务、并发控制等机制密切相关,具体细节在后续章节展开。
sql
DELETE FROM student WHERE id = 1;

如果省略 WHERE 子句,例如执行
DELETE FROM student;,则会对表中的所有记录逐行进行标记删除 ------在业务语义上等价于清空整张表。这类操作是生产环境中最常见且代价极高的误操作之一。因此,在执行 DELETE 之前,务必先通过一条等价的SELECT语句验证 WHERE 条件,确保筛选结果符合预期。类比而言:在 C++ 中,对一个非法指针执行
delete往往会导致程序崩溃;而在 SQL 中,不带 WHERE 条件的DELETE,虽然语法完全合法,但"崩溃"的将是你的业务数据。
结语
那么这就是本篇文章的全部内容,带你认识以及掌握表的DML操作,下一期我会更新多表查询,我会持续更新,希望你能够多多关注,如果本文有帮助到你的话,还请三连加关注,你的支持就是我创作的最大动力!感谢各位大佬对我的支持!
